この記事は
を解説しています。
「30分でわかる」のは、だいたい、
4. モナド(Monad)とは何か?
の読了までを想定しています。
また速い人なら、30分で全部一気に読み通せる分量でもあると思います。
30分以上かかっても一気読みしてしまうことが推奨されますし、一気読みできるように、前に戻って知識の再確認をしなくて済むように、最大限留意して構成を設計した上で執筆されています。
1. モナドが難しい?
巷の解説が混乱に満ちあふれている・・・
1.1. モナドを理解するのが難しい理由
-
数学と用語問題。モナドの理論的基盤として圏論があるのは事実。理論的基盤がしっかりしているのはプログラミングという数学的作業において歓迎すべきことではある一方で、他方そのため一般的なプログラマにとってはまず用語に馴染みがない。歴史的に、圏論ベースのモナドを理論から関数型プログラミングに応用されていく過程では、実際、先駆者の間でさえ紆余曲折があったのだが、学習者へは馴染みのない用語を伴って、いきなり高度な数学的概念全開で天下り的に提示されてしまうことが多い。わかっている人、そもそも実用性以上に数学性、理論的側面に興味がある人にとっては知的好奇心を掻き立てられるトピックではあるが、そうでないプログラマにとっては「難しい、とっつきにくい、学習コストが大きすぎて実用性もよくわからない」となることが多い。「わからないの?ならとりあえず、巷の半端な解説より Philip Wadler先生の数々の素晴らしい論文を読んだほうがいい!」という人もいるが、ほとんどの学習者にとって、そういうアドバイスをされる時点で、このアプローチは絶望的である。そもそも彼らには初学者に向けて噛み砕いて教えるつもりはない。そして、実はモナドを理解するために高度な数学の素養は不要。小中高で習った数学レベルで十分だ。
-
逆に過度に理論面を放棄した解説を読んだ結果、余計にわけわからなくなった問題。 モナド解説に限らず科学分野の一般読者向け解説記事でアルアル。比喩、例示という極めて高度な芸術的作業が不十分なため、一瞬わかったような錯覚にさせられはするが、実際はなにもわかっておらず、その後長期間に渡り理解の不整合に苦しむ羽目になる、という不幸なパターン。読者、特にプログラマは馬鹿ではないので、そういう読者の知性を愚弄する真似は努めて避けるべき。小中高レベルの数学で十分ならばちゃんと説明すればいいだけのこと。それができないというのは、説明者自身が理解していない証拠。
-
Haskellに寄りすぎ問題。歴史的に、圏論のモナドが関数型プログラミングへ応用できることが発見され、論文が発表された際に、使用された言語はHaskellであり、関数型言語としてのHaskellの根源的なフレームワークとして積極的にモナドが導入された。そのためHaskellerにとってはモナドの理解は必須要項であり、情報交換もHaskellのSyntaxをもって活発に行われている。彼らの知識の源泉は主にMonad - Haskell Wikiであったり、Haskell/圏論#モナドであったり、Learn You a Haskell for Great Good!(無料公開中)(有料日本語訳『すごいHaskellたのしく学ぼう!』 )であったりして、ほとんどの場合そのHaskellで一般的な用語、Syntaxで語られる。Haskellerにとっては「モナドとはすでに手元にあるもの」であり、手元あるいは、足場となる言語を活用するための学習モティベーションも極めて高い。裏を返すと、Haskellerでないその他大勢のプログラマにとっては以上の事実は逆風となる。
-
複数の新規概念ごっちゃまぜ問題。モナドが関数型プログラミングに応用される際、学習者にとっては。複数にわたる本来興味深いはずの新規概念があるのだが、それらはほとんどの場合整理されて説明されることはない。たとえば上記のHaskellに寄りすぎ問題により、Haskellの基本的文法とからめて天下り的に
do
とかIO
だ、などとしょっぱなから当たり前のように言われるのだが、これらはモナドを遅延評価、イベント、非同期プログラミング、IO/状態(State)、FRPの概念と合わせて応用する話であり、モナドの概念導入段階では本来すべき話ではない。事実モナドの関数型プログラミングへの応用黎明期では、モナドによって入出力(IO)が扱える、とPhilip Wadler先生たちから提案されたのはちょっと後になってからだ。聡明な専門家の間でさえそんな感じだったのだから、IO、それから状態管理への展開はこれはこれでひとつの一大発明であって、モナドの応用シーンとして、面白い別トピックとしてわけて考えたほうがいい。しかし、「モナドが一体何に役立つのか?」という強い要請のために「ほらHaskellではIOやdoで使われてるよ」と言いたい事情もわかる。これはHaskellに寄りすぎ問題の弊害でもある。
これはFRPの先駆者であるConal Elliott先生もStackOverflowのモナドの何がそんなに特別なのか?への回答として、似たようなことを主張している。
(Haskellの)Monadタイプクラスへの不釣り合いなまでの大注目度合いは歴史的な幸運にすぎない。彼らはよくIOとモナドを関連づけるが、この2つは独立した有用な概念だ。(関数型プログラミングでの)IOはマジカルで、モナドはそのIOとしょっちゅう関連づけられているので、モナドがマジカルだという錯覚に陥りやすい。
2. JavaScriptプログラマのためのモナド入門
これは、一般的なJavaScriptプログラマのためのモナド入門記事です。
2.1. 対象とする読者
関数型プログラミングをしたいJavaScriptプログラマーでモナドを理解したい人。
義務教育レベルの数学を理解していることが望ましい。
モナドを知りたいと思ってWikipediaやWeb上の解説記事などを漁ってみたが、やっぱりさっぱりわからずに挫折していたところ、たまたまこの記事にたどり着いた人。
関数型プログラミングについて入門したい人は、
当ブログの入門記事
|
とりあえず配列とMapがわかればいいです。
2.2. 本稿のアプローチ
モナドを理解するのが難しい理由をアンチパターンとして最大限留意しています。
3. なぜモナドか?
JavaScript上でかなり実用的だから。
上述のとおり、モナドとは関数型プログラミングの一部です。
関数型プログラミングは、プログラミングの複雑性を、以下の2つ
-
値
-
値でもある関数
の組み合わせ(function composition)で制御します。 代数学と関数型プログラミングとオブジェクト指向の用語・記法の相互関係 以降で詳しく解説します。
3.1. jQuery
いろんな値&関数が考えられるわけですが、JavaScript世界で超有名なのが、 jQueryでしょう。jQueryのオフィシャルロゴには "write less, do more" とあり、それまで不十分なAPIにより煩雑だったDOM操作を簡潔な記法で柔軟に操作できる値&関数を提供し、その実用性の高さから人気を集めました。
"write less, do more" とは、複雑なプログラムをなるだけシンプルに取り扱おうとする関数型プログラミングの唯一にして究極のゴールの具現化そのものであり、一例をあげてみると、
$("#p1")
.css("color", "red")
.slideUp(2000)
.slideDown(2000);
と、メソッドチェーンをもって書き連ねるだけで、Demo:こんなことができるようになるとか、当時JavaScriptコミュニティに衝撃を与えました。要するに、この "write less, do more" こそが、関数型プログラミングの真価であり、jQueryはただひとつの、 $()
というjQueryオブジェクト生成関数と、それにぶら下がる巨大なメソッド群から成立していて使い方自体はシンプルです。
jQueryは値(オブジェクト)&関数(オブジェクトにぶらさがるメソッド群)のペアです。
jQueryがモナドかどうか?というのはしばしば議論にあがるところですが、jQueryのAPIは巨大なので、その全部がモナドであるわけではないが、そのうちの一部はモナドになっている、というのが答えでしょう。
jQueryの一部の特性としてモナドの性質を備えている理由はメソッドチェーンを壊さないためです。
全部がモナドではないが一部は確実にモナドである、という別の事例として、最近のJavaScriptのArrayがあげられます。これについては次の章で。
3.2. MonadicReact
jQueryは標準DOMのAPIがかなりマシになってきたこととでパフォーマンスの観点からも、jQuery非依存で書こうというトレンドが見られます。さらに仮想DOMのコンポーネント機構をもつReactが登場したことにより、世代交代が起こった感もあります。
Reactをより関数型プログラミングで、という目的でいろんなライブラリがありますが、
みたいなReactのモナドラッパーがあります。
Ph.Dを持つ作者が、Medium記事:Type-safe monads and Reactで Yet another introduction to monadsとモナドの紹介をしながら「便利でパワフルだ」みたいなことをエンドユーザに向けて書いてますが、とりあえず何が書かれているのかさっぱり理解できない!という人は、でもやっぱり理解したい、となるでしょう。
3.3. Promise
ES6+ 以降で導入された Promiseも一部モナドっぽいふるまいをします。モナドだと言う人もいますが、モナドではありません。PromiseはjQueryほど巨大なAPIではないので、すべて厳密にモナドであったほうが有用性はあがるはずですが、そうではないので残念です。
Promiseはすでに、ESModule
参考記事
|
の動的Importの返り値として標準化されるなど、今どきのJavaScriptプログラマにとっては必須事項となってしまいました。Promiseが「モナドっぽい」振る舞いをするが、そうでない振る舞いするときもある、と挙動を把握しておくこと、人に説明できるほど理解しておくことは、Promiseを正しく使いこなすためにも重要だと思います。
3.4. Fluture
FantasyLand compliant (monadic) alternative to Promises
Much like Promises, Futures represent the value arising from the success or failure of an asynchronous operation (I/O). Though unlike Promises, Futures are lazy and adhere to the monadic interface.
Promises(ES6+ Promise含む)のオルタナティブ。
npmのデータでは、それなりのパッケージから依存され、それなりのダウンロード数も誇るようです。
Promisesと違ってモナドインターフェイス(monadic interface)になっているよ、と書かれています。
何が違うのか、どんなのメリットがあるのか?そもそもモナド理解してないと意味不明ですよね?
3.5. まとめ
今どきのJavaScriptプログラマならば、モナドくらいは知っておいたほうが良さそうだ。
4. モナド(Monad)とは何か?
Haskellerにとっては「モナドとはすでに手元にあるもの」であり、手元あるいは、足場となる言語を活用するための学習モティベーションも極めて高い。裏を返すと、Haskellerでないその他大勢のプログラマにとっては以上の事実は逆風となる。 モナドを理解するのが難しい理由
さらに裏を返すと、JavaScriptプログラマにとっては、JavaScriptですでにモナドが実装されていて、使えればそれなりの恩恵にあずかることがすぐできる、となれば、テンションもあがるんじゃないでしょうか?
「全部がモナドではないが一部は確実にモナドである」ってどういう意味?っていうのもここでわかります。
全部がモナドではないが一部は確実にモナドである、という事例として、最近のJavaScriptのArrayがあげられます。
JavaScript の
Array
オブジェクトは、配列を構築するためのグローバルオブジェクトで、配列とは複数の要素の集合を格納管理するリスト構造です。
モナドを紹介するにあたって、Array
が優れているのは、
-
すでに手元にある。すぐに触れる。最新のモダンブラウザやNode.jsならすでに実装済みだ。得体のしれない誰かのモナド実装コードを解読する必要なし。
-
馴染み深い。誰でも知ってる。みんな使える。基本的API。かんたん。
-
見える。コンソール出力したときの値はそのまま値の構造を表している。どうなっているのか一目でわかるので理解も容易。
と、まさに早い安いうまいの三拍子揃っています。
まずは、Array
のモナドではない部分を復習して、それからモナドである部分を紹介します。
4.1. Array.map
Array.mapのことは、JavaScriptプログラマなら誰でもよく知っているでしょう。
配列の構造(リスト構造)を保ったまま、値にある関数を適用した結果の値を返す、というメソッド(オブジェクトに紐付いた関数)です。
[1, 2, 3, 4, 5]
に、
値を2倍する関数
a ⇒ a * 2
を .map
すると、
[2, 4, 6, 8, 10]
が返ってきます。
const array1 = [1, 2, 3, 4, 5];
const array2 =
array1
.map(a => a * 2);
console.log(array2);
[ 2, 4, 6, 8, 10 ]
ここでポイントは、配列の Array.map
前と後で
-
構造を保ったまま、要素を1:1で転写(map)する
-
自分自身=
Array
オブジェクトを返してくる
ことです。
このような性質のメソッドを持つオブジェクトのことを、圏論の用語では
endofunctor(自己関手)と呼びます。
Functor ?? — Haskellに寄りすぎ問題、再度勃発!
圏論で、functor(関手)の定義は、任意の2つのオブジェクト間の転写(map)をするオブジェクトなので、たとえば、 Array(配列)→Object(オブジェクト)とすると、
でも構わないでしょう。 転写(map)先を自分自身である
Why is functor in Haskell defined like the endofunctor from category theory?(なぜ、Haskellのfunctorは、圏論のendofunctorみたいな定義になってるの?)という極めてまっとうな疑問が出て、"Convenience and history." 便利さと歴史的経緯のせいだ。(中略) endofunctorというタイプクラス(型クラス・type class)名よりも、Functorという名前のほうが" much nicer name"だ。正確じゃないかもしれないが機能している・・・ ちなみに、Learn You a Haskell for Great Good!(無料公開中)(有料日本語訳『すごいHaskellたのしく学ぼう!』)にも、endofuntorのことを意味しながらも単にfunctorとだけ書かれています。どうもHaskellコミュニティでは、一部の(わかってる)人達は「タイプクラスの命名のノリのことだから些細だ」という暗黙の了解としながらも、大勢はタイプクラスの名前と圏論用語がごっちゃになったまま伝播し続けているようです。実際に、圏論のFunctorとプログラミングのFunctorでは【意味】が違うとまで言い切っている人も見てきているので、今からでも遅くないのでちゃんと直したらどうか?と思うわけです。厳密に定義づけされた圏論用語ぽいので真に受けて聞いていたら、後で「意味が違う」とか「え?ちょっと待て」と思いますよね。いちいち言葉の定義レベルで話が通じなくなるので困るし、大事なことなのでここでちゃんと書いておきます。 プログラマ界隈では、ReferenceTransparency(参照透過性)にしろ、もともとの用語の意味が完全に損なわれた不正確な意味で用語を天下り的に教えられて、その不正確な意味を知っていて当然、のようなことが横行しているので要注意。 |
Array.map
メソッドは自分自身= Array
オブジェクトを返してくる、というendofunctor(自己関手)の特性の良さにより、メソッドチェーンが可能です。
const array2 =
array1
.map(a => a * 2)
.map(a => a + 1);
console.log(array2);
[ 3, 5, 7, 9, 11 ]
Array.map
のメソッドチェーンでは、まるでパイプラインの中を Array
オブジェクトがずっと流れているようで、エコの統一性が保証されています。
jQueryが便利だ、というのも、モナドどうこう言う以前に、ほぼほぼこのendofunctor(自己関手)がもつ関数型的特性とメソッドチェーンのメリットが大きいです。
入れ子構造
ただし、構造を保ったまま、といえども、渡す関数を、
Console
と、各要素の階層を追加することは可能です。 |
4.2. Array.mapと関数型プログラミングの限界
そんなにendofunctor(自己関手)の性質が良いのならば、モナドの立場は??モナドの意味は?何が良いの、違うの?となるわけですが、ここの差分をきっちり理解しておくことが重要です。
const array2 =
array1
.map(a => a * 2)
.map(a => a + 1);
という一連のシークエンスを再利用可能とするために関数化します。
const f = array =>
array
.map(a => a * 2)
.map(a => a + 1);
関数を利用します。
const array1 = [1, 2, 3, 4, 5];
const array2 = f(array1); (1)
console.log(array2);
1 | f 関数の利用 |
[ 3, 5, 7, 9, 11 ]
想定通りの振る舞いで何の問題もありません。
ただし、これまで、Array
操作は、.map
のメソッドチェーンで実現していたのに、f(array1)
とSyntaxが変わったことが気になります。
Array.map
のメソッドチェーンでは、まるでパイプラインの中をArray
オブジェクトがずっと流れているようで、エコの統一性が保証されています。
という観点からは。Array.map
のメソッドチェーンを再利用するための関数 f
を定義したはいいが、この関数を利用するときは、そのメソッドチェーン(パイプライン)の外でやっているので、本当にこの Array
エコに合致するのか?その保証がほしいです。
ひとつの方法としては、TypeScriptを使って、定義した関数の入力値/出力値の両方に Array
の型付けをして、TypeScriptトランスパイラにチェックさせる方法があり、これは当然推奨されます。
しかしそれでもなお Array.map(f)
のメソッドチェーンから飛び出して、f(array1)
とSyntaxが変わったエコの不整合さは解消されません。
適用したい関数 f
が先きてかっこでくくるのが普通の関数適用、メソッドチェーンでは尻尾に f
つけていますね。ここは結構重要で、メソッドチェーンのエレガントさは、チェーンの後に、また中間でも、追加、挿入自由自在なところにあります。
たとえば、複数回連続して、f
適用したい場合、
f(f(array))
はネストが深くなっていき、可読性も悪く「なんとか地獄」の様相なので
Array.map(f).map(f)
と連鎖で平らに書けたほうが良いですよね?
ピンと来た人はご名答
ES6+Promiseで、「コールバック地獄」から開放される、とか言ってるのも、まさにこの話に対応しています。 |
f
というのは、そもそもメソッドチェーンの再利用関数だったので、それを再度、メソッドチェーンの中で使うっていうことなので、メソッドチェーンのネスト・入れ子構造って可能なの?ってお話をしています。
ネスト・入れ子構造っていうのは、関数型プログラミングのお家芸というか、自由自在になんでも組み合わせができてなんぼの関数型プログラミングです。今、関数型プログラミングの限界を試しているところです。我々はどこまで行けるのか?
Array.map
のメソッドチェーンでいけるかどうか?ダメ元で試してみましょうか。
const array1 = [1, 2, 3, 4, 5];
const array2 =
array1.map(f); (1)
console.log(array2);
1 | Array.map(f) のダメ元チャレンジ |
TypeError: array.map is not a function
TypeError つまり型が合いませんでした。
じゃあ、.map
元がとりあえず Array
にだけなるよう []
でくくって再チャレンジ。
const array1 = [1, 2, 3, 4, 5];
const array2 =
[array1].map(f); (1)
console.log(array2);
1 | [] でくくって [array1] とする |
[ [ 3, 5, 7, 9, 11 ] ]
いちおう通って Array
が出てきました!しかし、残念ながら期待していた [ 3, 5, 7, 9, 11 ]
とはならず、ネストした二重の Array
になってしまっています。
もうにっちもさっちもいかないので、ここが Array.map
の関数型プログラミングでの限界です。
Array.map
は、自分自身= Array
を返すというendofunctor(自己関手)の特性があり、メソッドチェーンが出来るのだが、メソッドチェーンが入れ子構造になると、自身の構造をコントロールできなくなる のです。
関数型プログラミングにとって、これは結構な大問題だとは思いませんか?
4.3. Array.flat の登場
ネストした二重の Array
を 平坦化するには、その機能をもった Array
メソッドが必要になってきます。
モダンブラウザでは、Chrome69/Firefox62などメジャーどころは、ごく最近、2018年9月に入って立て続けに、 Array.flatを実装しました。
Node.jsの最新版でも実装されています。正確なNodeバージョンまでは調査していない。以前までは、これ使いたくても、Polyfillなど使って自前でなんとか拡張する必要があって面倒だったのですが、未だ実験的実装とはいえ歓迎すべきことです。
Array.flat
メソッドは、その名の通り、ネストした配列構造をフラット化します。
const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]
パラメータを指定することで、フラット化するネストの階層を指定できますが、デフォルトでは 1
で、1階層だけフラット化します。それ以上再帰的に追求しません。そして、この1階層だけフラット化するというデフォルトの挙動が本トピックでは適切な振る舞いなので、そのままにしておきましょう。
4.4. unit の定義
JavaScriptは、裸の値を Array
にしたり、すでにある配列・要素をさらにネストしたいとは、各々の値を []
でくくればよいだけなので直感的で良いですが、これはれっきとした、値の変換なので、今後のためにちゃんと関数としておきましょう。
unit = a ⇒ [a]
と定義しておきます。
const unit = a => [a];
console.log(
unit(7)
);
console.log(
unit([7])
);
[ 7 ]
[ [ 7 ] ]
特に問題ないですね?
4.5. unit と Array.flat の対称性
なんでわざわざ unit
を定義したのか?というと、以下の話をしたいからです。
unit
と flat
を図式化するとこうなります。
どちらも、関数の出力値は、Array
一択 です。ここ重要。
まあ、対称性があるように見えて単純で美しい構造だと思うのですが、これは何気に奥深くて、まるで論理クイズみたいな様相を呈します。
-
unit
とflat
を眺めると、どうも双方は明らかな対称性があるようだ。 -
双方の関数の出力値は、
Array
一択という強い縛りが効いている。 -
ならば、双方は対称にはなりえない。
意味わかります?
この界隈では、「コンテナに入れる」「箱に入れる」「箱から出す」「ラップする」「一枚皮を剥く」「カラに入れる」「カラから出す」はたまた「純粋にする」とか「リフト(アップ)」するとかいろんな言い草がありますが、ここでは単純に「階層」の上下関係で上げる、下げると言いましょう。
ここでの絶対的ルールは以下の2つだけです。
-
unit
は1階層だけ上げる。(さっき実際そのとおり定義した) -
flat
はネストしていれば1階層だけ下げる。
ルール2で flat
のネストしていればと、しれっと条件分岐をしている部分が、無条件に1階層上げるという unit
と非対称です。
たとえば、
console.log(
[[7]].flat() (1)
);
console.log(
[7].flat() (2)
);
1 | ネストしてる |
2 | ネストしてない |
[ 7 ] (1)
[ 7 ] (2)
1 | ネストしてたので1階層下げた Array |
2 | ネストしてなかったので、そのままの Array |
Array.flat
は、もし Array
がネストしてたら、1階層下げて Array
を返しますが、ネストしていなかったらそのままの Array
を返します。最後の配列の皮を剥いで、裸の値 7
を返すようなことはありません。
つまり、Array.flat
の返り値は必ず Array
タイプである、中の値を裸では提供はしません、という基底が保証されています。
Array.map
はendofunctorで、返り値は必ず Array
タイプである、という例のメソッドチェーンのエコの部品としてドハマりしますよね?
Array.flat
の仕様あるいは、flat
という共通概念の特性は、 f関数の利用@map ダメ元チャレンジの結果、裸の値に .map
してしまいタイプエラーが出るような不整合を未然に防止してくれそうです。
flat
しても基底で止まるように条件分岐でしっかり保証!されたところで、あとは、unit
と flat
の上下移動の対称性をもって、どの階層にも自由に移動させながら、Array.map
メソッドが使えるようになる・・・はずです。
こうしてみると、unit
と flat
は概ね対称的ペアだけど非対称だ、というのがよりはっきりわかると思います。
また、エコが破綻する裸の値はまずいですが、ネストした構造が別に悪いわけではありません。ネストした Array
を扱いたいのならば、そのネスト構造を扱うことも含め自由にコントロールしながら、Array.map
することができる・・・はずです。
要するに、Array.map
こいつ単独ではどうも役不足だ。特にメソッドチェーンでネストしたら途端に構造が破綻するので扱いづらくてかなわん・・・ここはひとつ、構造に直接アプローチできる、対になった unit
と flat
ペアを導入してやって、なおかつ、flat
が裸の値を返さないような安全装置つきなら、言うことないだろう・・・そういう理屈(皮算用)が今進行しているわけです。
ああ、紹介が遅れましたが、今話しているこれがモナドです。
世の常として結果論ですが、結果的にこの理屈はうまく機能します。
じゃあ実際どうやって上手く機能するんだ?ってことになるわけですが、ポイントは、モナドっていうのは、関数型プログラマコミュニティ(Haskell)がもてはやす前から、圏論で定義される数学的構造として存在していて、それをどうやって上手く使うのか?っていうのは、また別の話なんですね。だから、特にモナドの紹介をするときにIOだのピュアだの言うのは、完全にお門違いです。
Arrayが自身の構造にアプローチできるモナドになった結果、実際いかに便利になりうるか?というのは、次の章から説明します。
4.6. モナド(Monad)
なんのことはない、Array
で言えば、普通の Array.map
に Array.flat
を付け加えたものがモナドになります。 unit
というのは、[]
なので最初からあるといえばありました。
自身の構造をコントロールしながらマップするためには、
-
自分自身のオブジェクト
Array
を返すArray.map
がベースとしてある endofunctor (Array
オブジェクト) -
1階層上げる
unit
-
(もしネストしていたら)1階層下げる
Array.flat
この 3点全部そろったら Array
は、Arrayモナド(Monad)になります。
念の為に読者へ保証しておきますが、これは、圏論でちゃんと定義づけされているモナド(Monad)のことです。プログラミングのモナドで定義が異なる、という例のトリッキーなアレではありません。
圏論(category theory)用語の紹介
英語版Wikipediaなどでは、 Monad (category theory)
圏論(category theory)では、モナド(monad)とは、自己関手(endofunctor=カテゴリを自身に転写するfunctor)で、2つの自然変換(natural transformations)を伴っている。 などと書かれていますが、圏論用語@日本語では、 \(C\) 上のモナドとは、
からなる3つ組 \(\langle T, \eta, \mu \rangle\) などと表記されることが多いです。 逐一、英語 category theroryのことを圏論、endofunctorのことを自己関手、natural transformationを自然変換と和訳してしまった結果、原語以上に難解さを醸し出す効果を持っており、なおかつギリシャ文字が出てきておっと思うわけですが、どう表記されようが、
に過ぎないし、それを念頭に式を眺めれば、この\(T\)ってのは \(C\) 上のモナドとは、とか言ってて、
\[T: C \rightarrow C\]
と \(T\) ( ここでなんの領域を限定しているのか?というと、今定義してるモナドの範囲限定していて、今の 「JavaScriptの値の全体」とはもっと正確にいうと、
次に
\[\eta: Id_C \Rightarrow T\]
\(C\)上にあるなんかの値(裸の値、それからArray自身も含む)を、\(T\)( 最後に、
\[\mu: T \circ T \Rightarrow T\]
\(T\)がもし二重にネストしてたら 一つ階層を下げて \(T\) にして返すという、条件分岐つきの性質を端的に定義しています。
|
4.7. まとめ
圏論のモナド(monad)の定義をまとめると
-
ベースとして、オブジェクト自身を返す
map
メソッドを持つendofunctorとしての特性をもつオブジェクトで、さらに以下の2つの関数(メソッド)がある -
unit
-
flat
この3つ組(トリプル)
をモナドと呼びます。
3つ組(トリプル)、オブジェクト、関数、メソッドという言葉遣い、きちんとした意味、さらに、bicategoryのことなどは、6章 代数学と関数型プログラミングとオブジェクト指向の用語・記法の相互関係 以降で詳しく解説します。
5. リストモナド(List Monad)のつかいかた
モナドの超基本的概念とそれに伴う定義付けは終わったので、あとはそれをどう使うか?です。
Array.map
に Array.flat
追加してモナドになった Array
は、特に リストモナド(List Monad)と呼ばれます。
復習しておくと、そもそも、わざわざArrayをモナドにした動機とは、endofunctorだけだと、
もうにっちもさっちもいかないので、ここが
Array.map
の関数型プログラミングでの限界です。Array.map
は、自分自身=Array
を返すというEndofunctor(自己関手)の特性があり、メソッドチェーンが出来るのだが、メソッドチェーンが入れ子構造になると、自身の構造をコントロールできなくなる のです。
という、限界突破の目的でした。構造をコントロールできるようになりたい。
あと、メソッドチェーンを入れ子構造になっても統一的に扱いたいという話の流れで、仮に構造コントロールできるようになったとして、そのトレードオフとして別のなにかが出来なくなると、同じ局面でendofunctorとモナドの使い分けが必要ということになってしまいます。こうなるとまた収集がつかなくなるのは目に見えているので、トレードオフは受け入れられません。
モナドは、endofunctorの完全な上位互換であってくれないと使い物にはならない、ということです。上位互換を目指します。
endofunctorとの互換性を担保するだけならば、理屈は簡単で、 unit
と flat
が対称なので、行って来いで、効果を相殺すれば済むことです。その上、flat
はモナドオブジェクト自身の基底にヒットすると、それ以上階層を下げて裸の値を返すことはない安心保証の性質があるので、それだけでも上位互換となるはずです。
実際にこの理論で上手く行くのか?やってみなきゃわからない。やってみよう。
まず、たたき台となる、普通の Array.map
だけのパターン
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.map(a => a * 2);
console.log(array2);
[ 2, 4, 6, 8, 10 ]
想定どおりの挙動です。上位互換となるモナドでも、まったく同じことが出来なければいけません。
const unit = a => [a];
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.map(a => unit(a * 2)) (1)
.flat(); (2)
console.log(array2);
1 | a ⇒ a * 2 の代わりに、a ⇒ unit(a * 2) |
2 | flat で相殺 |
[ 2, 4, 6, 8, 10 ]
できました。想定した挙動になっています。
さて、重要なポイントとして、unit
と flat
で相殺するには単純に考えると複数のパターンが考えられるはずですが、順番として、なぜ、 unit
⇒ flat
となっているのでしょうか?
理由:
-
最後に
flat
することで、裸の値でないモナドオブジェクトを返すことを保証できる -
あらじめ
unit
で構造を自由に指定した上で、flat
できる
1,2により、相殺して互換性を保つ以上の上位機能が得られます。 1についてはこれ以上説明は不要でしょうが、2については今から説明していきます。
unit(a) = [a]
同じ意味ですが、あきらかに可読性と構造の直感的把握がしやすいのは、 特にネストした構造になると、
すでに、 |
メソッドチェーンではどうでしょうか?
まず、たたき台となる、普通の Array.map
だけのパターン
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.map(a => a * 2)
.map(a => a + 1);
console.log(array2);
[ 3, 5, 7, 9, 11 ]
想定どおりの挙動です。上位互換となるモナドでも、まったく同じことが出来なければいけません。
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.map(a => [a * 2]).flat()
.map(a => [a + 1]).flat();
console.log(array2);
[ 3, 5, 7, 9, 11 ]
問題なく出来ました。
5.1. リストモナドでリスト構造をコントロールする
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.map(a => [a, a]) (1)
.flat(); (2)
console.log(array2);
1 | a ⇒ [a, a] 返り値としてリスト構造を規定する |
2 | [ [ 1, 1 ], [ 2, 2 ], [ 3, 3 ], [ 4, 4 ], [ 5, 5 ] ] を flat |
[ 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 ]
[ [ 1, 1 ], [ 2, 2 ], [ 3, 3 ], [ 4, 4 ], [ 5, 5 ] ]
という構造が欲しいので .mapと同じ結果を寄越せconst array1 = [1, 2, 3, 4, 5];
const array2 = array1
.map(a => [[a, a]]) (1)
.flat(); (2)
console.log(array2);
1 | a ⇒ [ [a, a] ] 返り値としてリスト構造を規定する |
2 | [ [ [ 1, 1 ] ],
[ [ 2, 2 ] ],
[ [ 3, 3 ] ],
[ [ 4, 4 ] ],
[ [ 5, 5 ] ] ] を flat |
[ [ 1, 1 ], [ 2, 2 ], [ 3, 3 ], [ 4, 4 ], [ 5, 5 ] ]
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.map(a =>
a % 2 === 1 (1)
? [a] (2)
: [] (3)
).flat(); (4)
console.log(array2);
1 | 配列要素 a を 2 で割って余りが 1 なら奇数 |
2 | 奇数なら、そのままの構造 [a] で返す |
3 | 奇数でなかったら、構造を削除したいので、[] を返す |
4 | [ [1], [], [3], [], [5] ] を flat して [ 1, 3, 5 ] |
[ 1, 3, 5 ]
5.2. Array.flatMapの登場
Array.map(f).flat()
となるモナドメソッドはendofunctorの上位互換として機能することが確認出来ました。もうこの確定したパターンでは、逐一尻尾に .flat()
くっつけて回るのは、付け忘れる可能性だってある、スマートではないし、見通しも悪く、バグの温床にもなりかねません。
そこで、もうこの2つの関数を合成してしまって、ひとつの関数として使い回せたほうが便利ですね。それが関数型プログラミングです。
もちろん合成された関数が Array
のメソッドとして実装されていないとまた自前でプロトタイプ拡張とかする羽目になって面倒ですが・・・
ということで、あります。
flatMap() メソッドは、最初にマッピング関数を使用してそれぞれの要素をマップした後、結果を新しい配列内にフラット化します。これは深さ 1 の flatten が続く map と同じですが、flatMap はしばしば有用であり、2 つのメソッドを 1 つにマージするよりもやや効果的です。
Array.flatMap
は 最終的に Array.flat
する Array.map
という合成関数です。
Array.flatMap
はもちろんモナドのメソッドです。
Array
以外のモナドで、既存のものにせよ、自前で何か実装するにせよ、endofunctor の map
に flat
合成するというパターンはもう決まりきっているので、多くのモナドの実装では、flat
は独立した関数として分離しておらず、flat
は、オブジェクト構造の平坦化 \(TTX \rightarrow TX\) という機能として、 flatMap
メソッド(概念として。名前は自由。)のコードに組み入れられて渾然一体となっているケースが多いと思います。
よく見ると、Array.flat
の実装状況と同じで、Array.flat
と Array.flatMap
はふたつセットで各ブラウザへ実装されたっぽいことが推察されます。
Array.map+ flat chain のコードは Array.flatMap
を使って書き換えられます。
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.flatMap(a => [a * 2])
.flatMap(a => [a + 1]);
console.log(array2);
[ 3, 5, 7, 9, 11 ]
5.3. Array.flatMapとモナド関数
Array.flatMap
メソッドの成り立ち、仕組みについて、我々はすでに熟知しているはずなので、あとはどう使いこなすか?です。
APIの仕様の天下りではなくて、数学的な特性から自然と振る舞いはわかるはずだし、使い方も見えてくるはずです。
まずベースは、Array.map
でこの機能は含まれています。
次に、Array.flat
を合成したので、この機能も含まれています。これにより、要素の増減がコントロールできるようになりました。
さらに Array.flat
は、空集合(配列)の []
は要素を削除してしまうので、場合分けすることで、Array.filter
の機能もあります。
Array.flatMap
メソッドをうまく使いこなすことさえできれば、Array.map
Array.flat
Array.filter
が不要になるばかりでなく、これ1つで、なんでもできて、統一的な視点が手に入るはずで、えーっとたしか Array.filter
っていうAPIがあったな、どういう仕様だったかな?・・・とか、この要素を削除したいがどうすればわからない、とか、ここの []
取ってフラットにしたいんだけど、どのAPI使えばいいのかな?とかGoogle検索しながら頭を悩ませる労力から開放される・・・はずです。
Array.flatMap
メソッドの挙動を司るのが、引数として渡す関数です。したがって、Array.flatMap
メソッドを使いこなすとは、この関数を使いこなすことに他なりません。
この関数のことを、理由は後で補足するとして、複数の理由から モナド関数(monadic functions)と呼ぶことにしましょう。とりあえずひとつの理由は、モナドメソッドである Array.flatMap
の挙動を司るからです。
モナド関数だけ設計すれば、なんでもできる。 モナド関数だけ見れば、何やってるのかわかる。そうなるはずなので、ここではモナド関数を研究する必要があるでしょう。
5.4. モナド関数の動作確認
まず基本的な動作確認をします。
-
Array.map
の互換 同じ階層にマップする
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.flatMap(a => [a * 2]); (1)
console.log(array2);
1 | a ⇒ [a * 2] モナド関数 flat と相殺するために返り値に [] をつけてモナド関数の中では階層をひとつ上げている |
[ 2, 4, 6, 8, 10 ]
-
Array.map
の互換 階層をひとつ上げる
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.flatMap(a => [[a * 2]]); (1)
console.log(array2);
1 | a ⇒ [[a * 2]] モナド関数 階層を1つ上げたいときは、[] を二重にする |
[ [ 2 ], [ 4 ], [ 6 ], [ 8 ], [ 10 ] ]
-
Array.map
にない機能 階層をひとつ下げる
const array1 = [[1], [2], [3], [4], [5]];
const array2 = array1
.flatMap(a => a) (1)
.flatMap(a => [a * 2]); (2)
console.log(array2);
1 | a ⇒ a モナド関数 階層を1つ下げたいときは、[] なしのままで |
2 | a ⇒ [a * 2] モナド関数 |
[ 2, 4, 6, 8, 10 ]
ES6以降の分割代入 (Destructuring assignment)を利用して、
とする手法もありえますが、煩雑に見えるし、手法に統一性がないので、ここでは採用しません。 また、分割代入を利用すれば、 |
-
Array.map
にない機能 要素を増やす
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.flatMap(a => [a, a * 2]); (1)
console.log(array2);
1 | a ⇒ [a, a * 2] モナド関数 要素を増やすときは、[] 内で要素を増やす |
[ 1, 2, 2, 4, 3, 6, 4, 8, 5, 10 ]
-
Array.map
にない機能 要素を削除
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.flatMap(a => []); (1)
console.log(array2);
1 | a ⇒ [] モナド関数 要素を削除するときは、[] 空配列を返す |
[ ]
5.5. モナド関数は必ずモナドを返す
以上のモナド関数の動作確認から、モナド関数は必ずモナドを返している、ということがわかります。
階層を1つ下げたいときは、モナド関数の返り値は、
[]
なしのままで
a ⇒ a
だった!?
と思うかもしれませんが、 元の操作対象となる Array
(モナド)は
[ [1], [2], [3], [4], [5] ]
でこのときの入力値 a
はその Array
の各要素で、たとえば [1]
という Array
なので、返り値となる a
も同様に Array
(モナド)です。
モナド関数は必ずモナドを返すというのが、モナド関数と呼ぶもう一つの理由です。
裏を返せば、モナド関数は、モナドさえ返せばなんであっても構わないでしょう。
モナドの構成要素となっている関数はすべてモナドを返す
モナドの構成要素となっているこれら3つの関数は、3つとも必ずモナドを返す関数であることに注目してください。 |
Array.flatMapは必ずしもモナド関数を要請しない
たとえば、
Array.flatMap
Console
タイプエラーが出ることもなく、
言い換えると、 |
5.6. モナド関数を設計する
モナド関数は、モナドを返せばなんでも自由だという事が判明したので、モナド関数を自由に設計してみます。
まず手始めに、もっとも単純な、何もせずに自分自身を返すモナド関数を作ります。
そして、だいたいモナド関数の感じもつかめてきたので、Array
に限定しないモナドでも通用しやすい unit
表記に戻してやります。
const unit = a => [a]; (1)
const identity = a => unit(a); (2)
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.flatMap(identity); (3)
console.log(array2);
1 | unit の定義 Array モナドで1階層上げる |
2 | a ⇒ unit(a) というモナド関数 a ⇒ [a] と等価 自分自身を変化させずに返す |
3 | identity モナド関数で flatMap |
[ 1, 2, 3, 4, 5 ]
const plus9 = a => unit(a + 9); (1)
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.flatMap(plus9); (2)
console.log(array2);
1 | 9を足すモナド関数 plus9 a ⇒ unit(a + 9) |
2 | plus9 モナド関数で flatMap |
[ 10, 11, 12, 13, 14 ]
const oddFilter = a => (1)
a % 2 === 1 (2)
? unit(a) (3)
: []; (4)
const array1 = [1, 2, 3, 4, 5];
const array2 = array1
.flatMap(oddFilter); (5)
console.log(array2);
1 | モナド a が奇数ならそのまま返し、奇数でなければ、空のモナド [] を返す oddFilter というモナド関数 |
2 | 2 で割って余りが 1 ならば |
3 | 奇数なので、unit(a) つまり、要素 a 自身 をモナド値として返す |
4 | 奇数でないので、[] 空のモナド値を返し、要素 a を削除 |
5 | oddFilter モナド関数で flatMap |
[ 1, 3, 5 ]
自分自身を削除するモナド関数
一般的にリストモナド関数 モナドが自分自身の構造をコントロールできる、という意味が実感できるでしょうか? 普通の モナドは、どういったタイプのモナドでも、こういった特異点というか、数字のゼロに対応するような特異なケースでかなりよく振る舞う性質を備えているようです。 たとえば、エラーを特異な値として持つと大きなメリットがあるなど。 Javaで悪名高い頻発するnull pointer exceptionは、このような値がないときの振る舞いを設計の段階で上手く規定できていないことが根本的原因ですが、モナドを積極的に取り入れることで問題の多くは解決するんじゃないでしょうか? また著者が最近書いたFRPオブジェクトをモナドになるように設計していると、値が |
const array1 = [1, 2, 3, 4, 5];
const array3 = array1
.flatMap(plus9) (1)
.flatMap(oddFilter); (2)
console.log(array3);
1 | plus9 モナド関数で flatMap |
2 | oddFilter モナド関数で flatMap |
[ 11, 13 ]
const plus9oddFilter = a => (1)
unit(a) (2)
.flatMap(plus9) (3)
.flatMap(oddFilter); (4)
const array1 = [1, 2, 3, 4, 5];
const array4 = array1
.flatMap(plus9oddFilter); (5)
console.log(array4);
1 | plus9oddFilter というモナド合成関数を作る |
2 | ここまで identity モナド関数 と一緒 自分自身を返している |
3 | plus9 モナド関数で flatMap |
4 | oddFilter モナド関数で flatMap |
5 | plus9oddFilter モナド関数で flatMap |
[ 11, 13 ]
モナド関数の設計も合成もすべて、Array.flatMap
メソッド1本で実現していることに注目してください。
5.7. まとめ
モナドは、map
メソッドに flat
メソッドを追加したオブジェクト。
map
と flat
を合成したのが、flatMap
で当然これもモナドのメソッド。
JavaScriptの Array.flatMap
でリストモナドに触れるので慣れよう。
モナドは構造がコントロールできるので、メリット多数。
メソッドチェーンがネストしても壊れない堅牢な構造。
Array.flatMap
は、Array.map
の上位互換。これ一本で何でも出来るようになる。
モナド関数の構成のことだけ気にしていれば良い。
Array.map
で消耗するのはもうやめよう。
6. 代数学と関数型プログラミングとオブジェクト指向の用語・記法の相互関係
この章では、今まで棚上げしてきた、モヤモヤしていたものをスッキリさせることを目指します。
関数型プログラミングは、プログラミングの複雑性を、以下の2つ
-
値
-
値でもある関数
の組み合わせ(function composition)で制御します。
メソッドチェーンをもって書き連ねるだけで、Demo:こんなことができるようになる
jQueryは値(オブジェクト)&関数(オブジェクトにぶらさがるメソッド群)のペアです。
なとど書いていますが、これは若干問題があります。多数の意味が曖昧な言葉、定義がはっきりしない、その正体についてはJavaScriptや関数型プログラミングとオブジェクト指向プログラミングで出て来がちな複数の文脈で暗黙の了解に委ねられている用語が散見されます。
-
値
-
関数
-
オブジェクト
-
メソッド
オブジェクト・メソッドについては明らかに出自がオブジェクト指向の用語です。
値、関数については、関数型ぽいが、同時にオブジェクト指向でも使われたりする。
JavaScriptは良かれ悪しかれ「マルチパラダイムプログラミング」言語なので、こういうわけのわからない状況に至ってもまあ仕方はないですが、特に関数型プログラミングを導入する際に曖昧さと混乱を引きずったままでゴリ押ししてしまうことが多いです。
用語は違うのに、数学的対象としては同じものを指し示していたりすることで、概念の重複、冗長性、曖昧さが生じてしまっています。
6.1. 二項演算とは 小学1/2年の算数からの復習
そこでとりあえず、根底となるプロトコルである数学の記法についてまず整理しておきましょう。
数学と言ってもたいしたことはない、小学1/2年の算数レベルのお話です。
これは初等数学で真っ先に習う 四則演算のうち「加算」と「乗算」です。
またさらに一般化、抽象化して、「2つの数から新たな数を決定する演算」のことを 二項演算と呼びます。要するに演算のパラメータが2つあったらそれは二項演算。また、2つのパラメータの中間に +
、-
などの演算子を置くのを 中置記法と呼びます。
パラメータが1個ならば、 単項演算 で、中学で習う平方根の演算子、ルートを使って
となりますね。
6.2. 演算は関数として捉えられる
パラメータの文字が出た時点でお察しですが、以上の演算は関数として解釈できます。
単項演算は、パラメータが1個なので、
Math.sqrt(9) //3
二項演算は、パラメータが2個なので、
const plus = (a) => (b) => (a + b);
plus(1)(2) //3
6.3. オブジェクト指向のメソッドでは
ここであえて復習するつもりもありませんが、オブジェクト指向のメソッドとは、オブジェクトに紐付いた関数のことですね。
JavaScriptの数値はカッコ()で囲んでやると、 Numberオブジェクトになるので、Number.prototype
へ新たに plus
メソッドを追加します。
Object.defineProperty(
Number.prototype,
"plus", {
value: function (b) {
return this + b;
}
});
(1).plus(2) //3
-
オブジェクト自身の値
this = 1
-
メソッド
plus
関数 -
パラメータ
2
二項演算(中置記法)
は、JavaScriptのオブジェクトとメソッドで書けます。
そして、このように、値(オブジェクト)&関数(オブジェクトにぶらさがるメソッド)のペアで書くのは非常に優れているんですね。
jQueryのメソッドチェーンのことを思い出しましょう。
は、そのまま、
と、メソッドチェーンで自然に書けてしまう。
逆に言うと、メソッドチェーンは、代数のなんらかの二項演算の連鎖をコード上に表現しているにすぎません。そして後からでてきますが、これはendofunctorやモナドにも当てはまります。 |
オブジェクトにぶらさがるメソッドではない普通の関数の形式
ではこううまくは行きません。
「なんとか地獄」と名前がつきそうな感じです。
JavaScriptがマルチパラダイムで、オブジェクト指向のメソッド形式で書けるおかげで、任意の二項演算、つまりパラメータを2つとる関数は、特別な定義不要で、その関数名(メソッド名)のまま中置記法が実現できてしまうという予期しない副産物(棚ぼた)です。
6.4. 値と演算は常に組(ペア)で存在する
抽象代数学におけるマグマ(英語: magma)または亜群(あぐん、groupoid)は、演算によって定義される種類の基本的な代数的構造であり、集合 M とその上の二項演算 M × M → M からなる組をいう。マグマ M における二項演算は M において閉じていることは要求するが、それ以外の何らの公理も課すものではない。 マグマ(数学)
基本的な代数構造において、演算だけ独立して存在していることはありません。必ず演算のターゲットとなる値の集合と組(ペア)として存在しています。
たとえば、 四則演算のうち「加算」は演算対象となるデータとは加算可能な数値ですよね?文字列であったり、なにかの画像データではありません。
抽象代数学 とか 代数的構造 とか言われると、つい数値のことを連想しがちなのですが、
マグマ M における二項演算は M において閉じていることは要求するが、それ以外の何らの公理も課すものではない。
とあるとおり、なんの制約もありません。
値が文字列ならば、その組となる、文字列というデータを演算するための二項演算は自由に定義可能だし、実際JavaScriptには、 Stringプロトタイプオブジェクトと、それ専用の二項演算子が実装されています。
"Hello" + " " + "world" //Hello world
文字列データを二項演算するときの +
は文字列の接続処理で、数値データを二項演算する +
の加算処理とは意味が異なります。値と演算は常に組(ペア)で存在するのであって、演算子の単独では意味を成しません。
そしてこれは、まさにオブジェクトとメソッドの関係に合致しており、二項演算の連続的操作が、そのまま上手くオブジェクトのメソッドチェーンで書けてしまう理論的背景が納得できます。
関数型プログラミングで、値、関数というとき、暗黙に組(ペア)となる相手がいます(プログラムで処理されないデータは意味がない)。そして、静的型付けの仕組み(JavaScriptならTypeScriptを使えばいい)などで、この値と関数の組(ペア)性を保証していきます。
しかし、繰り返し、これはまったく想定外のことですが、関数型プログラミングであっても、オブジェクト指向のオブジェクトとメソッドという組は、値(データ)と演算(関数)が組となる二項演算を定義する代数構造と解釈することで極めて有用です。
6.5. まとめ
二項演算をベースに考える。
マグマ(英語: magma)または亜群(あぐん、groupoid)は、演算によって定義される種類の基本的な代数的構造であり、集合 M とその上の二項演算 M * M → M からなる組をいう。 値と演算は常に組(ペア)で存在するのであって、演算子の単独では意味を成しません。 と、逐一書くのも面倒なので、今後マグマという組(ペア)は
\[(M, ∗)\]
と書くことにします。 演算 たとえば、二項演算が自然数の足し算と定まれば、ワイルドカード
\[(自然数,+)\]
二項演算が自然数の掛け算と定まれば、
\[(自然数,\times)\]
繰り返し念の為ですが、代数構造といえども、対象となるデータは、数値に限りません。 二項演算が文字列の接続と定まれば、ワイルドカード
\[(文字列,+)\]
|
マグマ(M, ∗) はプログラムの世界にそのまま展開できて、
M
= 値、データ、オブジェクト
*
= 二項演算、パラメータ2つの関数、メソッド
と言うように、データと処理の組、つまりデータ処理のことだと解釈できます。
という二項演算の連続的操作は、そのまま、
とオブジェクトのメソッドチェーンとして表現できる。
代数 |
値 |
演算 |
---|---|---|
関数型 |
値、データ |
関数 |
オブジェクト指向 |
値、データ、オブジェクト |
メソッド |
7. モノイド(Monoid)
モノイドは、理解するのが簡単、しかし奥が深く、モナドと同じかそれ以上に関数型プログラミングで応用局面もあり実用性が高いという、費用対効果(コスパ)抜群の品質の高いプログラミングの部品となりうるものです。
だいたい、モナドを知りたいのなら、同時にモノイドを知っておくべきなのは当たり前のことなのですが、ここまでモナド偏重でモノイドについてはあまり語られないのは、モナドを理解するのが難しい理由の事情が原因です。
7.1. 単位元
また、小学1年算数を復習すると、
みたいな足し算を最初に学びます。
子供というか、我々大人でも、脳は、すでに馴染みがある事象の延長・拡張でしか「理解する」というのは無理で、まず最初は、具体的な物質である「数え棒」「おはじき」を渡されて、徐々に数学的な抽象的概念に慣らされていきます。
どういう数学なのかというと、(正の)自然数全体のなす加法の二項演算ですよね。一番シンプルなパターンです。
このとき、 +
は、(正の)自然数全体 と組(ペア)となる二項演算としてしっかりと定義されています。
児童が(正の)自然数全体のなす加法の二項演算という抽象的作業に慣らされたところで大事件が起こります。
1 から 1 を ひいた かず を ゼロ と いいます。 ゼロ は 0 と かきます。
0は、なにも、ない かず です。
だから、 かず に 0 を たしても、 かわりません。
たとえば
7+0=7 「なな たす ゼロ は(わ) なな」です。
ゼロの発明は、数学史の飛躍の一つで、5世紀ごろのインド文明 で数字としてのゼロが発明されたのも数学が生まれてから2000年くらい経過した後ですし、ヨーロッパで広まったのは、中世を経てルネサンスのさらに後のニュートンの時代ですから、人類の数学史を考えると結構最近の発明だと言えます。
それなのに、さらっと小1の児童にゼロの概念をさも当たり前のように伝えるのですから、教育というものの凄まじさを実感できます。
数の体系が
(正の)自然数全体
↓
(ゼロを含む)自然数全体
にしれっと拡張されてしまいました。
そして、ここで誤魔化されてはならないのが、同じ記号 +
であっても、ゲームのルールが異なる、ということ。
二項演算というのは、必ず、演算対象と組(ペア)となってはじめて意味がある定義が成されるはずだったので、
(正の)自然数全体のなす加法の二項演算
↓
(ゼロを含む)自然数全体のなす加法の二項演算
と、二項演算も同時に更新されてしまいました。
単位元の添加
こういう正の自然数に更に「ゼロの後乗せ」してゼロを含む自然数に拡張する、同時に組(ペア)になっているはずの二項演算子も更新することを、単位元の添加と言います。 マグマ (M, ∗) が与えられたとき、M に M のどの元とも異なる新たな元 1 を付け加えた集合 M1 := M ∪ {1} で 任意の a ∈ M1 に対して a * 1 = 1 * a = a と定めて、M の演算 ∗ を M1 上に延長することにより、元 1 を M1 の ∗ に関する単位元とすることができる。この (M1, ∗) を (M, ∗) の 1-添加という。 もし、M がもともと ∗ に関する単位元 e を持っていたとしても、e は M1 上ではもはや ∗ に関する単位元ではない。 |
こういう、「だから、 かず に 0 を たしても、 かわりません。」というような、ある対象を演算しても不変になるような対象を 単位元と言います。
ここで「かず」とは書かずに「対象」とか書いたのは、演算も単位元も、べつに数に限らないからです。
数学、とくに抽象代数学において、単位元(たんいげん, 英: identity element)あるいは中立元(ちゅうりつげん, 英: neutral element)は、二項演算を備えた集合の特別な元で、ほかのどの元もその二項演算による単位元との結合の影響を受けない。
単位元を表す記号
「単位元」は、identity elementから、
あるいは「中立元」neutral elementの頭文字nに似ている(と筆者は思っている)
と表記されることが多いですが、本稿では、とっつきやすさを重視して、 eと表記することにします。 ただし、後々まったく同じ数学的対象なのに、後から、単位元のことを、identityと書かれたり、ηと書かれたり、場合によっては |
7.1.1. 左右の単位元
加法の単位元 e
は 0
で、
乗法の単位元 e
は 1
で、
文字列の単位元 e
は ""
となります。
7.1.2. 結合法則
このように
-
左右の単位元 e がある
-
結合法則が成り立つ
代数構造のことを、モノイド(monoid)と呼びます。
ちなみに、四則演算の仲間でも引き算と割り算は、モノイドにはなりません、念の為。
7.2. なぜモノイドと結合法則が重要なのか?
モノイド(monoid)だの「結合法則」だの言われると、理屈は単純でも、仰々しい天下り説明ぽくて、なんでそんなことが必要なのか?と思いがちなので説明します。
モノイドは、構造として対称性があって、適当に組み合わせても不変性があるので、関数型プログラミングの部品としては優れています。
部品の組み合わせということで、たとえばLEGOブロックを考えてみると、組み立て順序は自由なはずです。ある部分を先に組み立てて、別の部分を組み立て、それらをまた組み合わせる。これがもし、aとbは先に組み立てなければいけない、bとcを先に組み立てたものに後からaを組み合わせても、別物になるから!となると面倒なことになります。
USBデバイスを考えてみましょう。USBハブやら組み合わせ自由で、その接続する順番は気にする必要はないですよね?組み合わせは組み合わせです。順序によって構造に違いは生まれません。
ちなみに、LEGOブロックの組み立て、USBデバイスの接続も二項演算です。小1の授業でやられたみたいに、何も組み立てない、何も接続しない、というゲームのルールを追加したならば、二項演算しても何も影響を及ばなさい単位元の添加したってことなので、それまで考えていた組み立ての意味とは異なるでしょうが、そういうモノイドになります。
結合法則が成り立つ というのは、法則によってプログラマが縛られたり、法則を満たすように留意事項増える、ということではありません。まったくその逆で、法則によって、こういった組み合わせ順序は自由、という自由度、柔軟性、堅牢性がある部品、という保証があるということです。言い換えると、使いやすい基準をパスしている品質の高い部品だということ。
プログラミングはただでさえ、複雑で、何も考えないでやると、どんどん複雑になっていってコントロール不能、デバッグ不可能になっていきますよね?なるだけ構造はシンプルに維持しておきたいのです。
この部品はモノイドなので、組み合わせの自由度が高い、逆に、モノイドじゃないので、どんどん構造が増えていって面倒なことになるな・・・という認識が持てるのと持てないとでは大きな違いです。この部品はモノイドであることは事前に十分確認済みなので、このメソッド(二項演算)まわりで予期しない振る舞いをして、バグが出るはずはない、と確信を持ってスルーできるのはかなり大きいメリットですよね?
モノイドは3つ組
でしたが、マグマ(M,∗)でも特に、
がモノイドです。モノイドのことは、
\[(M,e,*)\]
と書くことにしましょう。 組(ペア)から3つ組(トリプル)になったのがポイントです。 具体的な二項演算が定まったときは、
\[(自然数,0, +)\]
\[(自然数,1,\times)\]
\[(文字列,"", +)\]
というようになります。 |
7.3. 単一のタイプで自己完結
モノイドは
というようにすべて、ただ一種類のタイプで自己完結している二項演算の世界です。
モノイドは連続的に接続可能で、自然数の加法の二項演算の場合、
という二項演算の連続的操作は、そのまま、
とオブジェクト指向のメソッドでは、メソッドチェーンとして表現できます。
7.4. Array(リスト・配列)は、モノイド
Array(リスト・配列)は、モノイドです。
7.4.1. Array.concat メソッドという二項演算
concat() メソッドは、配列に他の配列や値をつないでできた新しい配列を返します。
7.4.2. Array.concat メソッドで不変の左右の単位元 eとは?
Array.concat
メソッドを二項演算 *
と再び捉え直すと、
と、Arrayモノイドの左右の単位元 e は [ ]
。
7.4.3. Array.concat は結合法則を満たす
const array1 =
[1, 2]
.concat([3]) (1)
.concat([4, 5]); (2)
console.log(array1);
1 | [1, 2] と [3] を接続 |
2 | [1, 2, 3] と [4,5] を接続 |
[ 1, 2, 3, 4, 5 ]
const array1 =
[1, 2].concat( (1)
[3].concat([4, 5]) (2)
);
console.log(array1);
1 | [1,2] と [3,4,5] を後から接続 |
2 | [3] と [4,5] を先に接続 |
[ 1, 2, 3, 4, 5 ]
と結合順序を変えても結果は変わりません。
7.5. まとめ
モノイドは関数型プログラミングで役立つし、理解しておくのは重要。この章はただの紹介にすぎず、もっと充実すべく加筆が必要。
Array.flatMapと似ている?
モノイドの結合法則から、 次の章ではそこを追求してスッキリさせましょう。 |
8. モノイドとモナドの関係
だいたい、モノイドとモナドは名前が似すぎています。何らかの密接な関係性がきっとあるのでしょう。
ここでは、まず Arrayモノイドと Arrayモナドの根本的な差を確認してから、関係性をみていきます。
8.1. モノイドは2つの単一のタイプの間の二項演算
Array.concat
を二項演算とするArrayモノイド(3つ組)
は、
[1, 2]
.concat([3])
.concat([4, 5])
とメソッドチェーンで書けます。
-
任意の
Array
の値をa
-
Array.concat
メソッドを二項演算子*
と置き換えてやれば、
が基本形で、連鎖できるのだから、
という形になっています。
と同じことです、念の為。
Array.concat
は2つのパラメータをとり、1つの返り値がありますが、すべて3つとも同一のタイプで閉じた世界の二項演算です。
8.2. モナドはモナド値とモナド関数の間の二項演算
Array.flatMap
を二項演算とするArrayモナド(リストモナド)は、
[1, 2, 3, 4, 5]
.flatMap(a => [a * 2])
.flatMap(a => [a + 1])
とメソッドチェーンで書けます。同じように
-
任意の
Array
の値をa
-
Array.concat
メソッドを二項演算子*
-
モナドが返り値と規定される関数を
f
と置き換えてやれば、
が基本形で、連鎖できるのだから、
という形になっています。
モノイドのように、2つのパラメータ、1つの返り値、すべて3つとも同一のタイプで閉じた世界だ、というのとは根本的に異なります。
Arrayモナドのメソッドである Array.flatMap
は
-
Array
の値a
-
モナド関数
f
と二つの異なるタイプの間の二項演算です。
A monad in a bicategory K というのは、とりあえず置いときましょう
の bicategory
(2つのカテゴリ)とか書かれていたのは、このことです。
8.3. モナドはモノイドなのか?
まあ、上記のとおり、モノイドは単一タイプの二項演算で、モナドは二つの異なるタイプの二項演算と根本的に異なるので、答えはNOのように思えますが、見方によってはYES・・・みたいなことにはならないでしょうか?
そういえば、モナド(Monad)とは何か?のまとめに、
圏論のモナド(monad)の定義をまとめると
この3つ組(トリプル)
\[(endofunctor, unit, flat)\]
をモナドと呼びます。 |
と厳密な圏論のモナド定義がありましたが、flat
はすでにモナド二項演算として関数合成されてしまって今は、flatMap
となっていたのでした。
8.3.1. モナドの単位元
では今度は、Array.flatMap
を二項演算とするArrayモナドを、
Array.concat
を二項演算とするArrayモノイド(3つ組)
のように、モノイドの観点から捉えられないか?
とならないか?
flatMapの左右単位元 の候補として手元に唯一残っている部品は、flatMap
に合成されてしまった flat
と対になる関数 unit
(a ⇒ [a]
) です。
Arrayモノイドの Array.concat
メソッドで確認したことは以下です。
Array.concat メソッドの二項演算と単位元
\[[\space].concat([1,2])\]
\[= [1,2]\]
\[=[1,2].concat([\space])\]
\[[\space]*[1,2] = [1,2] = [1,2]*[\space]\]
と、Arrayモノイドの左右の単位元 e は モノイド(3つ組)
\[(Array,[\space],concat)\]
|
flatMapの左右単位元 が unit
だと証明するためには、これをリバースエンジニアリングしていければいいでしょう。多分。
Array.flatMap
メソッドを二項演算 *
と再び捉え直すと、
としたいところですが、これではタイプエラーになります。
Array.flatMap
は
-
Array
の値a
-
モナド関数
f
と二つの異なるタイプの間の二項演算
で、右辺はこのタイプで合致しますが、左辺は、最初に Array
の値 a
が入るべきところ、unit
関数になっているのでタイプが合いません。
逆に、モナド関数 f
を使っても
同じ理由で左右タイプエラーになります。
なので、すべての項において、この二項演算に合うようにパラメータと返り値のタイプを合わせます。
これが本当に成立していれば、flatMapの左右単位元 は unit
だと言えそうです。
二項演算 *
をまた Array.flatMap
メソッドに戻して、具体的な値を決め打ちして挙動を検証してみます。
const unit = a => [a];
const a = [1, 2];
const f = a =>
a.flatMap(a => [a, a * 5]); (1)
const left = unit(a).flatMap(f); (2)
const center = f(a); (3)
const right = f(a).flatMap(unit); (4)
console.log(left);
console.log(center);
console.log(right);
1 | 適当なモナド関数 モナド関数を設計する |
2 | \(unit(a)*f\) 左単位元 |
3 | \(f(a)\) |
4 | \(f(a)*unit\) 右単位元 |
[ 1, 5, 2, 10 ]
[ 1, 5, 2, 10 ]
[ 1, 5, 2, 10 ]
本当に成立したので、flatMapの左右単位元 は unit
だと言えそうです。
8.3.2. モナドの結合法則
あとモノイドの重要な特性として、結合法則を満たしている、というのがあります。
単一タイプ(a,b,c)間の二項演算 *
をもつモノイドの結合法則は、
モナド値 a とモナド関数(f, g)の2タイプ間の二項演算 *
をもつモナドの結合法則では、
モナド合成関数 fg
\[a * f = a\]
(二項演算の後ろに来るのは必ずモナド関数だ) という制約があるため、右辺の結合では、先に
\[a \Rightarrow a * f * g\]
と、モナド関数の合成をしていることに留意してください。 合成モナド関数
\[fg = a \Rightarrow a * f * g\]
と置き換えた上で、結合法則を書き直せば、
\[(a * f) * g = a * f * g = a * fg\]
となります。 |
{
const array1 =
[1, 2, 3] (1)
.flatMap(a => [a * 2]) (1)
.flatMap(a => [a + 1]); (1)
console.log(array1);
}
{
const array1 =
[1, 2, 3] (2)
.flatMap( (2)
a => [a] (3)
.flatMap(a => [a * 2]) (3)
.flatMap(a => [a + 1]) (3)
);
console.log(array1);
}
1 | \(a * f * g\) |
2 | \(a * fg\) |
3 | \(a \Rightarrow a * f * g\) モナド合成関数 fg |
[ 3, 5, 7 ]
[ 3, 5, 7 ]
というか、実はこれ リストモナド(List Monad)のつかいかたのモナド関数を設計するで、やっていたことの繰り返しで、とっくに検証は終わっています。 モナドの結合法則とは、モナド関数の合成のことだったんですね。
8.4. クライスリトリプル(Kleisli triple)
このように、
「モナドっていうのは、ただ単に、自己関手(endofunctor)の圏の中におけるモノイドのことなんだよ、なにか問題でも?」
などと時折言われるわけですが、モナドをモノイドの性質を備える特殊なendofunctorであると捉え、
(オブジェクト、左右単位元、二項演算)の3つ組(トリプル)にしたもの
を、クライスリトリプル(Kleisli triple)と呼びます。
比較してみよう ふたつのトリプル
圏論で一般的に定義されるモナド(monad)/トリプル
\[(endofunctor, unit, flat)\]
クライスリトリプル(Kleisli triple)
\[(endofunctor,unit,flatMap)\]
|
8.5. モナド則(Monad Laws)
さらに推し進め、モノイド則の用語をまるまる踏襲した上でモナドの法則として列挙したのがモナド則(Monad Laws)です。
すでに書いていますが、再掲すると、
ですね。
モナド則解読不能版
モナド則は、左単位元と右単位元にバラされた上で、右単位元の
という感じでエンドユーザに提供されることが多いようです。 とりあえず、モノイドのことを知らない人、知っててもモナドとの関連がわからない人には、特に上のように式変形された結果、対称性も読み取りにくい左右の単位元とか解読不能でしょう。 あとモナドがプログラミングに導入された例の歴史的経緯により、Haskell特有の二項演算子の表記と、Syntaxで提示されることが多いので、HaskellのSyntaxがわからない人はお手上げとなる可能性が高いです。 |
モナドの二項演算 *
を Array.flatMap
メソッドとして具体化して書き直すと、
const left = unit(a).flatMap(f);
const center = f(a);
const right = f(a).flatMap(unit);
となりますが、これは Array.flatMap
に限った構造ではなく、他のモナド実装でも同じ様相になります。もちろん、unit
flatMap
などの関数名は実装者の好み、さじ加減1つなので、ケースバイケースです。
8.6. まとめ
モナドを知るときは、同時にモノイドのことも知っておこう。
9. Promise (ES6+) はモナドか?
ES6+ Promiseがモナドだ、としばしば言われますが、モナドではありません。
Promiseは事前想定の多すぎるAPIで使いにくいと思うのですが、事前想定が多い1つの弊害として、構造が限定されてしまっている、ということがあります。
Promiseで unit
に相当するのは、Promise.of
ですが、そのまま置き換えることは出来ないようなので、 Promise.resolve.bind(Promise)
として、
const unit = Promise.resolve.bind(Promise);
const p = unit(3); (1)
const pp = unit(p); (2)
const ppp = unit(pp) (3)
console.log(p);
console.log(pp);
console.log(ppp);
1 | ただのPromiseオブジェクト |
2 | 二重にネストしたPromiseオブジェクト? |
3 | 三重に・・・ |
Promise { 3 }
Promise { 3 }
Promise { 3 }
おっと、全部同じ値になってしまいました。
Promiseは、ネストしたPromiseオブジェクトを許容しておらず、ぜんぶ平坦化してしまうようです。
つまり、Promiseは、これ
をやっているわけですが、問題は、endofunctor の map
に相当する then
をする 前 に、問答無用で flat
かましてるんですね。
結果、裸の値 or Promise1階層の二択しかありません。Promiseでネストした構造をもつことは出来ません。
本稿で何度も例に出てくる数字や文字列は、モノイドでありモナドですが、これらもご存知の通り、内部構造を持てません。
{
const unit = Number;
const n = unit(1);
const nn = unit(n);
console.log(n);
console.log(nn);
}
{
const unit = String;
const s = unit("Hello");
const ss = unit(s);
console.log(s);
console.log(ss);
}
1
1
Hello
Hello
しかし、数字と文字列がPromiseと違うのは、構造が自分自身の1種類だけなんですね。Promiseは、別のタイプの裸の値かPromiseの値か2種類。
ネストした数字や文字列っていうのは、(ここで扱っているものに限れば)はなっから存在せず、構造持たない1種類で整合性が取れてますが、Promiseはそうではないので整合性が取れません。
モナドというのは、このように、
自由に階層と構造を指定できる(それか数や文字列のようにそもそも階層が存在しない)ので、ここであえてモナド則を持ち出すまでもなく、Promiseがモナドではない、というのは明らかです。
ネストできるだろうと想定してPromise自身を受け渡す Promise.then
メソッドを無理やり組むと、それ以前に即座に flat
かまされているわけですから、タイプエラーが出るでしょう。モナド則(モノイド則)で言うと、Promiseオブジェクトをモナド値としたときの結合法則が成り立ちません。
Promiseがネスト構造の操作を許容しない、ということで得られるメリットとは?ちょっと思いつかないですね。
9.1. まとめ
自由に内部構造(ネスト構造)を持てないオブジェクトはモナドではない。ただし、数字、文字列などは最初から自身でネストしてるような構造(JavaScriptのPrimitive)は除く。それ以外にもさらに何か特別な例外がある可能性までは厳密に検証はしていないが、少なくともPromiseがモナドでないことは明らか。
10. ミュータブルな状態、IO、そしてモナド
これについては、一大別テーマなので、「後編」として別に書きます。長いし。