読者です 読者をやめる 読者になる 読者になる

セカイノカタチ

世界のカタチを探求するブログ。関数型言語に興味があり、HaskellやScalaを勉強中。最近はカメラの話題も多め

マーブルワーズ

モナドが解らない人へ、図解で絶対わかるモナドのしくみ

Scala Haskell

前置き

みなさん、モナドって、わかりにくいですよね。
なので、図解することで解りやすく説明できるんじゃないかと、何回かモナドの図解を試みてきたのですが、むしろ複雑さが強調されてしまい残念な感じになってしまいました。

過去の図解
モナドってなんだよ!?全然わからないんで分解して図解してみた(´・ω・`)
モナドの分解ふたたび

ただ、以前よりモナドを表すメタファのイメージがあって、レゴブロックを組み合わせるようなカタチに例えてうまく説明できるんじゃないか。という予感がしていました。
そして、去年の年末ぐらいに、ついにそのカタチの具体的なアイディアを閃きました。

今日、この記事を書ききったとして、イメージの着想から半年ほど掛かってしまった計算になりますが、何とか説明してみたいと思います。

二つの世界

この世界は大きく二つに分かれている。我々の住む、この穢れた世界と、関数型プログラマたちの約束の地、穢れなき純粋な関数の世界だ。
まずは、その純粋な関数の世界に住む住人たちを紹介しよう。

図 ピュア妖精たち

彼らが、この世界の住人、ピュア妖精だ。ピュア妖精はピュアなので、同じ刺激を入力してあげると、必ず同じ行動をし、同じ結果を返してくれる。
実に不思議なピュア世界の住人たちだ。
先頭にいるのはモナドちゃん。ちょっとドジっ子な点を除けば、他のピュア妖精と変わる所はないし、勿論、同じ入力に対しては必ず同じドジを繰り返す。

彼らの住む世界と我々の世界は隔離されている。通常の方法で我々が彼らに何かを働きかける事は出来ない。存在するが見ることのできない禅問答のような存在なのだ。

しかし、気を落とすことはない。穢れた世界の我々と純粋な彼らを繋ぐための夢の装置がある。


そんなメルヘンチックなアイテムがこれだ。

図 メインソケット

助手「え。ただのコンセントにしか見えませんが・・・」

彼らと我々を繋ぐ装置は、この様なカタチをしている。我々の世界のコンセントに近い形状をしているが、違う点もある。
あと、急に助手が出てきてビックリしたかもしれないが、一人で喋っているのが寂しくなって来たので呼んでみた。

さて、一見コンセントの様に見えるこのソケットだが、あちらの世界にただ一つしかない。このコンセント以外に我々と通信できないと言う点において、唯一無二の存在ということになる。逆にこのコンセントの無い真に純粋な関数の世界は無数にあるとも言えるが、我々が知り得る事が出来ないので無いのと一緒ということになる。

助手「何か、無駄に哲学的ですね」

彼らの住むファンシーワールドはこんな感じだ。

図 ピュア妖精のおウチ

助手「これはまた随分所帯じみているというか・・・やる気が感じられませんね」

この外の世界から引き込まれたコンセントが重要な役割を担っているのだ。

IOモナドを繋ぐ

いい忘れたが、mainコンセントは、IO aという型を持っている。
戻り値がIO aとなる関数をつなぐと、コンセントから電気のようなものが通って、回路が動くようになっている。この点において、我々の世界のコンセントと家電の関係と同じだと言える。
つまり、モナドとは家電のことである。

助手「無茶苦茶いいますね。下手な事言うとマサカリ飛んできますよ」

試しに、繋いでみよう。丁度ここに"getChar"と言う装置がある。

図 getChar

これを、こうやって・・・。mainに繋ぐ。

図 mainにgetCharを繋ぐ

すると、標準入力から1文字読み込むプログラムが出来上がる。なお、"getChar"自体は純粋な関数世界に影響を与えないため、ピュア妖精たちは、何もしない。
Haskellのプログラムで表すとこうなる。

main = getChar

このプログラムを実行すると、標準入力を受付け、1文字入力すると何も起きずに終了する。
重要なのは、標準入力の受付という副作用を伴う操作をしても、ピュアな関数の世界には何ら影響を与えないと言うことだ。さらに言うと、"getChar"は引数を伴わず関数ではない。にもかかわらず、我々の世界には影響を与えている。

この様に、IO aを返り値とする関数は、引数がなくとも電源(main)さえあれば力を発揮する不思議な装置なのだ。

部品を組み合わせる

先ほどの例で我々は、ピュアな関数の世界に足を踏み入れることはできたが、あちらの世界に関与することはできなかった。実際に、ピュア妖精たちを働かせて彼らと意思疎通を行うためには、部品を組み合わせて、もっと本格的な通信を行えばよい。

そのためには、まだ説明していない部品がいくつか必要になる。
まずは、部品を組み合わせるための道具を紹介する。これだ。

図 延長コード?

助手「まさかとは思いますが、ただの延長コードではないですよね?」

もちろん。この道具には、大いなる秘密が隠されている。
まず、コンセントの差込口だか、2つある。これは、厳密に決められており、一つや三つでは具合が悪い。
なぜならば、これは、二つの部品を組み合わせるための特別な装置であり、我々の世界のマルチタップと似ているのは、偶然の一致と言うほかない。
さらに、コンセントの片方には、情報を引き出すためのプラグの差込口がついている。つまり、このタップは、電源を供給するとともに情報のやり取りもできる装置なのだ。
そのため、三つ又になっている方の差込口にはめるための特別な装置が存在する。これだ。

図 情報端末

助手「なんか、ちょっと凄そうな物が出てきましたね」

そう。この端末は特別だ。情報取得のためのプラグと電源の両方が刺さっていないと動作しない。そして、情報プラグは、マルチタップのもう一つの差込口につながれた機器が発する情報を取得し、画面に表示させる機能がある。
この画面こそが、我々とピュア世界をつなぐ通信装置となる。

言葉では、わかりにくいと思うので、繋ぎ込んだ様子を図で示そう。

図 組み合わせ例1

これは、"getChar"にて我々の世界から一文字読み込み、ピュアな世界で大文字に変換したものを"putChar"で、我々の世界に返すプログラムを表している。
順を追って説明しよう。

  1. mainコンセントから電源を引き込む。電源は、右側のコンセントを通って、"getChar"装置に流れ込む。
  2. 先ほどと同じく"getChar"を利用して、我々の世界と通信する。
  3. こちらで入力した文字は、コンセントと情報プラグを通して、情報端末に流れ込む。
  4. 情報端末では、受け取った情報を元に、モニター上に「a」を表示する。
  5. ピュア妖精(モナドちゃん)は、モニターを観察していて、文字が表示されると、決められた反応を示す。多少のドジを織り交ぜたとしても、最終的な結果はいつも同じとなる。
  6. この場合、"putChar"装置に「A」を入力するという反応を起す。
  7. そして、その反応は情報端末に用意されたコンセントと通じてマルチタップに引き渡される。
  8. マルチタップは、渡された情報(「A」)をmainコンセントを通じて我々の世界に返す。

このような動きを通じて、我々は、関数世界に関与し、ピュア妖精を動かして、「A」という情報を得ることができる。
注意してほしいのは、情報端末のモニターを見るのは我々ではなく、ピュア妖精たちであるという点だ。
我々は、関数の世界で起こることの途中経過を直接見ることはできない。すべては、mainコンセントから出力された情報を通してみることになる。
そして、さらに特筆すべきことに、マルチタップのコンセント×2、情報端末のコンセント×1、mainコンセントのすべてに装置が接続された状態でないと、回路そのものが動作しない。
この回路をHaskellのプログラムで表すと、こうなる。

main = getChar >>= \x -> putChar (toUpper x)

"toUpper"の部分が、モナドちゃんの仕事を表している。
"getChar"を通して、モニター(引数x)に映った「a」という文字を大文字に変換し、"putChar"に入力している妖精の姿が見えてきたのではないかと思う。

複雑化

この装置の素晴らしいところは、組み合わせていくことで、幾らでも複雑な回路を作り出すことができるところだ。
例えば、こんな形で組み合わせると、2文字の入力を受け取って、大文字にして出力するプログラムが書ける。

図 組み合わせ例2

先ほどより大分複雑な印象を受けるが、使用しているパーツは、同じである。違いと言えば、情報端末に別のマルチタップが接続され、mainからの流れと同じく、"getChar"にて文字列をもう一回読み込んでいる。
ピュア妖精は、二つのモニターを同時に観察し、大文字化、結合、出力という仕事をしている。
Haskellのプログラムで表すと、こうなる。

main = do x <- getChar
          y <- getChar
          putStr [toUpper x, toUpper y]

さらに、別の意味でも素晴らしい点がある。それは、組み合わせた装置も、個々の装置と同じ形をしており、実装の細部を無視して一つのパーツとみなせることだ。

図 部品化する

先ほどの、"getChar"と"putChar"の組み合わせに"getputChar"というラベルを付け、ふたをしてしまうと、"getChar"と同様に部品として扱うことができる。

図 部品を組み合わせる

この図は、上記の二つの回路にラベルを張り、組み合わせる様子をを表している。
Haskellのプログラムでは、このようになる。

getputChar = getChar >>= \x -> putChar (toUpper x)

getgetputStr = do x <- getChar
                  y <- getChar
                  putStr [toUpper x, toUpper y]

main = getputChar >>= \x -> getgetputStr

このような特性を利用し、次々に部品を組み合わせていくことで、まるでブロックを組み合わせていくような感覚で、プログラムを書いていくことができる。


これが、モナドのしくみである。


そして、この時、どこまで組み合わせても、IO aのmainコンセントからパーツを組み合わせる限り、我々の世界とピュア世界が完全に隔離されていることが保障される。
これは、I/Oに特化し、副作用を隔離するIOモナドの特性となっている。

助手「ご清聴、ありがとうございました。」