2

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

我有几个函数可以访问数据库,例如:

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

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

我想完全摆脱我的操作中的错误处理:

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 也可以,但我想知道如何通过使用功能工具来解决这个问题。

I know it is possible for monads to short-circuit (like Either >> Either >> Either) but have no idea how to use it in this slightly more complicated case.

4

3 回答 3

4

解决方案是使用EitherTmonad 转换器(从either包中)来处理错误的短路。 EitherT扩展任何具有与命令式语言中的检查异常完全相同的功能的 monad。

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

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

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

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

EitherT fails :: EitherT Error m r

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

要提升成功的计算,请使用lift, from transformers

lift :: m r -> EitherT Error m r

lift succeeds :: EitherT Error m r

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

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

...在我们的例子t中是EitherT 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将是 a 。LeftRight

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

如果你从我的包提供的导入,你甚至可以做catch类似的行为。这使您可以编写与命令式代码非常相似的代码:catchControl.Errorerrors

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

但是,即使您已捕获并处理了错误,您仍需要runEitherT在代码中的某个时间点使用来解包。EitherT这就是为什么对于这个更简单的示例,我建议直接使用runEitherT而不是catch.

于 2013-10-24T14:53:28.743 回答
2

您正在寻找Errormonad

你想写这样的东西:

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

transformersErrorT为现有的 Monad 添加了错误处理。

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

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

或者你可以使用类似的东西

toErrors :: m (Either e a) -> ErrorT e m a

使用现有功能而不修改它们。快速搜索表明 type 已经有了一些东西m (Either e a) -> ErrorT e m a,那就是 constructor 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是什么?借用handleErrorAnkur 的例子:

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.

于 2013-10-24T14:55:42.970 回答
1

您需要的是如下所示的函数,它可以将 a 映射Error到 a 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
于 2013-10-24T11:21:46.917 回答