2015年5月16日土曜日

モナドとは

関数を繋ぐ

読んだ本にはモナドとは「文脈を持つ計算を扱う」ための仕組みとあるが、何を言っているのかさっぱりわからない。

ネットでもいろいろ勉強した結果、今のところの自分の理解だとモナドは関数を繋げるためのものだと解釈している。(間違っているかもしれないが)

単に関数を繋げるだけならHaskellにはすでに前回のCombineに相当する演算子が用意されている。これを使えば、単純に「 g . f x 」と書けば「 g(f(x)) 」と等価な処理に変換してくれる。

この場合は、単純に下図のように関数fの出力をそのまま関数gの入力として利用する。
 しかしながら、必ずしも毎回出力と入力が一対一対応するとは限らない。時には間に変換を挟んだりする必要が出てくる。そこでモナドが登場する。
 
イメージ的には下図のように先行の関数は出力の値を含んだモナドを出力して、モナドは必要により値を変換して後続の関数に渡す。
具体的な動きについて例としてListモナドを利用して見てみる。モナドの例としてはMaybeモナドがよく挙げられているが、Maybeモナドは動きがトリッキーなので個人的にはListモナドの方が入りやすいと思う。
 

Listモナド

Listモナドでは配列の要素を入力として配列を返す関数を想定している。例えば、値aを入力として、それを[a,a]という2要素の配列に変換する関数fがあったとして、これを前回のmapを利用して配列の各要素に適用すると、
 
f(1) → [1, 1]
map(f)([1,2,3])  → [[1,1],[2,2][3,3]]
 
このように、各要素にfを適用した戻り値の配列が配列として格納されるため、配列が入れ子になってしまう。
これの出力を別の関数gに同じくmapで適用するためには、入れ子を外して

[1,1,2,2,3,3]
 
という配列に変換する必要が出てくる。

Listモナドはこのmapと変換を一括してfとgの接続部分で行ってくれる。個人的にはこの変換に需要があるかはよくわからないが、Listモナドが用意されていると言うことは関数型言語の世界ではそれなりに需要があるのだろう。

Listモナドの挙動を理解するために、試しにJavaScriptで正確かどうかはわからないがそれっぽい挙動をするコードを書いてみた。

var List = function(array) {
  return {
    // (>>=) = concatMap に相当
    bind: function(f) {
      return List(concatMap(f)(array));
    },
    // 中身の配列を取得
    inner: array,
  }
}
// return = (:[]) に相当
List.return = function(a) {
  return List([a]);
}

var concatMap = function(f) {
  return function(array) {
    var newArray = [];
    for(var i = 0; i < array.length; i++) {
      var tmp = f(array[i]);
      newArray = newArray.concat(tmp.inner);
    }
    return newArray;
  }
}

これで、f,gを次のように定義して適用すると、期待通り入れ子を外した配列が出来上がる。

var f = function(a) {
  return List([a, a+1]);
}

var g = function(a) {
  return List([a, a*3]);
}

// f >>= g に相当
document.write( f(1).bind(g).inner + '<br>' );

自分の理解で作ったJavaScriptなのでHaskellと同じことになっているかはわからないが、これからListモナドの動作を見ていくと、まずListモナドでは前の関数の出力aを持ったbind関数を生成する。bind関数では右辺に来る関数を受け取ってこれと持っているaを組み合わせて出力(モナド)を生成する。
絵的に書くと下のようになるだろうか。
Listの場合はモナドを使わずにconcatMapだけで繋げることも出来るが、この場合は
concatMap(g)(concatMap(f)(1))

とconcatMapの方が目立ってしまうので、関数型言語ではモナドを使うことで間の変換処理を隠蔽してfとgの関係性を見えやすくしたということだと思う。

ちなみに、うえの説明ではわかりやすさのためにListオブジェクトにbind関数をぶら下げたが、Array.prototypeに追加することでよりシンプルに使えるようになる。

Array.prototype.bind = function(f) {
  return concatMap(f)(this);
}

var List = {
  return: function(a) {
    return [a];
  }
}

var f = function(a) {
  return [a, a+1];
}

var g = function(a) {
  return [a, a*3];
}

document.write( f(1).bind(g)+'<br>' );

モナドの大まかな挙動がわかったところで、今後はより複雑なMaybeモナドとStateモナドについても理解していく。

0 件のコメント:

コメントを投稿