モナドって何ですか?
これをちゃんと説明できる人に会ったことがない。
↑1に挑戦してみます。
まず、モナドには数学の圏論におけるモナドとプログラミングにおけるモナドがあります。圏論のモナドについては僕もよくわかっていません。圏論のモナドとプログラミングのモナドは同じものだというような説明がされていますが、本当に等価なのか僕には判断できません(どうもプログラミングのモナドは圏論のモナドの条件を満たしていそうですが、逆は成り立たないんじゃないかと思っています)。
ここでは、 プログラミングにおけるモナド について説明します。コードはすべて Swift で書きます。モナドについて調べると、よく モナド則 について書いてあるんですが、肝心の記述が関数型言語で書かれていてわからん!となることが多いんじゃないかと思います。本投稿では、 モナド則についても Swift の構文で記述します。
まず前半でモナドとは何かについて述べ、 それだけではさっぱり意味がわからないと思うので、後半でモナドがどのように役立つのかを例を使って説明します。
モナドとは
Foo<T>
が次の条件2を満たすとき、 Foo<T>
はモナドです。
-
init(_ x: T)
というイニシャライザを持つ。 -
func flatMap<U>(f: T -> Foo<U>) -> Foo<U>
というメソッドを持つ。 - 上記の実装が モナド則(後述)を満たす。
-
Foo<T>
は ファンクター(後述)である。
ざっくりと言えば、適切に実装された(モナド則を満たした) flatMap
を持った型はモナドと言えるでしょう(モナドはファンクターでもあるので map
も持っている必要がありますが)。
モナド則
-
Foo(x).flatMap(f) == f(x)
が成り立つ。 -
foo.flatMap { Foo($0) } == foo
が成り立つ。 -
foo.flatMap { f($0).flatMap(g) } == foo.flatMap(f).flatMap(g)
が成り立つ。
モナド則は、中身をじっくり見てみればわかりますが、ごく当たり前に満たすべき性質を言ってるだけです。言ってみれば、積の結合法則 (a * b) * c == a * (b * c)
のモナド版のようなものです。
例えば、最初の法則は x
を Foo
で包んでからやっぱり中身の x
を取り出して関数 f
を適用した結果は、直接 f
を x
に適用した場合と等しいという意味です。要するに、イニシャライザで値を包むときや flatMap
で取り出すときに余計なことをするなというだけです。
積の結合法則を満たさなければ数とは言えないというのと同じように、モナド則を満たさなければモナドとは言えないという当然の性質を述べているに過ぎません。自作の Number
型に結合法則が成り立たないような *
を実装することはできますがそれが意味を持たないのと同じように、モナド則を満たさないモナドっぽい型を実装することはできますがそれはナンセンスなのです。
他の二つについても、中身をじっくり見てみればそれが何を意味するのかわかると思いますし、当然満たすべきものであることがわかると思います。
ファンクターとは
前述の通り、モナドはファンクターでないといけません。ファンクターとは何でしょうか。
Foo<T>
が次の条件を満たすとき、 Foo<T>
はファンクターです。
-
func map<U>(f: T -> U) -> Foo<U>
というメソッドを持つ。 - 上記の実装が ファンクター則(後述)を満たす。
同じくざっくりと言えば、適切に実装された map
を持った型はファンクターと言えるでしょう。
ファンクター則
- 恒等関数
id
(後述)に対して、foo.map(id) == id(foo)
が成り立つ。 -
foo.map { g(f($0)) } == foo.map(f).map(g)
が成り立つ。
ファンクター則もモナド則同様、じっくり見れば当然満たすべきものだとわかると思います。
恒等関数 id
恒等関数 id
とは、次のような引数で受けたものをただ返すだけの関数です。
func id<T>(x: T) -> T {
return x
}
モナドって何の役に立つの?
ここまで見てきても、モナドがどういうものか、どのように役立つのか、よくわからないと思います。本節では例を使ってモナドを説明します。
まず、僕がモナドについていちばんしっくり来たのは説明を紹介します3。実際にモナドを使っていてもこういう感覚で使います。
モナドというのは、モナドでくるまれた中の世界ではモナドを気にせずに処理が記述でき、外からみるとモナドでくるまれているという、外と中を分離するための仕組みです。
具体例を見てみましょう。
身近なモナドの例として Optional
を取り上げます。なお、以下では Int?
の代わりに Optional<Int>
と書きますが、これらは等価です。 Optional<Int>
と書いた方が、前述のモナドの条件と見比べやすいと思うのでそのように書きます。
なお、最低限の知識として、 Swift の Optional
とクロージャ、高階関数の基本については理解しているものとして説明します(参考: "SwiftのOptional型を極める" )。
map の例
let s: String = ...
let n: Optional<Int> = Int(s) // s のパースに失敗したら nil
上記のような n
があったとします。例えば、 s
が "123"
なら n
は 123
ですし、 s
が "abc"
なら n
は nil
になります。
さて、この n
を二乗するにはどうすれば良いでしょう? n
は Int
ではなく Optional<Int>
なので、そのまま n * n
とは計算できません。答えは↓です。
// n が数値なら nn は二乗した結果、 n が nil なら nn も nil
let nn: Optional<Int> = n.map { $0 * $0 }
これがやっていることは、まさにさっき引用した文章そのままです。 map
に渡したクロージャ( { $0 * $0 }
)の中に 「モナドでくるまれた中の世界」 を作り、そこでは 「モナドを気にせず」 Int
として $0 * $0
が計算できています。一方、 「外からみると」 n
も nn
も Optional
という 「モナドでくるまれ」 たままです。このように、モナドの持つ map
や flatMap
が 「外と中を分離する仕組み」 となり、 それらを利用することで外と中を分離したまま処理することができるようになる わけです4。
flatMap の例
map
で「中の世界」を作れればそれだけで十分じゃないかと思ってしまいますが、 flatMap
がなければうまくいかないケースもあります。
"SwiftのOptional型を極める" の「力試し」の問題 3, 4 の解答がそれに当たるので、そちらを御覧下さい。実は上記の map
の例も「力試し」の問題 1 と同じです。
Optional 以外の例
Optional
だけではわかりづらいところもあるので、過去の投稿の中から、他のモナドについて述べたものを挙げておきます。
-
Either
: Swift 3.0で追加されそうなEitherについて -
Array
: ArrayとOptionalのmapは同じです, "Swift 2.0の新しいflatMapが便利過ぎる" -
Promise
: koher/PromiseK, "Promise と比べてみる"
まとめ
ざっくり言うと、モナドとは map
と flatMap
を持った型で、それらがファンクター則、モナド則を満たしている必要があります。
map
や flatMap
を使うと、モナドに包まれた中の世界とモナドの外の世界を分離したまま処理を記述することができ、いちいち包まれた値を取り出したり包みなおしたりしなくていいので便利です。
-
@syou007 さん、"プログラマー初学の人への質問" より。 ↩
-
十分条件 です。 必要条件 ではありません。つまり、この条件を満たせばモナドと言えますが、これでなければならないわけではありません。他の表現の仕方もあります。そのため、「定義」という言葉は使っていません。例えば、
map
やflatMap
という名前でなければならないわけではありませんし(flatMap
はbind
と呼ばれることも多いです)、map
やflatMap
がメソッドでなくグローバル関数だとしてもモナドと言えます。 Haskell ではmap
は(T -> U) -> Foo<T> -> Foo<U>
という型の関数です。他にも、flatMap
の代わりにfunc flatten<T>(foo: Foo<Foo<T>>) -> Foo<T>
という関数を実装しても OK です。その場合、flatMap
はflatten(foo.map(f))
によって実現されます。詳しくは知りませんが、圏論ではflatMap
ではなくflatten
相当のものによって表現するのが一般的なようです。 ↩ -
ただし、ここまでは
map
しか使っていないのでモナドでなくてもファンクターで十分な話です。 ↩