透明的错误处理

时间:2013-10-24 10:24:02

标签: haskell error-handling monads

我正在使用Scotty和Persistent开发REST后端,我无法找到处理错误的正确方法。

我有几个访问DB的功能,如:

getItem :: Text -> SqlPersistM (Either Error Item)

它返回sql monad内部。然后我在我的动作中使用它来检索项目并返回其JSON表示:

get "/items/:name" $ do
    name       <- param "name"
    eitherItem <- lift $ MyDB.getItem name

    case eitherItem of

      Left NotFound -> do
        status status404
        json NotFound

      Left InvalidArgument -> do
        status status400
        json BadRequest

      Right item -> json item

我可以通过引入一些帮助程序使代码更漂亮,但模式将保持不变 - 访问db,检查错误,呈现适当的响应。

我想完全摆脱我的行为中的错误处理:

get "/items/:name" $ do
    name <- param "name"
    item <- lift $ MyDB.getItem name

    -- In case of error, appropriate
    -- HTTP response will be sent,
    -- else just continue

    bars <- lift $ MyDB.listBars

    -- In case of error, appropriate
    -- HTTP response will be sent,
    -- else just continue

    json (process item bars)

即。 getItem可能会返回错误,它将以某种方式转换为json响应,所有这些都对操作代码透明。如果getItem对行动和json回应一无所知,那就太好了。

我过去使用命令式语言解决了这个问题,从各处抛出异常,然后在一个地方捕获并呈现适当的响应。我想Haskell也有可能,但我想知道如何通过使用功能工具来解决这个问题。

我知道monad可能会短路(如Either >> Either >> Either),但不知道如何在这个稍微复杂的情况下使用它。

3 个答案:

答案 0 :(得分:4)

解决方案是使用EitherT monad变换器(来自either包)来处理错误的短路。 EitherT扩展任何monad,其功能与命令式语言中的checked异常完全相同。

这适用于任何“基础”monad,m,让我们假设您有两种类型的计算,其中一些失败,其中一些永不失败:

fails    :: m (Either Error r)  -- A computation that fails
succeeds :: m r                 -- A computation that never fails

然后,您可以将这两个计算都提升到EitherT Error m monad。解除失败计算的方法是将它们包装在EitherT构造函数中(构造函数与类型同名):

EitherT :: m (Either Error r) -> EitherT Error m r

EitherT fails :: EitherT Error m r

注意Error类型现在如何被吸收到monad中,并且不再显示在返回值中。

要解除成功的计算,请使用lift中的transformers

lift :: m r -> EitherT Error m r

lift succeeds :: EitherT Error m r

lift的类型实际上更通用,因为它适用于任何monad变换器。它的一般类型是:

lift :: (MonadTrans t) => m r -> t m r

...在我们的案例中,tEitherT Error

使用这两种技巧,您可以将代码转换为自动短路错误:

import Control.Monad.Trans.Either

get "/items/:name" $ do
    eitherItem <- runEitherT $ do
        name <- lift    $ param "name"
        item <- EitherT $ lift $ MyDB.getItem name
        bars <- EitherT $ lift $ MyDB.listBars
        lift $ json (process item bars)
    case eitherItem of
        Left NotFound -> do
            status status404
            json NotFound
        Left InvalidArgument -> do
            status status400
            json BadRequest
        Right () -> return ()

runEitherT运行EitherT直到它完成或遇到第一个错误。如果计算失败,则eitherItem返回的runEitherT将为Left;如果计算成功,则返回Right

这使您可以在块之后将错误处理压缩为单个case语句。

如果您从我的catch软件包提供的catch导入Control.Error,您甚至可以执行errors行为。这使您可以编写与命令式代码非常相似的代码:

(do
    someEitherTComputation
    more stuff
) `catch` (\eitherItem -> do
    handlerLogic
    more stuff
)

但是,即使您已捕获并处理错误,您仍需要在代码中的某个位置使用runEitherT来打开EitherT。这就是为什么对于这个更简单的例子,我建议直接使用runEitherT而不是catch

答案 1 :(得分:2)

您正在寻找Error monad

你想写一些类似的东西:

get "/items/:name" $ handleErrorsInJson do
    name <- param "name"
    item <- lift $ MyDB.getItem name    
    bars <- lift $ MyDB.listBars    
    json (process item bars)

transformers' ErrorT为现有的Monad添加了错误处理。

为此,您需要使数据访问方法表明它们在Error monad中遇到错误而不是返回Either

getItem :: Text -> SqlPersistM (Either Error Item)

或者你也可以使用像

这样的东西
toErrors :: m (Either e a) -> ErrorT e m a

使用现有功能而不修改它们。快速Hoogling表示已经有m (Either e a) -> ErrorT e m a类型的东西,那就是构造函数ErrorT。配备这个我们可以写:

get "/items/:name" $ handleErrorsInJson do
    name <- lift $ param "name"
    item <- ErrorT $ lift $ MyDB.getItem name    
    bars <- ErrorT $ lift $ MyDB.listBars    
    lift $ json (process item bars)

handleErrorsInJson会是什么?从Ankur的例子中借用handleError

handleErrorsInJson :: ErrorT Error ActionM () -> ActionM ()
handleErrorsInJson = onError handleError

onError :: (e -> m a) -> (ErrorT e m a) -> m a
onError handler errorWrapped = do
    errorOrItem <- runErrorT errorWrapped
    either handler return errorOrItem 

注意:我没有对编译器进行检查,这里可能会出现小错误。 编辑以在看到Gabriel的回复后修复问题。 handleErrorsInJson不会输入检查,它缺少急需的runErrorT

答案 2 :(得分:1)

您需要的功能如下所示,可以将Error映射到ActionM

handleError :: Error -> ActionM ()
handleError NotFound = status status404 >> json NotFound
handleError InvalidArgument = status status400 >> json BadRequest
...other error cases...

respond :: ToJSON a => Either Error a -> ActionM ()
respond (Left e) = handleError e
respond (Right item) = json item

然后在你的处理函数中使用上面的函数:

get "/items/:name" $ do
    name       <- param "name"
    eitherItem <- lift $ MyDB.getItem name
    respond eitherItem
相关问题