Haskell: 非IO单子中的异常处理

7

我开始使用Yesod来开发一个小项目,这是我第一次使用Haskell来做真正的事情。 这段处理注册表单的代码运行良好:

postRegisterR :: Handler ()
postRegisterR = do email <- runInputPost $ ireq textField "email"
                   user  <- runInputPost $ ireq textField "user"
                   pwd   <- runInputPost $ ireq textField "pwd"
                   cpwd  <- runInputPost $ ireq textField "cpwd"
                   if pwd == cpwd && isValidEmail email
                      then do
                        tryInsert email user pwd
                        setSession "user" user
                        redirectUltDest SessionR
                      else do
                        redirect HomeR

tryInsert :: Text -> Text -> Text -> Handler ()
tryInsert email user pwd = do pwdbs <- liftIO $ hashedPwd pwd
                              _ <- runDB $ insert $ User email user pwdbs
                              return ()

现在的问题是:如果我使用相同的凭据登录两次,我会收到一个“InternalServerError”的错误。这是正确的,因为在我的模型配置中有“UniqueUser email username”。所以我想以某种方式捕获和处理这个错误。当你处理定义在外部库或框架中的非IO单子时,Haskell中的异常处理如何工作?我该怎么做?
PS:我阅读了this教程,但它只适用于设计新库。我尝试使用catch函数,但是我得到了很多类型错误。
编辑
谢谢Ankur,你的代码经过一些修改后起作用了,以消除这个错误:
   Ambiguous type variable `e0' in the constraint:
      (Exception e0) arising from a use of `catch'
   Probable fix: add a type signature that fixes these type variable(s)

代码:

tryInsert :: Text -> Text -> ByteString -> Handler Bool
tryInsert email user pwd = HandlerT (\d -> catch (unHandlerT (runDB $ insert $ User email user pwd) d 
                                                  >> return True)
                                                 (\(e :: SomeException) -> return False))

启用了ScopedTypeVariables扩展

编辑2

bennofs的提示后,最终版本:

{-# LANGUAGE ScopedTypeVariables #-}
import Control.Exception.Lifted (catch)
import Control.Monad (void)

postRegisterR :: Handler ()
postRegisterR = do email <- runInputPost $ ireq textField "email"
                   user  <- runInputPost $ ireq textField "user"
                   pwd   <- runInputPost $ ireq textField "pwd"
                   cpwd  <- runInputPost $ ireq textField "cpwd"
                   if pwd == cpwd && isValidEmail email
                      then do
                        pwdbs <- liftIO $ hashedPwd pwd
                        success <- tryInsert email user pwdbs
                        case success of
                          True -> do setSession "user" user
                                     redirectUltDest SessionR
                          False -> redirect HomeR
                      else do
                        redirect HomeR

tryInsert :: Text -> Text -> ByteString -> Handler Bool
tryInsert email user pwd = do void $ runDB $ insert $ User email user pwd
                              return True
                              `catch` (\(e :: SomeException) ->
                                  do return False)

2
你可以使用 checkUnique 在插入之前测试键是否唯一,通过不同的处理方式避免异常。 - bennofs
嗯...在较新版本的Yesod中没有checkUnique,但我找到了insertUnique,谢谢。无论如何,我仍然对异常处理感兴趣。 - andrebask
1
你可以使用 ScopedTypeVariables 语言扩展,然后执行 (\ (e :: SomeException) -> return False) - Ankur
2个回答

8

有一个名为lifted-base的包,它也提供了一个更通用的catch函数:

Control.Exception.Lifted.catch :: 
  (MonadBaseControl IO m, Exception e)
  => m a         -- ^ The computation to run
  -> (e -> m a)  -- ^ Handler to invoke if an exception is raised
  -> m a

存在一个实例MonadBaseControl IO Handler,因此您可以直接使用此函数:

{-# LANGUAGE ScopedTypeVariables #-} -- I think this is needed PatternSignatures.
import Control.Exception.Lifted (catch)
import Control.Monad (void)

tryInsert :: Text -> Text -> Text -> Handler ()
tryInsert email user pwd = do 
  pwdbs <- liftIO $ hashedPwd pwd
  (void $ runDB $ insert $ User email user pwdbs) `catch` \(e :: SomeException) -> do
    -- Your exception handling goes code here. This code also lives in the Handler monad.
    return ()
 return ()

另一个可能性是使用MonadCatchIO-mtl,它也提供了一个通用的catch函数。不过,MonadCatchIO-mtl无法在GHC HEAD上构建。我仍然认为使用insertUnique是处理这个问题最清晰的方法。


谢谢,这也是一个不错的解决方案,你可以避免所有的Handler/unHandler麻烦。是的,在这种情况下,insertUnique方式可能是最好的解决方案,但总的来说,这个讨论将在未来有用,我没有找到太多关于这个主题的清晰信息。 - andrebask

3
您可以尝试以下示例,基本上HandlerHandlerT,它是一种单子变换器(我还没有对下面的代码进行类型检查 :))。
tryInsert :: Text -> Text -> Text -> Handler Bool
tryInsert email user pwd = HandlerT (\d -> do pwdbs <- hashedPwd pwd
                                              catch (unHandlerT (runDB $ insert $ User email user pwdbs) d >> return True)
                                                    (\e -> return False))

检查返回的布尔值,以确定是否发生异常。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接