![]() |
この項目は、編集者が飽きたために内容が少ないです。 調べものなどの参考にはなりますが絶対的に内容が不足しています。 加筆、訂正などをして下さる協力者を求めています。 |
![]() |
この項目を分割する提案が出されています。 分割内容は掲示板をご参照ください。2018年12月22日以降に実施予定です。 |
プログラミングに出てくるモナドとはよくわからないなにか、あるいはHaskellの危険地帯である。
この記事を開いたあなたはきっと、「今度こそよくわからないモナドをきちんとわかるように解説してくれるページが現れたかもしれない(たぶん違うんだろうな)」と思ってページを開いたことと思う。
すまない。この投げやりな冒頭にさぞ手抜きだ立て逃げだと憤ったことであろう。しかし、モナドの説明に深入りしたあなたは、あちこちを調べて回った挙句、この冒頭に憤ったことも忘れてこう思うかもしれない。もうよくわからないなにかでいいや、と。
以下の記事を読んで「なんだ簡単じゃないか、脅かしやがって」と思えたなら、この記事の説明がよその記事よりは良かったということいやむしろ読者の理解力を褒め称えるべきであり、「もうよくわからないなにかでいいや」と思ったらこの冒頭は間違っていなかったことになる。
なお、最近では関数型プログラミングにモナドの理解は必要ないという意見が主流を占めつつあるように思える。当記事の初稿編集者も同意見で、モナドに属する個々の型・クラスのAPIドキュメントを理解することが必要かつ十分だと思うので引き返すなら今のうちである。
モナドに言語レベルでのサポートがあるのはHaskellくらいなので、他のプログラミング言語でモナドを知る必要性はさらに薄いといえる。
なお、この記事はやたら長くなっているが、それでもモナドの理論の入り口くらいまでで、実践するところまではカバーしていない。いくらやっても入り口から先に進むことが出来ないから、少々手間でも入り口くらいには立ってみたい。そういう人向けの記事でありたいけどこめん無理wwwww。
モナドの理解には関わらない以上、たとえ以下の記事の内容がこの記事よりも優れていたとしても以下のリンクには進まないのが賢明な判断というものである。
モナドに対しての関わり方には以下の3通りがある。
モナドを使うのが目的であれば、今からでも遅くはない。ブラウザの戻るボタンを押して使いたいモナドのAPIドキュメントを読んだほうが良い。
モナドなクラスを設計するのが目的であれば、そんなことにこだわるより、便利なクラスになるように心がけた方が良い。必要であればそれは自然にモナドになるし、必要がなければそうはならない。きっとそんなものだと思う。
モナドが何であるかを理解するという問題については、そもそもそれが可能かというところから検討しなければいけないかもしれない。
以下は、他の人の説明を読んで理解に苦しんだものの愚痴か負け惜しみと思って頂いても構わない。
だいたいのモナドの説明は、Haskellなどの関数型プログラミング言語か、圏論の用語と記号で書かれている。Haskellか圏論を勉強しないと説明を読むことすらままならない。
また、圏論の説明に深入りしようとすると、1つの知らない用語を調べると2つ以上の知らない単語に出くわすという泥沼が待ち構えている。
型の情報だけ与えられて、あとは自分で考えろ的な説明も多い。確かに型をみればだいたい挙動の見当がつくが、同じ型だからといって同じ値であるとは限らない。しかも、文脈によって同じ値であったり同じ値でなかったりするので、脳内で補完しながら解説を読まなければならなくなる。わかっている人にはすんなり入っていくのだろうが、どちらかわからない状態で読み進めるのは困難である。
たいていは好きで記号を使っているわけではないんだろうと思うが、特に圏論方面に深入りするうちに独特の記号に慣れてしまい、また、説明内容が高度になるにつれて記号を用いずに言葉だけで表現することに困難を覚えるようになるのかもしれない。まさにミイラとりがミイラ。Haskellや圏論の記号はその内容を最も簡便に記述できるように考えた結果できたもののはずなので、必然といえば必然なのかもしれない。
またHaskellの説明に出てくる関数名returnとbind(>>=)が、感覚的には全然returnでもbindでもないことも、話をわかりにくくしているといえよう。ちなみにこのモナドを考える上では不自然なネーミングでも、Haskellのdo記法ではbindは「値を束縛(代入)する」部分に相当し、returnはdo記法の末尾で、do記法の結果生じるモナドの「値を返す」のに用いられるので、実用上はそちらの方が有利なのである。しかし、モナドそのものを考えるときにはかえって名前に対するおかしな先入観とのギャップに苦しめられることになる。
また、言語仕様とまではいかないが、命名規則も理解を妨げる。Haskellの命名は f とか a とか一文字だけのことが多く、強力な型推論により型情報がほとんど省略され、それが仮引数なのか実引数なのか関数なのか型なのか、言語にかなり精通しないと不正確な理解にとどまってしまう。何が書いてあるか見当がついているのならともかく、初見の説明に出てきた fa と f a の違いを見抜けといわれても無理がある。
さらに抽象化が行き過ぎていて、liftとか名前からでは何を持ち上げるのかさっぱり見当がつかなかったり、しかも同じ関数に別の文脈では違う名前がついていたりする。
圏論方面から入ろうとしても障壁が大きい。まず、圏というもの自体が、集合のような均質な集まりでなく、「対象の集合と射の集合がセットになったもの」という複合体である。世の中で流通している名詞のほとんどは「単一のもの」もしくは「単一の共通した特長を備えたものの集合」を指している。一般人にとって圏のように異質な特長を備えた2つのもの(かつその2つには共通点がない、あるいは共通点があってもその単語の指すところではない)の組み合わせという概念を受け入れるには、これまでの名詞に対する先入観が邪魔をする可能性がある。他の概念も一つの概念の中に複数のものを含んでいることがあり、「整数とは〜」、「実数とは〜」というようなものと同じように受け入れようとすると混乱をきたす。
また、圏論では複数の概念の組み合わせであるのに、Haskellではプログラミング言語の性質上「圏論で言う「複数の概念」が定義できるデータ型」に同じ名前が割り当てられるという少しずれた定義がなされていることがある。しかも、解説する側にとってはそれは自明なことらしく、明示的に書かれていることはまずない。
圏論が抽象的すぎて難しいため、中途半端な理解のもとに書かれて微妙に間違っていたり、途中で力尽きて省略していたりする説明も少なくない。まさにこの記事がそうである。
この記事では、プログラミング言語や圏論の記号を使わずに説明することを目指したいが、おそらく何らかの記号を導入せざるを得ないので大して差は出ないかもしれない。ただ型と高階関数については知っているものとする(関数の引数に関数がくるくらいであるから、引数が実数や複素数とは限らないのはもちろんのことである。)。この2つがわからない人は厳しいことを言うようだが、残念ながら、モナドはもちろん関数型プログラミングに向いていない。しかし非関数型プログラミング言語でもチューリング完全であり、参照透過性が実現できるなら関数型プログラミングの利点と言われているものの多くは享受できると思われるので悲観することはない(ような気がする)。
とりあえず目標としては、モナドがHaskellで参照透過性を保ちながら副作用を実現しているということの意味(Haskellをしない人がモナドを調べようとする動機の多くはこれではないかと勝手に思ったため)を理解可能なものにしたい。と思っていたのが、いつの間にか圏論の泥沼に...。
いろいろな人がいろいろな解説を試みており、モナドとは象である(だれも全貌を把握できない)とか、モナドとはモナドである(モナドと同じ性質を持つものはモナドしかないという意味で、必ずしも説明を放棄しているわけではないと思われる。)といったいかがわしい説明まで出る始末である。
プログラミングにおけるモナドとは、プログラミングにおける一つの定型的手法(デザインパターン)が、Hakellでいうところの型クラス、Scalaでいうところのtrait(英語で特性という意味でJavaの抽象クラスが多重継承可能になったようなもの)、Javaでいうところのインターフェースで提供されるものだといえる。
もうこの時点で、モナドの意味を考えるという行為に疑問を抱かねばならない。デザインパターンに「意味」を求めたりするだろうか。仮に求めているとして、同じ種類の意味をモナドに求めようとしているだろうか。
モナドには種類があり、個々のモナドごとに違った意味を持っている。個々のモナドの意味を通り越して、全てのモナドに共通の意味を求めようとすることが不幸の始まりなのである。
モナドを語るのに高階関数は欠かせない。そしてそれを表現する手段としてラムダ式の表現方法はないとかえって煩雑になる。ラムダ式の書き方はプログラミング言語により多少差があるが、ここでは
f(x) = x2 + x + 1のとき、f = λ x → x2 + x + 1
という表記法をとることにする。
型を意識せざるを得ないのだが、どう表記しよう。ジェネリクスとかタプルとか。
xをインスタンスに持つXに対応するAモナドを
unitA(x): A[X]
関数型は
unitA: X → A[X]
と書くことにする。
まず、モナドとはMaybeモナドやStateモナドなどの総称であり、たとえばMaybeモナドという個々のモナド自体が、Maybeモナドのモナド則を満たすものの総称である。つまりモナドは総称の総称ということになる。
モナド則を満たす一番単純でわかりやすい実装は、上記リンク先でも取り上げられているデータを格納した箱である。実際たいていの実装は何らかの形で内部にモナドの元になるデータを保持している。
前提なのか定義なのかで混乱しやすい。
また、値の定まっていない変数なのか、代入されて値が固定されているのかが字面からは判断しづらいことも混乱に拍車をかけているように思える。
たとえば仮にAモナドというものを考える。
任意の値xに対応する(1対1とは限らない)AモナドAxが存在する。1対1とは限らないので、この記法は問題があるどころかこの記法でついた先入観から脱却するのが困難になるのだがとりあえず目をつぶることにする。
ここで関数unit(x)を考える。Haskellのreturnに相当。モナドごとに異なる関数なので、再代入により変数名を使いまわしたりしない関数型プログラミングの考え方を参考にAモナドのunit()をunitAと書くことにする。
unitA(x) = Ax (Haskellだとretrun x == Ax)という定義になる。
とりあえず、x を A という箱の中に入れるイメージでよい。(モナドにするときに情報が維持される場合以外に、情報が増える場合と減る場合があり、箱モデルではその点については誤解が生じやすい。ちゃんと通用するのはIdentityやMaybe(Option)モナドなど、xとAxに一対一対応がある時くらいで、モナドの種類によってはすぐに限界を露呈し、かえって理解の妨げになる。後で補足する。)
x
unitA()
→
x
= Ax
もう一つflatMap(これもモナドごとに異なる関数である)という高階関数を考える。Haskellの>>=に相当する。
箱モデルはいけないと思いつつも、イメージをつかむ方法が他にないので、箱モデルで説明すると、
f(x) = Ayのとき、
flatMapA(Ax, f) = Ay (HaskellだとAx >>= f == Ay) = unitA(y)
x
mapA(f)
→
f(x) = Ay
=
→
y
flattenA()
→
y
箱の中でxに関数fを適用すると、箱の中に、箱に入ったyが出てくる。いうなればAAyの状態である。flatMapではAAyの状態をAyにする。AAyをAyにすることをflatにする(flatten)と呼ぶ。納得行かなくてもそう呼ぶ慣習になっているので。
map(これもモナドごとに異なる関数(ry)という関数が名前の元になっている。
g(x) = y のとき
mapA(Ax, g) = Ay
Ax(たりうるものの集合)からAy(たりうるものの集合)への写像(map)になっているので、こう呼ぶ。
mapしてflattenするという考え方から、flatMapという関数名で扱う言語が多い(Haskellはそうではない)。
unit()同様とりあえず把握するために箱モデルで説明しているが、このモデルで考えるとかえって混乱するモナドが多いので、あくまでもとりあえずの理解のためと割りきっておくこと。
AAyをAyにできるのだから、Ayをyにすることも出来そうだが、それができることは保証されない。
また説明の都合上map関数を定義したが、map関数が定義できることは保証されていない。flatMapから合成できるとか何とか。
箱モデルだとAxからxを取り出せそうに見えるし、実際取り出せるモナドが多いのだが、モナドの性質としては保証されない。後述の定数モナドを参照のこと。
unit()もflatMap()もそういう関数が「ある」というのではなく、モナド則を満たすようにunit()とflatMap()を「定義する」のである。unit()ともかく箱モデルのflatMap()はunit(x)で得られるモナドには通用するものの、それでは説明できない実装の方が多い。モナド則さえ満たせれば箱モデルの実装である必要は全くない。
モナドという単語で、上記で言うAxのようなデータを指すことも多いが、データ型のみではモナドになることはできない。unit()とflatMap()を組み合わせてはじめてモナドと呼べるのである。
宣言型プログラミングにおいて手続きの順番は指定できない。順番を指定したければ関数適用の順番によってするしかないのである。
モナドは文脈を持った計算という表現をされることが多々ある。
強いて言うならば、モナドにするときに情報が増えたり減ったりするが、情報が増えたこと、減ったことを気にせずに、計算を続けたいという希望を叶えることができる。しかし、値を取り出そうとすると最終的にはその情報の増減には向き合わねばならない。
モナドの闇である。
ABx と BAxは一致しない。
モナド変換子(monad transformer)。
たとえば、List<Optional<T>>とOptional<List<T>>を考えれば分かるように、前者は個々の要素について値がある場合とない場合があるが、後者は要素全てに値があるか、要素が存在しないかの2通りしかない。
解決策として以下のようなものが提唱されているらしいが、普及はしていないようだ。
急上昇ワード改
最終更新:2025/12/06(土) 06:00
最終更新:2025/12/06(土) 06:00
ウォッチリストに追加しました!
すでにウォッチリストに
入っています。
追加に失敗しました。
ほめた!
ほめるを取消しました。
ほめるに失敗しました。
ほめるの取消しに失敗しました。