一番簡単な MonadFail インスタンス
The English version is at Dev.
導入
fail
が Monad
から剥がされて早や幾年、私は失敗する可能性のある計算は MonadFail
を使って型を付けるのが好きです。
foo :: MonadFail m => m a
こうすると IO
の文脈であればその中で、純粋な文脈であれば Maybe
などで具体化して呼ぶことができます。
-- IO の文脈では foo :: IO a -- 純粋な文脈では foo :: Maybe a
さて、純粋な文脈として Maybe
を使うと失敗のメッセージを失ってしまうことが嬉しくありません。では、Either
を使えばいいのではないでしょうか?実は Either
は MonadFail
のインスタンスになっていません。提案はされていますが、失敗・成功以外に同列にパラメーターを扱うケースもあるのでそのときに MonadFail
であることは適切でないからです*1。
そういうわけで一番簡単な MonadFail
インスタンスとして次のような Result
型が欲しくなりました。
newtype Result a = Result (Either String a) instance MonadFail Result where fail = Result . Left
実をいうとこれに相当するものはすでにあるのですが非推奨となっています。それは mtl パッケージの ErrorT
です。
ErrorT String Identity (ただしdeprecated) https://t.co/fzXcYTY4gw https://t.co/Ac6drQmfw1
— だめぽラボ@技術書典9 (@mod_poppo) 2020年7月26日
either-result パッケージ
そういうわけで Result
に加えいくつかの関数をまとめてリリースしたのが either-result パッケージです。
実際には Result
はモナドトランスフォーマー版の ResultT
を使って実装され、ResultT
は transformers パッケージの ExceptT
の newtype です。
type Result a = ResultT Identity a newtype ResultT m a = ResultT (ExceptT String m a)
ResultT
が ExceptT
と異なるのは MonadFail
インスタンスで、fail
を呼ぶと ResultT
は Left
でくるむのに対して ExceptT
ではベースのモナドの fail
を呼びます。ですので、ResultT
ではベースのモナドに Monad
しか要求しませんが、ExceptT
では MonadFail
であることを要求します。
instance Monad m => MonadFail (ResultT m) where … instance MonadFail m => MonadFail (ExceptT e m) where …
モナドトランスフォーマーにしたついでに mtl の MonadError
インスタンスにもなっているので throwError
・catchError
することができます。
exceptions パッケージは?
MonadThrow
という型クラスもなかったっけ?はい、あります。exceptions パッケージ*2に MonadThrow
と ManadCatch
型クラスがあります。こちらは投げる・捉えるものが Exception
型クラスであることを要求します。使い分けとしては、投げる・捉えるものを型で区別したい場合は MonadThrow
・MonadCatch
にして、単にメッセージのみでよい場合は MonadFail
にすればよいと思います。
class Monad m => MonadThrow m where throwM :: Exception e => e -> m a class MonadThrow m => MonadCatch m where catch :: Exception e => m a -> (e -> m a) -> m a class Monad m => MonadFail m where fail :: String -> m a
まとめ
- 定義時、失敗する計算は
MonadFail m => m a
にしよう - 使用時、
IO
などの文脈ではIO a
などとして使おう - 使用時、純粋な文脈では
Result a
として使おう - GitHub リポジトリーの Star というボタンを押そう