使用EitherT处理异常

时间:2015-01-22 18:26:17

标签: haskell monad-transformers

我已经在那里问了一个类似的问题:exceptions and monad transformers但不知何故没有正确地表达自己并得到了另一个问题的答案,而不是我要问的问题(至少我是如何解释的)它)。

我现在再次遇到这个问题,让我再试一次,制定我的问题......

我必须编写一个函数来获取可能包含身份验证密钥的服务器,以及一个目标文件,用于保存身份验证密钥内容。

saveAuthKey :: Text -> Server -> IO (Either Text Text)

在三种情况下,该函数可能会返回Left

  1. 目标路径格式错误:不以" file://"
  2. 开头
  3. 服务器未持有身份验证密钥
  4. 将错误保存到文件
  5. 时出现IO错误

    这似乎是EitherT的主要候选人,在我看来。

    所以我从:

    开始
    {-# LANGUAGE OverloadedStrings #-}
    
    import Control.Error
    import Control.Monad.Trans
    import Data.Text (Text)
    import qualified Data.Text as T
    import Data.ByteString (ByteString)
    import qualified Data.ByteString as BS
    import Control.Exception
    
    data Server = Server { authKey :: Maybe ByteString }
    
    main = putStrLn "OK"
    
    saveAuthKey :: Text -> Server -> IO (Either Text Text)
    saveAuthKey path server = do
        result <- try $ runEitherT $ do
            targetFile <- hoistEither $ note "Invalid target file name"
                $ T.stripPrefix "file://" path
            key <- hoistEither $ note "No authentication key for that server!"
                $ authKey server
            lift $ BS.writeFile (T.unpack targetFile) key
    

    我在try之上应用了runEitherT,因此try将结果包装在另一个Either中。不太优雅。但如果我不直接尝试,那么这个例外就不会被抓住。在我之前的问题中,我尝试将try放在lift旁边的runEitherT内,这也不能很好地运作。

    所以,如果你必须用这个签名编写这样一个函数,你将如何处理它?我也理解我应该通过一些例外而不是系统地捕捉SomeException的部分,我不认为它与我的问题直接相关。让我们说try我会发现相关错误(磁盘已满,没有写入权限等)。

    我根本无法做try,让调用者处理它(所有这个函数都在IO monad中,因此存在风险),但在某些时候会有人会必须使用try。同样在我的情况下,我使用hsqml库,这是来自haskell中处理的Javascript的调用,如果我让通过应用程序的异常会崩溃。

    编辑:我将当前的解决方案提交到此问题in this commit。但是我觉得在这个功能中可以更好地实现更好的功能,而无需改变应用程序其余部分的设计。请注意,我会捕获所有异常情况,我知道这些异常情况并不存在,但它现在就会这样做。真的没有什么可以实现的吗?或者我完全以错误的方式解决问题?

1 个答案:

答案 0 :(得分:1)

也许这就是你的意思?我将try推送到可能抛出的特定调用中,并使用bimapEitherT将异常转换为Text

saveAuthKey :: ObjRef ProjectViewState -> Text -> ObjRef (Entity Server) -> IO (Either Text Text)
saveAuthKey _ path (entityVal . fromObjRef -> server) = runEitherT $ do
  (targetFile, key) <- hoistEither $
     (,) <$> note "Invalid target file name"
             (T.stripPrefix "file://" path)
         <*> note "No authentication key for that server!"
             (serverAuthKey server)
  bimapEitherT textEx (const mempty) . EitherT . try $
    BS.writeFile (T.unpack targetFile) key

但是,我觉得这有点过分,因为可以抛出异常的部分本地化为一个调用(BS.writeFile),而可以返回Left的部分都是预先发生的纯计算。当你的代码严重交织EitherTEither逻辑时,IO很不错,但这里的分离非常明确。这是我如何在没有EitherT的情况下编写它:

saveAuthKey :: ObjRef ProjectViewState -> Text -> ObjRef (Entity Server) -> IO (Either Text Text)
saveAuthKey _ path (entityVal . fromObjRef -> server) =
  either (return . Left) save authKey
  where authKey = (,) <$> note "Invalid target file name"
                          (T.stripPrefix "file://" path)
                      <*> note "No authentication key for that server!"
                          (serverAuthKey server)
        save (targetFile, key) = either (Left . textEx) (const (Right ""))
                             <$> try (BS.writeFile (T.unpack targetFile) key)