モナド関数で検証付きのエラーモナドを使用するか、バインドで直接検証付きの独自のモナドを実装する方が良いですか?


9

使いやすさ/保守性の観点からデザインが賢明なものは何なのか、コミュニティとの適合性に関しては何が優れているのかと思います。

データモデルを考える:

type Name = String

data Amount = Out | Some | Enough | Plenty deriving (Show, Eq)
data Container = Container Name deriving (Show, Eq)
data Category = Category Name deriving (Show, Eq)
data Store = Store Name [Category] deriving (Show, Eq)
data Item = Item Name Container Category Amount Store deriving Show
instance Eq (Item) where
  (==) i1 i2 = (getItemName i1) == (getItemName i2)

data User = User Name [Container] [Category] [Store] [Item] deriving Show
instance Eq (User) where
  (==) u1 u2 = (getName u1) == (getName u2)

モナディック関数を実装して、たとえばアイテムやストアを追加するなどしてユーザーを変換できますが、無効なユーザーになる可能性があるため、これらのモナディック関数は取得または作成したユーザーを検証する必要があります。

だから、私はただ:

  • エラーモナドでそれをラップし、モナド関数に検証を実行させる
  • エラーモナドでラップし、適切なエラーレスポンスをスローするシーケンスでモナディック検証関数をコンシューマーにバインドします(検証しないように選択し、無効なユーザーオブジェクトを持ち歩くことができます)。
  • 実際にそれをユーザーのバインドインスタンスにビルドし、すべてのバインドで自動的に検証を実行する独自の種類のエラーモナドを効果的に作成します

3つのアプローチのそれぞれに良い点と悪い点がありますが、このシナリオでコミュニティがより一般的に行っていることを知りたいです。

したがって、コード用語では、オプション1:

addStore s (User n1 c1 c2 s1 i1) = validate $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]

オプション2:

addStore s (User n1 c1 c2 s1 i1) = Right $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ Right someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"] >>= validate
-- in this choice, the validation could be pushed off to last possible moment (like inside updateUsersTable before db gets updated)

オプション3:

data ValidUser u = ValidUser u | InvalidUser u
instance Monad ValidUser where
    (>>=) (ValidUser u) f = case return u of (ValidUser x) -> return f x; (InvalidUser y) -> return y
    (>>=) (InvalidUser u) f = InvalidUser u
    return u = validate u

addStore (Store s, User u, ValidUser vu) => s -> u -> vu
addStore s (User n1 c1 c2 s1 i1) = return $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someValidUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]

回答:


5

自問自答:無効なUserコードのバグまたは通常発生する可能性のある状況(たとえば、誰かがアプリケーションに誤った入力を入力した場合)があるか。それがバグである場合、私はそれが絶対に起こらないようにします(スマートコンストラクターの使用やより高度な型の作成など)。

それが有効なシナリオである場合は、実行時のエラー処理が適切です。それから私は尋ねます:a User無効であることは私にとって本当に何を意味しますか?

  1. これは、無効なUserコードがコードを失敗させる可能性があることを意味しますか?コードの一部は、a Userが常に有効であるという事実に依存していますか?
  2. それとも、後で修正する必要があるが、計算中に何も壊さないことを意味しますか?

1.の場合、間違いなくある種のエラーモナド(標準または独自のモナド)を使用します。それ以外の場合は、コードが適切に機能しているという保証が失われます。

独自のモナドを作成したり、モナドトランスフォーマーのスタックを使用したりすることも別の問題です。これはおそらく役立つでしょう。モナドトランスフォーマーを実際に遭遇したことはありますか?


更新:拡張オプションを確認します。

  1. 行くための最良の方法として見えます。おそらく、本当に安全にするために、私はコンストラクタを非表示にし、User代わりに無効なインスタンスの作成を許可しないいくつかの関数のみをエクスポートしたいと思います。これにより、いつでも適切に処理されます。たとえば、を作成するための一般的な関数はUser次のようになります。

    user :: ... -> Either YourErrorType User
    -- more generic:
    user :: (MonadError YourErrorType m) ... -> m User
    -- Or if you actually don't need to differentiate errors:
    user :: ... -> Maybe User
    -- or more generic:
    user :: (MonadPlus m) ... -> m User
    -- etc.
    

    たとえばMap、多くのライブラリは同様のアプローチを採用するSetSeq、基になる実装を非表示にして、不変条件に従わない構造を作成できないようにします。

  2. 検証を最後まで延期し、Right ...どこでも使用する場合、モナドは必要ありません。純粋な計算を行い、起こり得るエラーを最後に解決することができます。十分に早く計算を停止しなかったため、無効なユーザー値が他の場所に無効なデータをもたらす可能性があるため、このアプローチは非常に危険です。また、他の方法でユーザーが更新されて再び有効になると、無効なデータがどこかにあり、そのことを知らないことになります。

  3. ここにはいくつかの問題があります。

    • 最も重要なことは、モナドはだけでなく、すべての型パラメーターを受け入れなければならないということUserです。したがって、に制限なしでvalidateタイプu -> ValidUser uを指定する必要がありますu。したがって、の入力を検証するモナドを作成するreturnことreturnはできません。
    • 次に、私が理解していないのcase return u ofは、の定義で一致しているということです>>=。の主なポイントは、ValidUser有効な値と無効な値を区別することであり、モナドはこれが常に真であることを確認する必要があります。だから単純に

      (>>=) (ValidUser u) f = f u
      (>>=) (InvalidUser u) f = InvalidUser u
      

    そして、これはすでに非常によく似ていEitherます。

通常、カスタムモナドを使用するのは、

  • 必要な機能を提供する既存のモナドはありません。既存のモナドには通常多くのサポート関数があり、さらに重要なことに、モナドトランスフォーマーがあるので、モナドスタックにそれらを合成できます。
  • または、モナドスタックとして説明するには複雑すぎるモナドが必要な場合。

あなたの最後の2つのポイントは非常に貴重であり、私はそれらについて考えていませんでした!間違いなく私が探していた知恵、あなたの考えを共有してくれてありがとう、私は間違いなく#1に行きます!
ジミー・ホッファ

昨夜モジュール全体を縛っただけで、あなたは正しかった。validateメソッドを、すべてのモデルの更新を行っていた少数の主要なコンビネーターにポップしましたが、実際には、このように非常に理にかなっています。私は本当に#3の後に行くつもりでしたが、今では、そのアプローチがどのように柔軟性に欠けるのかがわかります。
ジミー・ホッファ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.