Eitherモナドの使い道 部分的な失敗を全体的な失敗とする計算(2)

※この記事のMaybe編はこちら

idと名前のデータベースと、順位とidのデータベースからなる、下記のようなフレームワークを考えてみよう。

2位の名前を取り出すには、idFromRank で 2位のidを取り出し、その id を nameFromId に渡して名前を得るものとする。
idFromRank も nameFromId も要求された順位やidのデータがなければ Left に評価されるものとする。

このフレームワークを使って、上位3位の名前を取得する関数を作成してみよう。
ポイントは、idFromRank も nameFromId も Left に評価されることがあるため、この関数もEither型に評価されるものとし、1位から3位までのidがすべて存在し、それらの名前がすべて存在する場合のみRightに評価され、それ以外のときはLeftに評価されるようにすることだ。

下記のように呼び出されるようになればよいだろう。

式を使ったプログラミングで素直に記述する場合、筆者の能力では下記の記述が限界である。
これはどう考えても地獄である。我々が関数プログラミングに求めていたものはこんなものではなかったはずだ。

Haskell には case 文があるので、効果的に使えば下記のように改善することができる。
しかし case 文の入れ子も本質的には地獄である。こんなことなら手続き型言語を使えばよいのではないか。

そこで Eitherモナド登場。
Either型のモナドとしての性質を使うと、上記とまったく同じ意味の関数を下記の内容だけで記述することができる。
モナドとしてのEither型には、失敗するかもしれない計算どうしを組み合わせるときは、一部でも失敗したらすべてが失敗したことにする
という性質が最初から組み込まれているためである。

全部のせておく。

Maybeモナドの使い道 部分的な失敗を全体的な失敗とする計算(1)

※この記事のEither編はこちら

idと名前のデータベースと、順位とidのデータベースからなる、下記のようなフレームワークを考えてみよう。

2位の名前を取り出すには、idFromRank で 2位のidを取り出し、その id を nameFromId に渡して名前を得るものとする。
idFromRank も nameFromId も要求された順位やidのデータがなければ Nothing に評価されるものとする。

このフレームワークを使って、上位3位の名前を取得する関数を作成してみよう。
ポイントは、idFromRank も nameFromId も Nothing に評価されることがあるため、この関数もMaybe型に評価されるものとし、1位から3位までのidがすべて存在し、それらの名前がすべて存在する場合のみJustに評価され、それ以外のときはNothingに評価されるようにすることだ。

下記のように呼び出されるようになればよいだろう。

式を使ったプログラミングで素直に記述する場合、筆者の能力では下記の記述が限界である。
これはどう考えても地獄である。我々が関数プログラミングに求めていたものはこんなものではなかったはずだ。

Haskell には case 文があるので、効果的に使えば下記のように改善することができる。
しかし case 文の入れ子も本質的には地獄である。こんなことなら手続き型言語を使えばよいのではないか。

そこで Maybeモナド登場。
Maybe型のモナドとしての性質を使うと、上記とまったく同じ意味の関数を下記の内容だけで記述することができる。
モナドとしてのMaybe型には、失敗するかもしれない計算どうしを組み合わせるときは、一部でも失敗したらすべてが失敗したことにする
という性質が最初から組み込まれているためである。

全部のせておく。

Readerモナドの使い道 仮想グローバル変数

現実的かどうかはさておき、税込み価格の商品と税抜き価格の商品が混在している店を考えてみよう。
会計の都合上、購入金額の合計を計算するときは、一旦すべての単価を税抜きに揃えてから集計し、まとめて税額を計算する必要があるものとする。

合計を計算するコードは下記のようになりそうだ。
このコードを眺めていると、toTaxIncluded と fromTaxIncluded の 引数 taxRate を消したくなってくるだろう。
ほぼ定数のようなものであり、合計の計算とは本質的に関係のない要素であるためだ。

そこでコードを下記のように改善してみよう。taxRate が引数から消えてすっきりした。
Haskell のコードをスクリプトとして使用している場合はこれで十分だろう。
ただ、一旦コンパイルされてしまうと、toTaxIncludedG と fromTaxIncludedG が使用する税率は固定されてしまう。

そこでReaderモナド登場。Readerモナドを使用すると、手続き型プログラミングでグローバル変数として保持したいような要素を、自然に保持することができる。
runReader関数 の 第2引数で指定した値を、モナド内の任意の関数内で ask 関数を使用して取り出すことができる。

全部のせておく。

Stateモナドの使い道 純粋関数内で状態を扱う

System.Random について調べるコードを考えてみよう。
0から9までのランダムな整数を繰り返し生成するとき、最初に5が現れるのが何回目か知りたいとする。

System.Random には randomRs という関数があり、型と範囲と乱数生成器を指定すると、ランダムな値の無限リストに評価される。
この関数とリストを操作する関数を使えば、下記のようにすっきり記述できる。

場合によってはこれで十分だろう。ただ、処理効率や可読性の面で、より手続き型に近い記述にしたい場面がありそうだ。

方法1. Stateモナドを使う

System.Random には randomR という関数があり、型と範囲と乱数生成器を指定すると、ランダムな値と新しい乱数生成器の組に評価される。randomRs の単発版である。

execState は初期状態と Stateモナドを使用する関数を指定すると、終了状態に評価される関数だ。
Stateモナドは、副作用を扱うという点ではIOモナドと似ているが、純粋関数内に閉じ込められる点が異なる。
Stateモナド内で現在の状態を得るにはgetを、状態を更新するにはputを使用する。

方法2. STモナドを使う

また、IOモナドから入出力に関する機能を取り除き、純粋関数内で評価できるようにした、STモナドも使用できる。
IOモナド内で IORef を使用する感覚で STモナド内で使用できる STRefという型があり、状態を保持することができる。
STRef は 複数作成しても良いので状態の管理が複雑なときは便利かもしれない。

全部のせておく。

Writerモナドの使い道 計算の経過を得る

1から10まで足し算するコードを考えてみよう。関数型言語では高階関数を使ってすっきり表現できる。
ただ、このコードには欠点がある。最後の結果求めるには十分だが、足し算の経過を見たいときどうしてよいか分からない。

手続き型言語で書かれたコードだったら、一行追加するだけで良いかもしれない。
では関数型言語ではどうするのか?

方法1. IOモナドを使う

(+) の IOモナド対応版(addIO)を作り、foldl を foldM に変えれば、addIO内でputStrLnが使えるようになる。ただ、これだと純粋な関数ではなくなり、IOを引きずっている箇所でしか利用できない。

方法2. trace を使う

Haskell には純粋な関数内での計算をデバッグ出力する関数が用意されている。trace は 第一引数を標準出力に表示し、第二引数と同じものに評価される関数だ。場合によってはこれで十分だろう。ただ、経過の値を他の計算でも利用したいとき、標準出力に表示されてしまったものを利用することはできない。

方法3. Writerモナド を使う

そこでWriterモナド登場。WriterモナドはIOモナドと違い純粋関数内で実行でき、コードに下記のように手を加えることで、結果にいたるまでの過程の値を最後にリストとして得ることが出来る。

全部のせておく。

継続モナドの使い道 早期リターン

引数をチェックして、問題があれば Left に包んだエラーメッセージに、問題がなければ Right に包んだ計算結果に評価される関数を考えてみよう。
純粋な関数でもロジックを表現することは可能だが、if文のネストが深くなればなるほど地獄である。手続き型言語であれば早期リターンで書きたいところだが、関数型言語ではどうするのか。

方法1. Eitherモナドを使う

※某所で指摘を受けたので追記。
Either モナド自体の性質を利用すれば、無尽蔵にネストが深くなっていく状況は避けることができる。場合によってはこれで十分だろう。ただ、より早期リターン風に表現したいときどうしたらよいか?

ついでにApplicativeスタイルでの記述も載せておく。

方法2. 継続モナドを使う

そこで継続モナド登場。純粋関数内で、早期リターンのような記述が可能となる。

全部のせておく。