関数型つまみ食い: モナドが難しいと思われている理由

(「モナドについて書かれたものを読む度に自分で書いてしまいたくなる。同じ過ちを繰り返すだけだから止めた方がいいんだけど。」という話を受けて、)

世の中に溢れる粗悪なモナド入門のせいで Philip Wadler氏の論文が埋もれているからモナドが難しいと思われるんだ。

ほほう、その論文を読めばモナドとやらを簡単に理解出来るのかな、と思いつつ読んでみると、

全然簡単じゃないんですけど…

でも、この論文でモナドの意義と基本的な仕組みはぼんやりと見えたような気もしなくはない。その理解をここに簡単にまとめておくことで、同じ過ちを繰り返してみることにしよう。

まずモナドを理解するためには、関数型プログラミングにおける、純粋な関数とそうでないものの区別をきちんとさせておく必要がある。

純粋な関数とは何か?

そもそも関数とは何か? それは、入力を出力に「変換」するものである。

関数: 入力 =(変換)=> 出力

至ってシンプルである。関数型プログラミングでは、この関数を組み合わせて、変換の連鎖としてアプリケーションを表現する。

さて、この関数が以下の二つの条件を満たすとき、その関数を「純粋」な関数と呼ぶ。

  1. 同じ入力に対しては常に同じ出力を返すこと
    • 例えば 1 を渡したら 2 が返ってくる関数があったとして、どのようなタイミングや事前条件で関数を実行しても 1 => 2 という同じ結果になること、そしてそれが他の入力パターンについても同様に成り立つこと。
  2. 関数は入力を出力に変換するものだと説明したが、その変換以外のことが一切起こらないこと
    • この変換以外の出来事を「副作用」と呼ぶ。

純粋な関数のメリットと痛み

Elixir試飲録 (4) – オブジェクト指向と関数型の違い」で触れたように、オブジェクト指向ではシステムの構成要素であるオブジェクトの「インターフェース」にフォーカスすることによって、システムの表現を単純化し、さらには疎結合による柔軟性を獲得していた。しかし一方で、インターフェースに現れない内部的なデータの流れは隠蔽されるため、インターフェースだけでシステムの(厳密な)状態遷移を把握することは困難になる。

object

少ない記述で多くを実現しようとするオブジェクト指向に対して、関数型、特に純粋関数型では、原理的に関数(入出力の変換)として書かれたことしか起こらないので、不測の事態が起こりづらく、より信頼性の高いシステムを構築しやすいと言える。しかし、逆に言えば、全てのデータの流れを入出力の変換として明示的に表現しなければならないので、時にはコードが冗長になってしまうケースがあるだろう。

function

この関数型の欠点とも呼べる冗長さを軽減するために導入されたのが「モナド」と呼ばれるコンセプトである。

モナド概念の導入

「関数型の欠点とも呼べる冗長さ」とは具体的にどのようなものだろうか? 以下のような例で考えてみる。

ここに「倍返し」という関数がある。名前の通り、受け取った数値を倍にして返す関数である。

倍返し: 数値 =(変換)=> 数値

この関数に 1 を渡すと 2 が返り、2 を渡すと 4 が返る。

この関数が「受け取った数値を倍にする」ことだけに専念しているとき、この関数は純粋であると言える。

しかし、世知辛い現実世界では、単に「倍返し」の純潔を守っているだけでは生き残って行けない。色んな要求が舞い込んでくる。例えば、倍返しに失敗したらどうしてくれるんだ?、だったり、倍返ししたことをちゃんと記録しておけよ、等々。そこで以下のような要求があった場合、どのように対応すれば良いだろうか?

  1. エラーが発生したら、それをきちんと知らせて欲しい
  2. 何回「倍返し」したのか記録しておいて欲しい
  3. 倍返しする度にログを吐いて欲しい

もし、関数が純粋でなくても良いなら、話は簡単である。エラー処理については、出力以外にも「例外」という経路を設けて、そこからエラーを知らせればよい。実際に多くのプログラミング言語ではこの方式を採用している。倍返し回数の記録についてはグローバルな変数を用意してそれをカウントアップして行けばよいし、ログについては深く考えることなく、普通にログを吐けばいい。

しかし、関数を純粋に保ったまま、つまり純粋な関数のメリットを維持したまま、このような機能を実現するためにはどうすればよいだろうか? 単純に考えると、出力にそのような情報を追加するしかない。

倍返し1: 数値 =(変換)=> 数値, エラー
倍返し2: 数値 =(変換)=> 数値, 回数
倍返し3: 数値 =(変換)=> 数値, ログ

エラーを投げたり、回数を記録したり、ログを吐いたりするのは「副作用」に当たるので、関数の純粋性を守るためには、これらの処理は「予定」として、関数の出力に含めて表現することになる。

これは明確さという観点から言うととても分かりやすい。関数の定義を見ればどのようなことが起こるのかが一目瞭然だからである。しかし一方で、関数の定義に、本来の処理「倍返し」以外の情報が紛れ込んでくるし、要求が増える度にいちいちこのような関数のバリエーションを増やして行かなければならないのだろうか?

そこで、このような「入出力に紛れ込む副作用」を本来の処理から隠蔽して冗長さを軽減するために導入されたのが、いわゆる「モナド」である。

「倍返し」の例を一般化すると、

倍返し: 数値 =(変換)=> 数値, おまけ

となるが、この関数に、どのような「おまけ」があるにしろ、そのことを意識せずに一番純潔な

倍返し: 数値 =(変換)=> 数値

として扱えれば便利である。そのために必要となるのが以下の仕組みである。

  1. まず前提としておまけ付きの出力: [本来の出力, おまけ]
  2. 本来の出力をおまけ付きに変換してくれる仕組み: 出力値 => [本来の出力, おまけ]
  3. おまけ付きの値を、そのままおまけ無しの値として関数に渡せる仕組み: [本来の入力, おまけ] => 本来の入力

この三つの組み合わせを「モナド」と呼ぶ。これらの道具立てがあれば、関数本来の計算がおまけに埋没することなく、かつ関数の純粋さを保つことができるというわけである。


[2017/05/18追記]

久しぶりにこの記事を読んでみて、もう少し説明を加えればモナドに対する理解がよりクリアになるような気がしたので、試しに書き加えてみる。

モナド三兄弟

関数型プログラミング(特に純粋関数型)は、システムの状態遷移を余すところなく関数の入出力として表現することで、システムの動きを明確にしようとする。全てを入出力として表現するので、ビジネスロジックにとって重要な「本来の入出力」以外の「おまけの入出力」がどうしても必要になってくる。

つまり、関数と関数の間を受け渡されていくデータには、常に何らかのおまけが含まれている可能性があるということだ。そのおまけは、例えば、処理に失敗した際のエラー情報だったり、値が存在しないかもしれないという可能性だったり、あるいは入出力には現れない文脈的なデータだったりする。このおまけの応用範囲がとても広いというのが、モナドのつかみどころのなさの一因になっていることは間違いなさそうだ。

しかし、関数の方はこのおまけを解釈するようにはできていないかもしれない、あるいはむしろ、特定のおまけを想定して関数を書いてしまうと、その関数の汎用性が大きく損なわれてしまうので、できれば本来必要な計算だけに集中したい。

そこで、おまけを想定しない関数を、おまけ付きの入出力を持つように変換するアダプター的な仕組みがあれば、本来の計算とおまけを切り離せる。この仕組みがモナドである。

より正確に言うと、この変換には三つのタイプがあり、その中の一つがモナドと呼ばれている。

具体的には、

a) 値 =(変換)=> 値  (全くおまけを想定してない関数)
b) 値 =(変換)=> 値, おまけ (おまけ付きの値を返す関数)
c) (値 =(変換)=> 値), おまけ (関数自体におまけが付いてる!?)

のような関数を、あたかも、

値, おまけ =(変換)=> 値, おまけ

のように扱えるようにしたい。この変換に対応するアダプターをそれぞれ、

a. Functor (ファンクタ)
b. Monad (モナド)
c. Applicative (アプリカティブ)

と呼ぶ。

ところで、上記三つの名前を見て、それぞれの機能が想像できるだろうか? これらは数学の「圏論 (category theory)」という分野由来の言葉で、そのままで意味をイメージできる人はかなりの少数派ではないかと思う。この名前の問題も、モナドが難しいというイメージを与える一因になっている。

そこで、以下のようにもっと分かりやすい名前で呼ぶようにしたらどうかという提案がなされているようだ。

  • Functor -> Mappable
  • Monad -> Chainable/Joinable/FlatMappable
  • Applicative -> Zippable

厳密にはこれらの語が意味するところとは異なるので(圏論的なニュアンスが抜け落ちるので)適切ではない、というのが専門家の見解であるようだが、これらの概念を理解する際の参考になることは間違いない。

まず Functor の Mappable という別名はとても分かりやすい。おまけ付きの値でも、[値 =(変換)=> 値] の関数でマッピングできるようにしてくれるものが Mappable というわけだ。

Functor で最もポピュラーだと思われるものが、値が「繰り返し」というおまけを持つ場合(これはつまりコレクションのこと)に対応する Functor だ。この Functor は、まさに map という名前で、多くの言語に導入されてお馴染みになった。コレクションを別のコレクションに変換するあの関数だ。この map に渡される変換用の関数は、値がコレクションの中に入っているという「おまけ」を気にすることなく、単に一つの値を別の一つの値に変換することだけに集中することができる。「本来の計算とおまけの分離」である。

Monad の Chainable も分かりやすい。

値 =(変換)=> 値, おまけ

という関数をつなげて (Chaining) 使いたい場合に、入力を変換するものが Chainable になる。

Applicative – 複数の入力値を取る関数をおまけ付き入出力に対応させる

Applicative はちょっとややこしい。これは、複数の入力値を取る関数に Functor 的な変換を行う際に使う。これまでの話では、あくまで一つの入力値を取るケースだけを想定して話をしてきた。

例えば、以下のように二つの入力を取る関数があるとする。

A, B =(変換)=> C

関数型プログラミングをサポートする言語の一部には、関数の「部分適用」という仕組みがある。これは複数の入力を受け取る関数があるときに、その中の一つの入力だけを渡して、その入力を受け取った状態の新たな関数を作る機能だ。

具体的に見てみよう。上の関数に、一番目の入力、A だけを与えると、入力を B 一つだけ受け取り、C を返す関数ができる。

A
↓
A, B =(変換)=> C
↓
B =(変換[A入力済み])=> C

さて、この部分適用という仕組みを踏まえて、二つの入力を取る関数におまけ付きの値を渡すためにはどうすれば良いだろうか?

まずは一つ目の入力値を Functor で渡してみよう。

A, おまけ
↓
A, B =(変換)=> C
↓
(B =(変換[A入力済み])=> C), おまけ

Functor は、入出力をおまけ付きに変換するので、部分適用して出来上がった関数がおまけ付きになってしまった。

というわけで、ここで Applicative の登場である。

すでに書いたように、Applicative は、

c) (値 =(変換)=> 値), おまけ

値, おまけ =(変換)=> 値, おまけ

に変換してくれる。つまり、

B, おまけ
↓
(B =(変換[A入力済み])=> C), おまけ
↓
C, おまけ

となって、最終的に欲しかった「C, おまけ」を手に入れることができた。

つまり、これら(Functor と Applicative)を組み合わせると、

A, おまけ and B, おまけ
↓
A, B =(変換)=> C
↓
C, おまけ

という感じで、複数の入力値を取る関数をおまけ付き入出力に対応させることができる、というわけである。

2 thoughts on “関数型つまみ食い: モナドが難しいと思われている理由”

コメントを残す