2

我正在练习与我的错误处理保持一致,并且我一直希望看到我编写的代码开始缩小。但是我构建了一个具有领域意义的持久性函数,而我必须编写的代码量只是为了进行 monad 处理和自定义错误处理是惊人的。

  • 对于“编程错误”,我只是调用error "assertion blown"
  • 对于真正平凡的事情,我返回 Nothing(请求的对象不存在)
  • 对于应该处理的错误,我Either E V通过创建一个Control.Monad.Error实例来处理它来返回或等效。

我在我的应用程序中有多个函数,我称之为原语,但它们可以捕获某些错误并将引发它们我抛出 DBError 类型的值。所以,我这样定义它们:

data DBError = ConversionError ConvertError
         | SaveError String
         | OtherError String 
         deriving (Show, Eq)

instance Error DBError where
    noMsg = OtherError "No message found"
    strMsg s = OtherError s

type DBMonad = ErrorT DBError IO

selectWorkoutByID :: IConnection a => UUID -> a -> DBMonad (Maybe SetRepWorkout)
insertWorkout :: IConnection a => SetRepWorkout -> a -> DBMonad ()

在调用应用程序级别,Workout 是一个持久保存到数据库的唯一对象,因此应用程序只调用 saveWorkout,它本身以您期望的方式使用 selectWorkoutByID、insertWorkout 和 updateWorkout:

saveWorkout :: IConnection a => SetRepWorkout -> a -> DBMonad ()
saveWorkout workout conn =
    r <- liftIO $ withTransaction conn $ \conn -> runErrorT $ do
        w_res <- selectWorkoutByID (uuid workout) conn
        case w_res of
            Just w -> updateWorkout workout conn >> return ()
            Nothing -> insertWorkout workout conn >> return ()
    case r of
        Right _ -> return ()
        Left err -> throwError err

这很丑陋。我必须运行并解包 DBMonad,在 IO monad 中运行它,将 IO 提升回 DBMonad,然后检查结果并在 DBMonad 中重新包装结果。

如何使用更少且更易于阅读的代码来做到这一点?

我期望使用我的自定义应用程序 monad 来处理可恢复的错误将帮助我减少我必须编写的代码量,但这恰恰相反!

以下是一些额外的问题:

  • 有没有更好的方法来构建应用程序语义错误?
  • 我应该改用 Control.Exception 吗?
4

1 回答 1

1

在查看了http://en.wikibooks.org/wiki/Haskell/Monad_transformers之后,这是第一个真正帮助我理解 Monad Transformers 的文档,我找到了一个不错的解决方案。

saveWorkout 函数的新版本如下所示:

saveWorkout :: IConnection a => SetRepWorkout -> a -> DBMonad ()
saveWorkout workout conn =
    ErrorT $ liftIO $ withTransaction conn $ \conn -> runErrorT $ do
        w_res <- selectWorkoutByID (uuid workout) conn
        case w_res of
            Just w -> updateWorkout workout conn >> return ()
            Nothing -> insertWorkout workout conn >> return ()

交易是这样的:

withTransaction 正在返回IO Either DBError ()liftIO有类型MonadIO m => IO a -> m a。ErrorT 是所有 ErrorT monad 的标准构造函数,我将 DBMonad 定义为该 monad。所以,我正在使用这些类型:

withTransaction conn $ <bunch of code> :: IO (Either DBError ())
liftIO :: MonadIO m => IO (Either DBError ()) -> m (Either DBError ())
ErrorT :: IO (Either DBError ()) -> ErrorT IO DBError ()

理想情况下,由于 ErrorT/DBMonad 是 MonadTrans 类的一部分,我会简单地使用lift它来提升IO (Either DBError ())到 ErrorT monad,但此时我无法让它真正正确地进行类型检查。然而,这个解决方案仍然通过删除我之前拥有的冗余重新包装来使代码更好。

于 2012-06-14T12:15:05.887 回答