Edited at

世界一わかりやすいStateモナドのしくみ、あるいは猫でもわからないモナドのおはなし

More than 1 year has passed since last update.

この記事は、


  • HaskellとかPureScriptみたいな関数指向言語をさわってみたことがある

  • Haskellっぽい記法がちょっとわかる(説明はします)

  • モナドを理解しようと頑張ったことがある

  • カリー化の意味はわかる

  • 嘘タイトルでもおこらないよ

みたいなひとを対象にしています。おこらないで。


何故今さらモナド?

世の中の全てがモナドになって久しい今でもモナドわかんないってひとが多いみたいなので備忘録がてらまとめることにしました。

聞かれた時にすぐ答えられるように考えをまとめる意図もあります。

厳密な話は一切しないので、基本的に説明不足だと思いますけど、そういうのは理解してから覚えればいいと思うのでゆるして。


ようご



  • a, b, c, d 任意の型を指します。よくIntとかStringになります。


  • a -> b 関数です。型aの値を入れると型bの値を返す関数です。


  • (a, b) タプルです。型aと型bをセットにして一つの型にしたものです。


  • (a -> b) >>> (b -> c) 関数合成です。a -> cになります。Haskell的にかくと、(>>>) :: (a -> b) -> (b -> c) -> (a -> c)ってとこです。

では、よろしくおねがいします。


Stateを、つくる


状態(State)とはなにか?

いろんな言い方があると思いますが、だいたいこういうもんだと思います。


  • 処理の「外」に持っていられる、書き換え可能なデータセット

  • 処理の連鎖の中で常に変化しながら伝播していくデータ

状態と呼ばれるものは「常に参照可能」で「更新可能」である必要があるようですね。


状態を持つ関数

さて、ここに関数があります。

a -> b

この関数は見ての通り「型aの値を受け取って型bの値を返す」ので、a以外の外部データを持つことはありません。「関数が状態を持つ」というのは、言い換えると、「関数内で状態の値を参照したい」「状態を任意に書き換えたい」ということになります。つまりこういうことです。

(a, s) -> (b, s)

sは状態(state)です。丁寧に読むと、「型asのペアを受け取って型bsのペアを返す」ということなので、関数内では受け取ったsの値を参照できて、好きに変更したsの値をbと一緒に返せるということになります。

ここで、こんな関数も拾ってきました。

b -> c

これも同じようにsを扱えるようにします。

(b, s) -> (c, s)

この関数は明らかに先ほどの関数と合成できます。

(a, s) -> (b, s) >>> (b, s) -> (c, s) == (a, s) -> (c, s)

こんなふうに、「sをおまけで受け取って、sをおまけで返す」を繰り返すことで、擬似的に状態を扱える関数を定義できた気分になりますね。なりませんか?なりませんか。


状態を持つ関数、だったもの

まずは、

(a, s) -> (b, s)

この関数を、おもむろにカリー化します

a -> s -> (b, s)

ここまではいいですね。

では、この関数の戻り値側にただの別名を付けてみます

a -> State b

え??


え????

State b == (s -> (b, s))

この形、どっかで見覚えありますね?

それはしばらく置いといて、

(b, s) -> (c, s)も同じように書き換えましょう。

b -> State c

これらの関数も明らかに合成できま……

(a -> State b) >>> (b -> State c)


合成できない!!!!!!!

>>>は左の関数の戻り値を右の関数の引数に渡すだけのものなので、State bbの型が合わないので合成できません!!どうしよう???

じゃあ、合成する演算子を用意しよう!!!


合成再び

(a -> State b) >=> (b -> State c) == (a -> State c)

すごい!合成できた!こんな都合のいい演算子を持ってくるなんてズルい!

でもこれ、Stateはただの別名なので、最初の状態伝播関数合成と中身はおんなじなんです。ということは、この中で実は状態が扱えるようになってるんですね!わけがわからん!

実は、これがモナドの全てなんです。


モナド


モナドって結局なんなの

モナドという単語が何を指すものなのかが曖昧なのが世の中のモナドが難しい理由の全てだと思います。


モナドはStateとかMaybeとかのやつだよ

それはただの「HaskellでMonadクラスのインスタンス」であって、実際はただのデータ型です。


モナドはただの型クラスだよ

間違ってはいないけど、それはHaskellの話であって、モナドを使うだけならいいけど理解するには向いてない説明だと思います。


モナドは単なる自己関手の圏におけるモノイド対象だよ

あってるけど、それで理解できるなら今困ってないね!!!


今回は、こういう説明をすることにします。

「モナドは、特定の形をした関数がキレイに合成できるという「性質」のことだよ」


ここで突然のかけ算

みなさんはかけ算って知ってますか?知らないかもしれないので、説明しましょう!!!

2 * 3 = 6

これはどういう意味でしょうか!



  • (*)という二項演算子があります


  • (*)IntIntを取って、Intを返します

つまり、かけ算(*)とは、二つのIntを合成して一つにするものと言えますね!

さらに、かけ算にはすごい性質があります!!

2 * (3 * 4) == (2 * 3) * 4

複数のかけ算をするとき、どこから先に計算しても同じ答えになります!

1 * 2345 == 2345, 2345 * 1 == 2345

右に掛けても左に掛けても答えが変わらない、特別な数字1が存在します!


かけ算の性質

実は、これらの性質を全て持った演算には特別な名前が付けられます。

それは、

「モノイド」

物々しい名前が出てきたけど、今の性質のことを指す用語なのでこわがらないでください!!

「性質のことを指す用語」って、さっきもあったような気がしますね……?


関数合成再び

関数合成って、(a -> b) >>> (b -> c) == (a -> c)なんですけど、これって二つの(? -> ?)をくっつけて一つの(? -> ?)になるように見えてきません?見えてきましたね?

「二つの関数を合成して一つにする」

(a -> b) >>> { (b -> c) >>> (c -> d) }

関数三つの合成ですけど、これって実は合成順番を入れ替えても同じ結果になるんです。

{ (a -> b) >>> (b -> c) } >>> (c -> d)

「どこから先に計算しても同じ結果になる」

a -> bばっかり出てきてますけど、a -> aとかb -> bっていう関数もありますね?

(a -> a) >>> (a -> b) == (a -> b)

(a -> b) >>> (b -> b) == (a -> b)

「右に掛けても左に掛けても結果が変わらない特別な関数、id :: a -> a

これは…………「モノイド」!!!!


さあ、Stateの出番だ!

「二つの謎関数を合成して一つにする」

(a -> State b) >=> (b -> State c) == (a -> State c)

「どこから先に合成しても同じ結果になる」

(a -> State b) >=> { (b -> State c) >=> (c -> State d) }

{ (a -> State b) >=> (b -> State c) } >=> (c -> State d)

「右に掛けても左に掛けても結果が変わらない特別な謎関数、return :: a -> m a

(return :: a -> State a) >=> (a -> State b) == (a -> State b)

(a -> State b) >=> (return :: b -> State b) == (a -> State b)


おわかりでしょうか

そう……謎関数(State付き関数)も、簡単に合成して一つにできる……「モノイド」……「モナド」……全ての謎が解けた……


わからない。結局モナドってなに

え?今説明した通りですよ?

* -> m *みたいな変な形の関数同士を合成して一つにできて、どこから先に合成しても結果が変わらなくて、右から合成しても左から合成しても式が変化しない特別な関数が存在する」ものがモナドです。

同じ説明がStateでもMaybeでもListでもEitherでも成り立つんです。ほんとですよ。

いや、本当にモナドってこれだけなんです。


モナド入門の鬼門IOを爆破せよ

そのまえに、Stateくんのことを思い出してあげてください。

Stateくんは、「状態sを受け取り内部で参照でき、かつ更新したものを返せる関数」に別名を付けただけのものだったこと、思い出しましたか?このsって何でしょうね。

sIntだったら?カウンタのようなものが作れますね。いかにもStateって感じで分かりやすい!

今度はsConsoleを入れてみましょう。Consoleってのはあなたの手元にあるコンソールです。そうですそれです。「Consoleを内部で参照でき、かつ更新して返せる関数」ということは、コンソールを参照してreadや、コンソールを更新してwriteができるということになりますね!なるんです!!

では……sこの世界のすべてを入れたら……?

それが、IOです。

IOはただのStateだったのです。そのsの内容が派手なだけで。Stateだということは、こうも言えます。IOはただの関数である。

ただの関数が副作用を起こせるのは、あなたがそのプログラムを起動するときに、無意識にこの世界の全てを引数に渡しているからだったのです。


まとめない

以下落ち穂拾いです

ここからは本気で全く説明する気がない説明なので読み飛ばしてよいです


>>=とかあったよね?

今日見てきた演算子は(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)ですけど、いろんなところで見かける演算子は(>>=) :: m a -> (a -> m b) -> m bですよね。これ、実はほとんど同じで、

(a -> m b) >=> (b -> m c) == (a -> m c)

a -> (m b >>= (b -> m c)) == a -> (m c)

演算子でつなぐ位置が違うだけなんですね。で、下の方がよく使われるだけです。

なんで>>=の方が使われるかというと、二つを見比べてもらえば分かるんですけど、b -> m cの関数のスコープ内にaが含まれてるんですね。つまりクロージャで左辺の引数まで参照できてしまうんです。これが便利な場面がとても多いので、>>=が主流になっています。


関数が合成できるだけでなんでそんなに騒いでたの?

え?関数が合成できるからプログラムが組めるんでしょう?


結局のところモナドのメリット

関数合成に任意の追加計算を含ませることができることがメリットですが、プログラミング言語的には、極論すると「do記法が使える」ってところでしょうか。do記法は言い換えると「純粋関数の世界で手続き型の記法をエミュレートできる」ものと表現できます。安全な純粋関数プログラミング中で、まるで副作用があるかのような単純明快な構文が使えたら、それって便利だと思いませんか?実はこれ、実現するためにモナドの合成ルールを活用する必要があるんです。モナドサポート構文自体はいろんなプログラミング言語に存在していて、Haskellはdo記法、F#にはコンピュテーション式、C#には(かなり限定的だけど)Linqのクエリ構文、などがあります。


何か問題でも?

モナドの関数a -> m bって、「*を受け取ってm *を返す関数m」みたいに表現することもできます。いま関数って言ったけど、ここで*がどんな型であっても成り立つので、「mは関手である」というのが正しいです。HaskellとかPureScriptでは対象は型しかないので、関手は必然的に「型と関数の圏」の自己関手になります。で、mを対象にするということは「関手の圏」上の話になり、そのmは先ほどモノイドみあふれる合成テクを見せてくれたばかりなので、「モナド(m)は単なる自己関手の圏におけるモノイド対象」なんですね。何も問題はありませんね?


おわり

まとめを書く体力はありませんでした。

モナドは難しくない、っていう表現はちょっと間違ってて、モナドに難しいとこなんてある??っていうのが正しいですかね!ぼくは理解するまでとても時間が掛かりました!!!!!

でも実際、純粋関数はこわくない。なんたってモナドがあるからね。というわけで、みんなで純粋関数プログラミングをはじめるなら今です!

今とってもHotな関数指向言語だと、PureScriptがめっちゃオススメです!