使いやすさ/保守性の観点からデザインが賢明なものは何なのか、コミュニティとの適合性に関しては何が優れているのかと思います。
データモデルを考える:
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"]
自分自身に問いかけると思います:無効なUser
コードバグまたは通常発生する可能性のある状況(たとえば、誰かがアプリケーションに間違った入力を入力した)があります。それがバグである場合、私はそれが絶対に起こらないようにしようとします(たとえば smart constructors を使用するか、より高度な型を作成するなど)。
それが有効なシナリオである場合は、実行時のエラー処理が適切です。それから私は尋ねます:User
がinvalidであることは私にとって本当に何を意味しますか?
User
は一部のコードを失敗させる可能性があるという意味ですか?コードの一部は、User
が常に有効であるという事実に依存していますか?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
、Set
またはSeq
は、基礎となる実装を隠して、不変条件に従わない構造を作成できないようにします。
検証を最後まで延期し、どこでもRight ...
を使用すれば、モナドはもう必要ありません。純粋な計算を行い、起こり得るエラーを最後に解決することができます。十分に早く計算を停止しなかったため、無効なユーザー値が他の場所に無効なデータをもたらす可能性があるため、このアプローチは非常に危険です。また、他の方法でユーザーが更新されて再び有効になる場合は、どこかに無効なデータがあり、それを知らないことになります。
ここにはいくつかの問題があります。
User
だけでなく、すべての型パラメーターを受け入れなければならないということです。そのため、あなたのvalidate
は、u
に対する制限なしにu -> ValidUser u
型を持つ必要があります。したがって、return
は完全に多態性でなければならないため、return
の入力を検証するモナドを作成することはできません。次に、case return u of
の定義で>>=
と一致していることを理解できません。 ValidUser
の主なポイントは、有効な値と無効な値を区別することである必要があるため、モナドはこれが常に真であることを確認する必要があります。だからそれは単に
(>>=) (ValidUser u) f = f u
(>>=) (InvalidUser u) f = InvalidUser u
そして、これはすでにEither
に非常によく似ています。
通常、次の場合にのみカスタムモナドを使用します