まとめ
モナドを組み合わせる
モナドをイチから書く場合、デザインパターンとしてはモナドに各作用を追加する方法を記述しがちです。リーダ作用はモナドの型をリーダの環境からの関数にすること、状態作用は初期状態から最終状態と計算結果のペアへの関数を含めること、失敗や例外は戻り値の型に直和型を含めること、ロギングやその他の出力は戻り値の型に直積型を含めることで、それぞれの作用が追加されます。既存のモナドも同様に戻り値の型の一部にすることができ、それによってその作用を新しいモナドに含めることができます。
こうしたデザインパターンはベースのモナドに作用を追加する モナド変換子 を定義することで再利用可能なソフトウェアコンポーネントによるライブラリとなります。モナド変換子はより単純なモナド型を引数として取り、拡張されたモナド型を返します。モナド変換子は最低でも以下のインスタンスを提供しなければなりません:
- 内側の型がすでにモナドであると仮定する
Monad
インスタンス - 内側のモナドから変換後のモナドにアクションを変換する
MonadLift
インスタンス
モナド変換子は多相構造体や帰納的データ型として実装されることもありますが、ベースのモナド型から拡張されたモナド型への関数として実装されることが最も多いです。
作用ごとの型クラス
モナド変換子のデザインパターンは共通して、作用を持つモナドとその作用を別のモナドに追加するモナド変換子、作用へのジェネリックなインタフェースを提供する型クラスを定義して、特定の作用を実装します。これにより、必要な作用を指定するだけのプログラムを書くことができ、呼び出し側は適切な作用を持つ任意のモナドを提供することができます。
ある時は補助的な型情報(例えば、状態を提供するモナドにおける状態の型や、例外を提供するモナドにおける例外の型)が出力パラメータになることもありますが、ならない時もあります。出力パラメータはそれぞれの種類の作用を一度だけ使用するシンプルなプログラムにおいて最も有用ですが、同じ作用の複数のインスタンスが特定のプログラムで使用される場合、型チェッカが間違った型にせっかちにコミットしてしまう危険性があります。そのため、通常は両方のバージョンが提供されており、通常のパラメータのバージョンの型クラスは -Of
で終わる名前を持ちます。
モナド変換子は可換ではない
モナド内の変換子の順序を変えると、モナドを使用するプログラムの意味が変わってしまうことに注意することが重要です。例えば、StateT
と ExceptT
の順序を変更すると、例外が投げられた時に状態の変更が失われるプログラムか、変更が維持されるプログラムかのどちらかになります。ほとんどの命令型言語では後者しか提供しませんが、モナド変換子によって柔軟性が増すため、目の前のタスクに適した種類を選択するための思考と注意が必要になります。
モナド変換子のための do
記法
Leanの do
ブロックは、ブロックで何かしらの値で終了する早期リターン、局所的な可変変数、break
と continue
を使った for
ループ、単一分岐の if
文をサポートしています。これはLeanを使って証明を書く際には邪魔になるような命令的な機能を導入しているように見えるかもしれませんが、実際にはただモナド変換子のある一般的な使用法に対しての便利な構文に過ぎません。裏では、do
ブロックがどのようなモナドで書かれていたとしても、これらの追加作用をサポートするために ExceptT
と StateT
の適切な使用によって変換されます。