2

Scotty と Persistent を使用して REST バックエンドを開発していますが、エラーを処理する正しい方法がわかりません。

次のような DB にアクセスするための関数がいくつかあります。

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

SQLモナド内のいずれかを返します。次に、アクションでそれを使用してアイテムを取得し、その 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応答について何も知らないといいですね。

私は過去に命令型言語を使用して、どこからでも例外をスローし、それを 1 か所でキャッチして適切な応答をレンダリングすることで、この問題を解決しました。Haskellでも可能だと思いますが、機能的なツールを使ってこの問題を解決する方法を知りたいです。

モナドが( のように)短絡する可能性があることは知っていEither >> Either >> Eitherますが、この少し複雑なケースでそれを使用する方法がわかりません。

4

3 に答える 3

4

EitherT解決策は、(パッケージの) モナド トランスフォーマーを使用してeither、エラーの短絡を処理することです。 EitherT命令型言語のチェック済み例外とまったく同じ機能を備えたモナドを拡張します。

これは、どの「ベース」モナドでも機能し、m2 種類の計算があり、失敗するものと決して失敗しないものがあると仮定します。

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

次に、これらの計算の両方をEitherT Error mモナドに持ち上げることができます。失敗した計算を持ち上げる方法は、それらをEitherTコンストラクターでラップすることです (コンストラクターは型と同じ名前を持ちます)。

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

EitherT fails :: EitherT Error m r

Error型がモナドに吸収され、戻り値に表示されなくなったことに注目してください。

成功した計算を持ち上げるには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

...私たちの場合tEitherT 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 ()

runEitherTEitherT完了するか、最初のエラーが発生するまで実行します。返される はeitherItem、計算が失敗した場合は 、計算が成功した場合は になります。runEitherTLeftRight

これにより、エラー処理をブロックの後の 1 つの case ステートメントにまとめることができます。

私のパッケージで提供されているものからcatchインポートすると、のような動作をすることさえできます。これにより、命令型コードに非常によく似たコードを記述できます。catchControl.Errorerrors

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

ただし、エラーをキャッチして処理したとしても、runEitherTコードのどこかで を使用して、作業が完了したらラップを解除する必要があります。EitherTそのため、この単純な例runEitherTでは、 ではなく直接使用することをお勧めしcatchます。

于 2013-10-24T14:53:28.743 に答える
1

必要なのは、 aErrorを an にマップできる以下のような関数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 に答える