使用 EitherT 处理异常

handling exceptions with EitherT

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

我现在又遇到这个问题了,让我再尝试制定我的问题...

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

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

函数可能returnLeft三种情况:

  1. 目标路径格式错误:不是以 "file://"
  2. 开头
  3. 服务器没有认证密钥
  4. 将密钥保存到文件时出现 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

我在 runEitherT 之上应用了 try,因此 try 将结果包装在另一个 Either 中。不太优雅。但是如果我不直接把 try 放在那里,异常就不会被捕获。在我之前的问题中,我试图将 try 放在 runEitherT 内的 lift 旁边,但效果也不佳。

那么,如果您必须编写这样一个带有该签名的函数,您会如何处理它?我也理解我应该让 一些 异常通过而不是系统地捕获 SomeException 的部分,我认为这与我的问题没有直接关系。假设使用 try 我会捕获相关错误(磁盘已满、没有写入权限等)。

我根本无法执行 try,让调用者处理它(毕竟这个函数在 IO monad 中,因此存在风险),但在某些时候有人将不得不使用 try。同样在我的例子中,我正在使用 hsqml 库,这是在 haskell 中处理的来自 Javascript 的调用,如果我让异常通过应用程序将崩溃。

编辑:我将当前的解决方案提交给这个问题in this commit。然而,我觉得在这个功能中可以实现更好的东西,而不必改变应用程序其余部分的设计。请注意,我捕获了所有异常,我知道不建议这样做,但现在可以。真的没有比这更好的了吗?还是我完全错误地解决了这个问题?

也许这就是您的本意?我将 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),而可以 return Left 的部分都是预先发生的纯计算。当您的代码将 EitherIO 逻辑严重交织在一起时,EitherT 很好,但这里的分离非常清楚。如果没有 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)