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 というボタンを押そう