C#/Javaで読む、HaskellがIOモナドで実現したいこと
モナドを理解した人がまだ理解していない人になんとか理解してもらおうと書く解説記事ばかり積み上がっていくのはもはやIT系ブログ界の伝統芸ですが、この記事で少しでも貢献になるでしょうか。
この記事ではHaskellも関数型言語も出て来ません。読み慣れた手続き型言語でIOモナドがどういうものか読み解いていきます。
関数プログラマが避けたいこと=不確定さ
解説の前にまず例題を決めましょう。例題は「現在時刻を表示するプログラム」とします。簡単ですね。ちょっと書いてみましょう。
// Javaの例 Calendar now = GregorianCalendar. getInstance(); DateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); System.out.println(formatter.format(now.getTime())); // C#の例 DateTime now = DateTime.Now; System.Console.WriteLine(now.ToString("yyyy/MM/dd HH:mm:ss"));
何の問題もなさそうなコードですが、関数プログラマはある一点に顔をしかめます。副作用があるのです。
手続きプログラマ視点では、このプログラムに副作用なんてありません。グローバル変数を使ったりしていませんし。
ところが違うのです。関数プログラマの言う副作用は、むしろ不確定さといった方が正しいものです。関数の結果が引数だけで決まらない、それが不確定さです。GregorianCalendar. getInstance()
やDateTime.Now
は引数なしの関数(だから毎回同じ値を返してしかるべき)なのに毎回違う値を返す、その不確定さがいただけないのです。
でも時刻を取ってこないことには仕方がないじゃない
関数に不確定さを持たせずに時刻を取ってきて扱う? 矛盾しているようですが、Ajaxプログラミングなんかされてる方は両立する方法をすぐ思いつきませんか? そう、取って来れないならこっちからコールバックを渡せばいいんです。
TimeFetcher tf = DateTime.Fetcher; tf.Bind(【現在時刻に対する処理(書式化して画面出力する)】);
TimeFetcher
なんてクラスはいまでっち上げたものですが、とにかくTimeFetcher.Instance
はクリーンです。必ずシングルトンのTimeFetcher
オブジェクトを返す確定的関数です。
で、時刻を受け取ってそれを画面出力する処理をこれに与えてあげれば、そのとき初めて時刻が内部的に取得され、コールバックに引数として与えられます。
これだったら不確定さはありません。関数プログラマの人もニコニコしています。
でもコールバックだけで完結する処理ばっかりじゃないじゃない
そう、今回は画面表示するだけでした。でも普通はそんな単純じゃありません。書式化した現在時刻をDB書き込みのときに使うかもしれません。つまりは結局、コールバック関数からその不確定な結果を外へ返したいんですよ。矛盾してる?
あるお約束を守れば、矛盾していないことにできます。それは、TimeFetcher.getInstance()
が「コールバックを受け取るオブジェクト」を返したように、外に返していいのも「コールバックを受け取るオブジェクト」だけということにするのです。
こんな風になります。
TimeFetcher tf = DateTime.Fetcher; StringFetcher result = tf.Bind( now => new StringFetcher(now.ToString("yyyy/MM/dd HH:mm:ss") ); result.Bind(str => System.Console.WriteLine(str));
ほら、コールバックから外に結果を返せました。
まとめに入ります。
外部からの入力は、「取り出せない(コールバックにしか渡してくれない)データコンテナ」という形で受け取り、その「取り出せないデータコンテナ」をコールバックで処理した結果も、やはり「取り出せないデータコンテナ」で返って来るというシステム、これでデータコンテナの外側の世界は不確定さない関数型プログラマニコニコの世界となりました。
では、こういう「取り出せないデータコンテナ」を総称型にしてしまいましょう。
IO<DateTime> tf = DateTime.Fetcher; 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型を含む同じような「自己を再生産できる」型をモナドと呼んでいます。
今回使ったBind
メソッドを定義しているインターフェイスのようなイメージです(C#/Javaの総称型システムでは宣言できない形のインターフェイスですが)。