我正在使用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
),但不知道如何在这个稍微复杂的情况下使用它。
答案 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
...在我们的案例中,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
将为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