2

私は自分のエラー処理に一貫性を持たせるための練習をしており、自分が書いたコードが縮小し始めることを期待し続けています。しかし、私はドメインを意味する永続化関数を構築しました。モナド処理とカスタム エラー処理を行うためだけに書かなければならなかったコードの量は驚くべきものでした。

  • 「プログラミングエラー」の場合は、呼び出すだけです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 モナドで実行し、IO を DBMonad に戻してから、結果をチェックして DBMonad で結果を再ラップする必要があります。

より少ない、読みやすいコードでこれを行うにはどうすればよいですか?

カスタム アプリケーション モナドを使用して回復可能なエラーを処理すると、記述しなければならないコードの量を減らすことができると期待していますが、これは逆です!

追加の質問を次に示します。

  • アプリケーションのセマンティック エラーを構築するためのより良い方法はありますか?
  • 代わりに 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 モナドの標準コンストラクタであり、DBMonad をそのモナドとして定義しました。だから、私はこれらのタイプで働いています:

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ため、単純に ErrorT モナドに戻すために使用しますIO (Either DBError ())が、現時点では実際に型チェックを正しく行うことができません。ただし、このソリューションでは、以前の冗長な再ラップが削除されるため、コードが改善されます。

于 2012-06-14T12:15:05.887 に答える