モナドについてSwiftで説明してみた

  • 262
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

モナドって何ですか?
これをちゃんと説明できる人に会ったことがない。

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) のモナド版のようなものです。

例えば、最初の法則は xFoo で包んでからやっぱり中身の x を取り出して関数 f を適用した結果は、直接 fx に適用した場合と等しいという意味です。要するに、イニシャライザで値を包むときや 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" なら n123 ですし、 s"abc" なら nnil になります。

さて、この n を二乗するにはどうすれば良いでしょう? nInt ではなく Optional<Int> なので、そのまま n * n とは計算できません。答えは↓です。

// n が数値なら nn は二乗した結果、 n が nil なら nn も nil
let nn: Optional<Int> = n.map { $0 * $0 }

これがやっていることは、まさにさっき引用した文章そのままです。 map に渡したクロージャ( { $0 * $0 } )の中に 「モナドでくるまれた中の世界」 を作り、そこでは 「モナドを気にせず」 Int として $0 * $0 が計算できています。一方、 「外からみると」 nnnOptional という 「モナドでくるまれ」 たままです。このように、モナドの持つ mapflatMap「外と中を分離する仕組み」 となり、 それらを利用することで外と中を分離したまま処理することができるようになる わけです4

flatMap の例

map で「中の世界」を作れればそれだけで十分じゃないかと思ってしまいますが、 flatMap がなければうまくいかないケースもあります。

"SwiftのOptional型を極める" の「力試し」の問題 3, 4 の解答がそれに当たるので、そちらを御覧下さい。実は上記の map の例も「力試し」の問題 1 と同じです。

Optional 以外の例

Optional だけではわかりづらいところもあるので、過去の投稿の中から、他のモナドについて述べたものを挙げておきます。

まとめ

ざっくり言うと、モナドとは mapflatMap を持った型で、それらがファンクター則、モナド則を満たしている必要があります。

mapflatMap を使うと、モナドに包まれた中の世界とモナドの外の世界を分離したまま処理を記述することができ、いちいち包まれた値を取り出したり包みなおしたりしなくていいので便利です。



  1. @syou007 さん、"プログラマー初学の人への質問" より。 

  2. 十分条件 です。 必要条件 ではありません。つまり、この条件を満たせばモナドと言えますが、これでなければならないわけではありません。他の表現の仕方もあります。そのため、「定義」という言葉は使っていません。例えば、 mapflatMap という名前でなければならないわけではありませんし( flatMapbind と呼ばれることも多いです)、 mapflatMap がメソッドでなくグローバル関数だとしてもモナドと言えます。 Haskell では map(T -> U) -> Foo<T> -> Foo<U> という型の関数です。他にも、 flatMap の代わりに func flatten<T>(foo: Foo<Foo<T>>) -> Foo<T> という関数を実装しても OK です。その場合、 flatMapflatten(foo.map(f)) によって実現されます。詳しくは知りませんが、圏論では flatMap ではなく flatten 相当のものによって表現するのが一般的なようです。 

  3. "Java8でのプログラムの構造を変えるOptional、ただしモナドではない - きしだのはてな" より。 

  4. ただし、ここまでは map しか使っていないのでモナドでなくてもファンクターで十分な話です。