将軍の困りごと
将軍「この屏風から毎晩 現在時刻という定数でない値 が飛び出してくる。関数的にしてみせよ」
一休『では捕まえますので、追い出してください』
将軍「だから出てきた時点で関数的じゃないってば」
一休『えっ』
将軍「代わりにお主が飛び込んで中で捕まえるがよい」
一休『ええっ!?』
手続き型プログラミングの世界では副作用というと
- 手続きを実行することで実行条件に影響を与えてしまうこと
などと解釈することが一般的ですが、関数型プログラミングでは
- 関数の戻り値が引数だけで決まらないこと
を意味します。つまり関数的でない、不確定であるというのと同義。将軍がお困りの現在時刻を返す関数というのは、そういう見方でいうと関数的でなく副作用があります。なにしろ、引数を取らない以上は定数を返すのが関数というものですから。
でも時刻を取ってこないことには仕方がないじゃない
関数に不確定さを持たせずに現在時刻を取得して使いたい? 矛盾しているんじゃないか?
それに対する答が、将軍の「代わりにお主が飛び込んで中で捕まえる」、すなわちコールバックです。そう、取って来れないならこっちからコールバックを渡せばいいんです。
ちょっと手続き型言語でそれっぽく書いてみましょう。C#で書いていきます。Java系の方はちょっと文法のおかしいJava8だと思って読んでください。
TimeFetcher tf = TimeFetcher.Instance;
tf.Bind(now => {/* 現在時刻に対する処理 */});
tf.Go();
TimeFetcherなんてクラスはいまでっち上げたものですが、とにかくTimeFetcher.Instanceプロパティは関数的です。必ずシングルトンのTimeFetcherオブジェクトを返す確定的関数です。
で、時刻を受け取ってそれを画面出力するなどといった処理をコールバックとしてこれに与えてあげれば、それをGoするとき初めて時刻が内部的に取得され、コールバックに引数として与えられます。与える処理というのはもちろんちゃんと関数的なものを書いて渡すことにしておきましょう。
これだったら不確定さはありません。将軍もニコニコしています。
でもコールバックだけで完結する処理ばっかりじゃないじゃない
でも普通はそんな単純じゃありません。取得した現在時刻を別の処理に -- 例えばDB書き込みのときに使うかもしれません。つまりは結局、コールバック関数からその不確定な結果を外へ返したいんですよ。やはり矛盾してる?
あるお約束を守れば、矛盾していないことにできます。それは、TimeFetcher.Instanceが「コールバックを受け取るオブジェクト」を返したように、外に返していいのも「コールバックを受け取るオブジェクト」だけということにするのです。
こんな風になります。
TimeFetcher tf = TimeFetcher.Instance;
StringFetcher result = tf.Bind(
now => new StringFetcher(now.ToString("yyyy/MM/dd HH:mm:ss")
);
result.Bind(str => System.Console.WriteLine(str));
result.Go();
ここでは、StringFetcherというクラスが「文字列を受け取るコールバックを要求するオブジェクト」です。ね、ほら、コールバックから外に結果を返せました。
ここで「resultは、値を取り出せないとは言え、実際には現在時刻を文字列化したものを抱えているのでデータとしては毎回違うものなのではないか?」と疑問をお持ちですか?
いいえ、resultが中に抱えているのは【現在時刻を文字列化したもの】ではなくコールバックです。コールバックは必要になったときに呼び出されます。最後のGoの時点で、表示するのに必要だから時刻を文字列化、それに必要だから現在時刻を取得、という処理が走ることになります。
まとめて言うと
外部からの入力は、「取り出せない(コールバックにしか渡してくれない)データコンテナ」という形で受け取り、その「取り出せないデータコンテナ」をコールバックで処理した結果も、やはり「取り出せないデータコンテナ」で返って来るというシステム、これでデータコンテナの外側の世界は不確定さない関数型プログラマニコニコの世界となりました。
では、こういう「取り出せないデータコンテナ」を総称型にしてしまいましょう。
IO<DateTime> tf = TimeFetcher.Instance;
IO<String> result = tf.Bind(
now => new IO<String>(now.ToString("yyyy/MM/dd HH:mm:ss")
);
result.Bind(str => System.Console.WriteLine(str));
はい、それがまさにIO型なのであります。
普通の値をIO型にすることはできるしIO値からIO値を作ることはできるけどIO値を普通の値には変換できません。そんな一方通行な、IO型のような「自己を再生産できる」型をモナドと呼んでいます。
(筆者のブログ記事 http://cs.hatenablog.jp/entry/2013/08/23/075647 とツイート https://twitter.com/yuba/status/528002501186818048 を改題再編集しました)