一番簡単な MonadFail インスタンス

The English version is at Dev.


導入

failMonad から剥がされて早や幾年、私は失敗する可能性のある計算は MonadFail を使って型を付けるのが好きです。

foo :: MonadFail m => m a

こうすると IO の文脈であればその中で、純粋な文脈であれば Maybe などで具体化して呼ぶことができます。

-- IO の文脈では
foo :: IO a

-- 純粋な文脈では
foo :: Maybe a

さて、純粋な文脈として Maybe を使うと失敗のメッセージを失ってしまうことが嬉しくありません。では、Either を使えばいいのではないでしょうか?実は EitherMonadFailインスタンスになっていません。提案はされていますが、失敗・成功以外に同列にパラメーターを扱うケースもあるのでそのときに MonadFail であることは適切でないからです*1

gitlab.haskell.org

そういうわけで一番簡単な MonadFail インスタンスとして次のような Result 型が欲しくなりました。

newtype Result a = Result (Either String a)

instance MonadFail Result where
  fail = Left

実をいうとこれに相当するものはすでにあるのですが非推奨となっています。それは mtl パッケージの ErrorT です。

either-result パッケージ

そういうわけで Result に加えいくつかの関数をまとめてリリースしたのが either-result パッケージです。

hackage.haskell.org

実際には Resultモナドトランスフォーマー版の ResultT を使って実装され、ResultT は transformers パッケージの ExceptTnewtype です。

type Result a = ResultT Identity a

newtype ResultT m a = ResultT (ExceptT String m a)

ResultTExceptT と異なるのは MonadFail インスタンスで、fail を呼ぶと ResultTLeft でくるむのに対して ExceptT ではベースのモナドfail を呼びます。ですので、ResultT ではベースのモナドMonad しか要求しませんが、ExceptT では MonadFail であることを要求します。

instance Monad m => MonadFail (ResultT m) whereinstance MonadFail m => MonadFail (ExceptT e m) where

モナドトランスフォーマーにしたついでに mtl の MonadError インスタンスにもなっているので throwErrorcatchError することができます。

exceptions パッケージは?

MonadThrow という型クラスもなかったっけ?はい、あります。exceptions パッケージ*2MonadThrowManadCatch 型クラスがあります。こちらは投げる・捉えるものが Exception 型クラスであることを要求します。使い分けとしては、投げる・捉えるものを型で区別したい場合は MonadThrowMonadCatch にして、単にメッセージのみでよい場合は 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 というボタンを押そう

*1:Rust ではデフォルトで Result 型で同列に扱いにくいため Either という名前がよかったという主張もあるみたいですね。

*2:使用する場合は safe-exceptions パッケージをおすすめします。