Hatena::ブログ(Diary)

セカイノカタチ::Techlog Twitter

2013-06-24

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

前置き


みなさん、モナドって、わかりにくいですよね。

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

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

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

そして、去年の年末ぐらいに、ついにそのカタチの具体的なアイディアを閃きました。



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

二つの世界


この世界は大きく二つに分かれている。我々の住む、この穢れた世界と、関数プログラマたちの約束の地、穢れなき純粋な関数の世界だ。

まずは、その純粋な関数の世界に住む住人たちを紹介しよう。

図 ピュア妖精たち
f:id:qtamaki:20130624203856p:image

彼らが、この世界の住人、ピュア妖精だ。ピュア妖精はピュアなので、同じ刺激を入力してあげると、必ず同じ行動をし、同じ結果を返してくれる。

実に不思議なピュア世界の住人たちだ。

先頭にいるのはモナドちゃん。ちょっとドジっ子な点を除けば、他のピュア妖精と変わる所はないし、勿論、同じ入力に対しては必ず同じドジを繰り返す。

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

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

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

図 メインソケット
f:id:qtamaki:20130624203857p:image

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

彼らと我々を繋ぐ装置は、この様なカタチをしている。我々の世界のコンセントに近い形状をしているが、違う点もある。

あと、急に助手が出てきてビックリしたかもしれないが、一人で喋っているのが寂しくなって来たので呼んでみた。

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

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

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

図 ピュア妖精のおウチ
f:id:qtamaki:20130624203858p:image

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

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

IOモナドを繋ぐ


言い忘れたが、mainコンセントは、「 IO a 」という型を持っている。

戻り値が「 IO a 」となる関数をつなぐと、コンセントから電気のようなものが通って、回路が動くようになっている。この点において、我々の世界のコンセントと家電の関係と同じだと言える。

つまり、モナドとは家電のことである。

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

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

図 getChar
f:id:qtamaki:20130624203859p:image

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

図 mainにgetCharを繋ぐ
f:id:qtamaki:20130624203900p:image

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

Haskellプログラムで表すとこうなる。

main = getChar

このプログラムを実行すると、標準入力を受付け、1文字入力すると何も起きずに終了する。

重要なのは、標準入力の受付という副作用を伴う操作をしても、ピュアな関数の世界には何ら影響を与えないと言うことだ。さらに言うと、"getChar"は引数を伴わず関数ではない。にもかかわらず、我々の世界には影響を与えている。

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

部品を組み合わせる


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

そのためには、まだ説明していない部品がいくつか必要になる。

まずは、部品を組み合わせるための道具を紹介する。これだ。

図 延長コード?
f:id:qtamaki:20130624220645p:image

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

もちろん。この道具には、大いなる秘密が隠されている。

まず、コンセントの差込口だか、2つある。これは、厳密に決められており、一つや三つでは具合が悪い。

なぜならば、これは、二つの部品を組み合わせるための特別な装置であり、我々の世界のマルチタップと似ているのは、偶然の一致と言うほかない。

さらに、コンセントの片方には、情報を引き出すためのプラグの差込口がついている。つまり、このタップは、電源を供給するとともに情報のやり取りもできる装置なのだ。

そのため、三つ又になっている方の差込口にはめるための特別な装置が存在する。

これだ。

図 情報端末
f:id:qtamaki:20130624220837p:image

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

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

この画面こそが、我々とピュア世界をつなぐ通信装置となる。

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

図 組み合わせ例1
f:id:qtamaki:20130624222230p:image

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

順を追って説明しよう。

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

このような動きを通じて、我々は、関数世界に関与し、ピュア妖精を動かして、「A」という情報を得ることができる。

注意してほしいのは、情報端末のモニターは我々の為にあるのではなく、ピュア妖精たちが使うために沿うん剤するという点だ。

我々は、関数の世界で起こることの途中経過を直接見ることはできない。すべては、mainコンセントを通して最終的に受け取れる情報を通して覗き見ることになる。

そして、さらに特筆すべきことは、マルチタップのコンセント×2、情報端末のコンセント×1、mainコンセントのすべてに装置が接続された状態でないと回路そのものが動作しない。

この回路をHaskellプログラムで表すと、こうなる。

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

"toUpper"の部分が、モナドちゃんの仕事を表している。

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

複雑化


この装置の素晴らしいところは、組み合わせていくことで、幾らでも複雑な回路を作り出すことができるところだ。

例えば、こんな形で組み合わせると、2文字の入力を受け取って、大文字にして出力するプログラムが書ける。

図 組み合わせ例2
f:id:qtamaki:20130624222804p:image

先ほどより大分複雑な印象を受けるが、使用しているパーツはだいたい同じである。

違いと言えば、情報端末に別のマルチタップが接続され、mainからの流れと同じく、"getChar"にて文字列をもう一回読み込んでいる。

ピュア妖精は、二つのモニターを観察し、大文字に変換して、文字列として結合、そして出力という仕事をしている。

Haskellプログラムで表すと、こうなる。

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

さらに、別の意味でも素晴らしい点がある。

それは、多数の部品を組み合わせた複雑な装置の場合でも、最終的にコンセントがひとつ飛び出した形をしており、実装の細部を気にすること無く、ひとつの部品とみなせるのだ。

図 部品化する
f:id:qtamaki:20130624225031p:image

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

図 部品を組み合わせる
f:id:qtamaki:20130624235629p:image

この図は、上記の二つの回路にラベルを張り、組み合わせる様子をを表している。

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モナド特性となっている。

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

*1:そしてずっこけながら

*2:つまり我々の世界

初心者初心者 2013/11/18 01:25 図組み合わせ例1 → 図組み合わせ例2 においてモナドちゃんからピュア妖精に変わった理由がはっきりしませんでしたm(_ _)m
結局二種類の違いはどこにあるのでしょうか。そしてモナドちゃんは先頭のただ一人だけなのでしょうか。

ストロベリーストロベリー 2014/04/08 19:36 すごくありがとうございました。m(_ _)m

きょうきょう 2014/08/28 12:15 モナドちゃんは初めに繋がったピュア妖精だから?最初の繋がりの時に何か他のピュア妖精さんと違うドジっ子による働きがあるからこそ最初に繋がるのでしょうか?なんだか楽しく解説していただいて、謎が一つ解けて謎が一つ増えましたがホンとにおバカ頭でも何とかついて行けましたありがとうございました(^O^)/

きょうきょう 2014/08/28 12:18 きゃ〜何故故こんなにメールがつらつらと出てきたの?超申し訳ございませんが消し方がわかりませんm(_ _)mゴメンナサイと玉ねぎ頭のピコも申しております☆彡

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証