岡部 健Ken Okabekentutorialbook@gmail.com 関数型プログラミングが『銀の弾丸』である という非常識な常識 2022Functional Programming as the Silver bullet, that is the Insane common sense 2022
関数型プログラミングFunctional programming)を解説します。
New!ReactのためのFRP(ReactiveMonad)のDemo その他】の章を最後に追加しました 1. 何が問題なのだろう? 2. 命令型プログラミング=ほぼすべてのプログラミング入門者が半ば強制的に辿る道 2.1. Start from Scratch(ゼロからのスタート) 2.2. 命令型コード(JavaScript) 2.3. 命令型プログラミングに傾倒している「歪な状況」を根本的に解決する必要がある 3. 関数型プログラミング(Functional Programming)は式(Expression)の組み立て 3.1. 関数型コード(JavaScript) 3.2. 関数型コードは式(Expression)である 3.3. 関数型プログラミングは式(Expression)を組み立てていく作業 3.4. 関数型コードは値が変化するようなことはない 4. 関数二項演算子 関数型コードの式を構成する主要部品 4.1. 関数(Function)と二項演算子(Binary operator) 4.2. 関数(Function)と演算子(Operator)は本当は同じものである 4.3. 二項演算子(Binary operator)はとてつもなく強力な記法である 5. オブジェクト指向のメソッドを関数型の二項演算子として置き換える 5.1. 関数型&オブジェクト指向のハイブリッド言語としてのJavaScript 5.2. オブジェクト指向時代の終焉 5.3. オブジェクト指向のメソッドと二項演算の関係 5.4. JavaScriptで独自の二項演算子(Custom operator)を定義する 5.5. 既存のオブジェクトメソッドを二項演算子として捉え直す 6. 式の組み立ては依存グラフの構成 6.1. もっとも単純な関数型コード 6.2. 少し複雑な関数型コード 6.3. 依存グラフ(Dependency graph)を構成する式(関数型コード)を書いてコンピュータに解決してもらう 6.4. 計算不可能な巡回グラフ 有向閉路グラフ (directed cycle graph) 6.5. 計算可能な有向非巡回グラフ(DAG) 6.6. 命令型プログラミングのフローをコントロールする文(Statement)は完全に不要だった 数式でやれば良い 6.7. 二項演算'reduce'の振る舞いの謎 6.8. 依存グラフを構成する二項演算を最大限に活用した式の組み立て 7. 命令型コードのif文switch文は場合分けの演算式にする 7.1. 命令型コードのif文 7.2. 関数型コードのif式 7.3. 条件 (三項) 演算子をつかう 7.4. より洗練され強力なパターンマッチングをつかう 7.5. 【余談】TC39の没落 8. 関数型プログラミングで複雑性との闘いに勝利するためのたったひとつの方法 8.1. ある式を別の式に交換する 8.2. 「等しい(Equality)」という超強力な概念 8.3. ソフトウェア危機に対する工学的アプローチの根本的な問題 8.4. 『銀の弾丸』等しくすれば組合せ爆発(Combinatorial explosion)を回避可能 9. 型(Type)は何故そんなに役に立つのか?そもそも型(Type)とは何か? 9.1. JavaScriptコミュニティでは型(Type)が圧倒的に重要視されている 9.2. 巷によくある、よくわからない、型(Type)の説明 9.3. わかりやすい、型(Type)の説明 9.4. スーパーイディオム 10. 関数(Function) 10.1. 関数の具体例 10.2. かんたんにテーブルで表される関数 10.3. テーブルからかんたんにグラフにできる関数 10.4. 数学法則を使う関数 10.5. 数式だけが関数なのではない 11. 写像(Map) 11.1. 写像(Map)という概念の定義 11.2. 写像(Map)と集合(Set)と型(Type) 11.3. 型(type)集合(set)写像(map)関数(function)定義域(domain)値域(range)終域(codomain) 12. 関数の表記法 12.1. f(x) 記法とアロー記法 12.2. プレースホルダ(placeholder)という概念 12.3. f(x) 記法の関数の定義 12.4. f(x) 記法の関数の適用 12.5. f(x) 記法でもアロー記法でもない関数の定義 12.6. アロー記法の関数の定義 12.7. アロー記法の関数の適用 13. f(x) 記法からの脱却 パイプライン演算子(Pipeline Operator) 13.1. f(x) 記法の問題点 13.2. データファーストになるパイプ構造 13.3. 独自のパイプライン演算子をJavaScriptで実装する試み 13.4. 関数(写像)の連鎖をイメージ通りに表現できるパイプライン演算子 13.5. 二項演算子で式を書けばコードはシンプルになりType定義の構造と一致しやすい 13.6. 二項演算子(Custom operator)は見やすくて書きやすい 14. 代数構造(代数的構造)(Algebraic structure)と有向非巡回グラフ(DAG)=依存グラフ 14.1. これまでのまとめ 14.2. Twitterは代数構造と有向非巡回グラフ(DAG)=依存グラフを活用して自社システムを組んでいる 14.3. 代数構造(代数的構造)(Algebraic structure) 14.4. 3つの代数構造と「等しい(Equality)」という概念 15. モノイド(Monoid)複雑性を排除してくれるシンプルな二項演算 15.1. モノイド(Monoid)の具体例 15.2. モノイド(Monoid)の定義 15.3. モノイドの素晴らしさ① 型(Type)が単一の閉じた二項演算 15.4. モノイドの素晴らしさ② 組み合わせ方に依存しない、結合法則(Associativity) 15.5. モノイドの素晴らしさ③ 単位元(Identity element) 15.6. 半群(Semigroup)にはいつでも単位元を追加してモノイドにできる 15.7. モノイドとFold(Reduce)の強力さ 15.8. Fold(Reduce)と単位元 15.9. 論理演算もモノイド 16. 関数(写像)の連鎖と関数(写像)の合成(Function composition) 16.1. 関数(写像)の連鎖 16.2. 関数(写像)の合成(Function composition) 16.3. 関数(写像)合成(Function composition)は二項演算 16.4. 関数(写像)合成(Function composition)はモノイド(Monoid) 16.5. 関数の合成という二項演算には結合性がある 16.6. 関数の合成という二項演算には左右の単位元が存在する 16.7. 関数合成という二項演算はモノイド(証明終わり) 17. 高階関数(Higher-order function) 17.1. 関数の合成(Function compositon)と高階関数(Higher-order function)という用語 17.2. 高階関数(Higher-order function)は世界にありふれている 17.3. パイプライン演算子をつかって高階関数を表現する 17.4. 関数の合成(Function compositon)と高階関数(Higher-order function)の比較 17.5. 関数同士の組み合わせでない高階関数 17.6. 高階関数(Higher-order function)とラムダ式(アロー関数式) 17.7. ラムダ式(アロー関数式)で遊ぶ identity関数 17.8. ラムダ式(アロー関数式)で遊ぶ right関数 17.9. ラムダ式(アロー関数式)で遊ぶ log関数 17.10. right関数で命令型コードの順次実行をエミュレートしてしまう 17.11. コンビネータ論理 17.12. 2+引数関数は使わず単項関数(Unary function)を使う  17.13. カリー化(currying) 17.14. カリー化(currying)する関数とアン・カリー化(uncurrying)する関数 18. 独自の二項演算(Custom operator)、関数合成の二項演算子、パイプライン演算子の実装 18.1. 道具立てを揃える 18.2. JavaScriptで独自の二項演算子(Custom operator)を定義する 18.3. JavaScriptで独自の二項演算子(Custom operator)を定義するための関数 18.4. 関数合成の二項演算子の実装 グローバル 18.5. 関数合成の二項演算子の実装 ローカル 18.6. パイプライン演算子の実装 18.7. null問題とオプション型(OptionType) 18.8. パイプライン演算子のコード 19. ファンクタ(Functor)すべてを包括するような代数構造 19.1. モノイド(Monoid)と同じくらい重要なファンクタ(Functor)という代数構造 19.2. ファンクタ(Functor)すべてを包括するような代数構造 19.3. Array.map()というファンクタ(Functor)を調べればわかってくること 19.4. パイプライン演算(Pipeline operation)は Identity functor 19.5. FunctorはIdentity functor(関数適用)を基本とする拡張可能な代数構造 19.6. Functorをもっと厳密にTypeScriptで定義していく 19.7. 圏論(Category theory)のFunctor 20. モナド(Monad)はファンクタ(Functor)の特別なケース 20.1. パイプライン演算は関数合成(Function composition)ができる 20.2. パイプライン演算はIdentity functorで関数合成(Function composition)ができる特別なFunctor モナド(Monad) 20.3. mapFunctorの連結の様子 Functorの合成(composition) 20.4. mapFunctorの合成(Functor composition)の問題点 20.5. flatMapMonadの登場   20.6. mapとflatMapの比較 20.7. flatMapは結合性と単位元があるFunctorなのでMonad 20.8. Monadの左右の単位元(Identity)はタイプコンストラクタ関数 20.9. Monadをもっと厳密にTypeScriptで定義していく 20.10. Monadの実装にはflatという概念が使われている 20.11. MonadはFunctorのより安全な上位互換となる代数構造 20.12. Monadのつくりかた 21. 「非同期(Asynchronous)」と命令型プログラミングという技術的負債とasync/await 21.1. ノイマン型コンピュータ 21.2. 現代ではノイマン型の命令型思考は技術的負債となっている 21.3. 「非同期(Asynchronous)」という用語 21.4. JavaScriptの非同期関数(Async function)async/await 22. 関数型リアクティブプログラミング(FRP) 時間に依存する関数型コードの書き方 22.1. 命令型コードは時間とコードの配置場所に暗黙に依存している 22.2. 命令型コードで普通にやっている値の書き換えというのは一体なんなのか? ミュータブル(mutable)な世界 22.3. イミュータブル(Immutable)な世界 22.4. GitはImmutableな永続データ構造Persistent data structure 22.5. 関数そして二項演算子は暗黙に時間依存してはならない 22.6. 関数そして二項演算子が時間依存するときは明示的に依存グラフを構成する 22.7. 時間軸上のイベントを参照する関数 22.8. イベント駆動型プログラミング(Event-driven programming) 22.9. リアクティブプログラミング(Reactive programming) 22.10. PromiseはJavaScriptに導入された時間軸上のイベントのFunctor 23. シンプルでミニマルかつ強力なFRPの実装 23.1. Demo 23.2. ReactiveFunctor ReactiveMonad 23.3. ReactiveFunctor を実装するための下敷きとなるアイデア 23.4. ReactiveFunctor を実装するスタート地点 23.5. ReactiveFunctor のタイプコンストラクタ(型構築子) 23.6. ReactiveFunctor の二項演算子を表すreactive関数① 23.7. ReactiveFunctor のトリガー関数 change() 23.8. ReactiveFunctor の二項演算子を表すreactive関数② 23.9. 初歩的なReactiveFunctor のコード 23.10. 初歩的なReactiveFunctor のテスト 24. より安全なReactiveFunctor version2 24.1. やはりReactive値を保持する必要があった 24.2. ReactiveFunctor version 2 24.3. タイプコンストラクタ 24.4. reactive関数 24.5. change関数 24.6. version2のテスト 25. ReactiveFunctor version3 (typed with None) 25.1. Noneの導入 25.2. 型(Type)の導入 25.3. reactive演算子の追加  25.4. タイプコンストラクタ 25.5. isNoneとoptionMap 25.6. version3のテスト 26. ReactiveMonad 26.1. flat機構の追加 26.2. flatReactive関数と演算子の追加  26.3. タイプコンストラクタ 26.4. ReactiveMonad 結合性(Associativity)テスト 26.5. ReactiveMonad 左右の単位元(left & right Identity)テスト 26.6. ReactiveMonad ES Modules(ESM) 27. ReactiveMonadのエクステンション関数 27.1. Demo 27.2. ReactiveMonadとエクステンション関数の関係 27.3. エクステンション関数の実装 28. ReactのためのFRP(ReactiveMonad)のDemo その他 29. MIT License 30. フリー/購入 31. Contact インスタフォロー/DM シェアコメント  1. 何が問題なのだろう?
バグがなく生産効率が高くメンテナンスが容易なプログラミングを実現したいのだが結構難しいフォン・ノイマン型コンピュータというマシンを操作するという発想の命令型あるいはオブジェクト指向プログラミングは、設計とコードが複雑になるからマシンの状態を操作するという発想から脱却し、数学的手法に基づくシンプルで堅牢な関数型プログラミングの習得と実践!!!!!
そして、なにより習得するための良い入門書がほぼほぼ存在していない、ことです。本稿は筆者が「もっと早く読みたかった」と2022年現在に考える関数型プログラミングの入門書です。 TypeScriptの知識と実践は推奨されますが、本書で読み進めるにあたって必須ではありません。むしろ多くの基本的な概念については、型(Type)は必要ではなく逆に煩雑になることが多いので、JavaScriptを優先しています。 2. 命令型プログラミング=ほぼすべてのプログラミング入門者が半ば強制的に辿る道2.1. Start from Scratch(ゼロからのスタート)命令型プログラミングImperative Programming)は、コンピュータと呼ばれる物理的なハードウェアに順番に命令を送るという一連のシークエンスをコードとして並べる作業のことで、命令を送ればそのとおりマシンは動作するのだろう、という極めて自然で直感的にも理解しやすく原始的なやり方です。 2020年現在、ほぼすべてのプログラミング入門者は、まず命令型プログラミングの作法(プログラミングパラダイム (programming paradigm))を徹底的に叩き込まれることからはじめる慣習になっています。
https://www.nhk.or.jp/school/programming/start/index.html これは、古典的で素朴なプログラミングの作法を学ぶという意味では正しいかもしれません。そして将来的にこの状況が改革されるのかどうかも良くわかりません。 命令型プログラミングのコードとは以下のようなものです。 1から10までの数を足すコード
ここで、たとえば
x = x + 1
というコードがありますが、実際には3つの操作を表現しています。 1. Xに割り当てられているメモリに格納された値を読み取る2. その値をひとつ増やす3. 新しい値を X に割り当てられているメモリに書き込む こういうメモリのに格納された「今」の値をプログラマは常に想像しながらコーディングしていくわけです。 http://www.f.waseda.jp/moriya/PUBLIC_HTML/social/Scratch/21.pdf
とたいへん丁寧に説明されているわけですが、詳細がわかればわかるほど、命令型プログラミングのコードはかなり複雑なことをやっていることがわかってきます。 2.2. 命令型コード(JavaScript)
let sum = 0;let x = 0; for (let i = 0; i < 10; i++) { x = x + 1; console.log(x); sum = sum + x;} console.log(sum);
1234567891055 // sum
10回繰り返すループを作るためにforステートメントに i というカウンター変数を使うパターンです。 sumx が繰り返しループとともに刻々と値が変化していき、コードの字面の変数名を見ただけでは、その値が何なのかは?頭のなかでコードのコンピュータの振る舞いをエミュレーションしなければならず、その途中で不等式 x ⩽10 なども判断していく、ということになります。 あるいはカウンターとして1ずつ増えていく変数の代わりに使うパターンもありえて、その場合は使う変数はひとつ減らせる!と考えられるのかもしれません。 しかし、実はこれは与えられた課題の本質とは全く関係ないコードの流れ自体をあれこれ考えなければいけないという別の問題に頭を酷使しているのです。 for 文
命令型プログラミングのコードはたいへん複雑なのです。 2.3. 命令型プログラミングに傾倒している「歪な状況」を根本的に解決する必要がある この根本的な問題根本的に解決する必要があるのですが、ほぼすべての人は最初に刷り込まれたプログラミングの作法が命令型プログラミングなので、この命令型の作法こそがプログラミングの作法として至極当然であるとし、ここの部分のメンタルブロックが強烈で、なかなか発想を転換することができません。 そしてこれは何もプログラミングの経験が浅い初心者を中心に起こっていることではありません。実際には、後述するとおり、JavaScriptの仕様策定をしているグループであるTC39
の参加者でさえ、この命令型プログラミングへ過剰なまでに執着している現状があって、その結果、元来が本稿で解説するところである関数型プログラミングの概念の機能ですら、JavaScriptの新機能の仕様としては、命令型プログラミングの作法で実装されようとしている、というビックリ仰天のかなり歪な状態を筆者自身が目撃しており現状はかなり深刻であると感じています。 またこの種の問題点を共有しながら適切に解説しているドキュメントは非常に少なく、適切に発想を転換する機会に恵まれないことが現状です。本稿はその問題を適切にアプローチして解説することを目的としています。 3. 関数型プログラミング(Functional Programming)は式(Expression)の組み立て3.1. 関数型コード(JavaScript)上の命令型コードを関数型で書き直すならば、こうなります。
const add = (a, b) => a + b;const sum = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce(add); console.log(sum);
55 // sum
課題の本質ではない無用な変数は取り除かれ、プログラマーの頭を悩ますループの流れも存在せず、ただ2つの式constによって宣言されているだけです。 このコードについては、今後複数章にまたがって詳しく解説していきます。3.2. 関数型コードは式(Expression)であるJavaScriptは、文(Statement)式(Expression)から構成されています。 文と式(JavaScript Primer) 命令型プログラミングのコードは文(Statement)が中心でした。たとえば、for
関数型プログラミングのコードは、それに対して、式(Expression)が中心です。 3.3. 関数型プログラミングは式(Expression)を組み立てていく作業 言い換えると、関数型プログラミングは式(Expression)を組み立てていく作業に他なりません。この作業は、命令型プログラミングの作業における、文(Statement)を組み立て、カウンター変数を別途に用意することも含めて刻々と変化する変数を監理しながら流れを整える、という作業とは根本的に異なります。 3.4. 関数型コードは値が変化するようなことはないlet で定義した i x sum のようにコードの流れで値がコロコロと変化していくようなことはなく、 const で定義するように値は定数として(一発で)定義されます。あとから値を二重に破壊的代入しようとするとエラーが出ます。
const x = 5;x = 10; // error
命令型では、
let x;x = 5;x = 10;
というコードがJavaScriptでもちろん有効で、実際、
for (let i = 0; i < 10; i++) {//....
というように命令型コードのforループを回すときなどに必要になってくるのですが、関数型コードではアンチパターンです。理由はコードの位置、上下関係で値が変化するので式自体に明示されていない余剰の要素が紛れ込んで論理構成が破綻するからです。これについては後々詳細に説明していきます、 関数型コードでは命令型コードのように命令文の上下の並びから発生するコードの流れ(フロー)をベースにするのではなく、式の構成によってプログラミングしていきます。 従って命令型のようにコードの振る舞いをコンピュータに成り代わってプログラマーの頭の中でのエミュレーションをしながら、あるいはデバッガーに利用して値の変化を追跡する必要がありません。与えられた課題の本質とは全く関係ないコードの流れ自体をあれこれ考えなければいけないという別の問題に頭を酷使する必要は消滅します。これは少なくともこのレベルではデバッグが不要である、ということを意味します。 そしてこの式(Expression)の中心となる素材が、関数二項演算子です。 4. 関数二項演算子 関数型コードの式を構成する主要部品4.1. 関数(Function)と二項演算子(Binary operator) 関数(Function)については数学的にも関数型プログラミングとしても、両方の観点で非常に重要で根幹となる要素だけに、後の章で改めて詳しく説明しますが、いきなり最初から数学全開でいくと解説としてとっつきにくい懸念があるので、今回はとりあえずなんとなくわかっているものとして取り扱います。 二項演算子(Binary operator)は、我々が小学1年かそれ以前から慣れ親しんでいる 1 + 2 というような2つの項目をあわせて一つにする演算オペレーション)を 二項演算Binary operattionと呼び、そのうち+演算子(Operator)、あるいは英語カタカナ読みでオペレータ1,2,3などは被演算子(Operand) あるいは英語カタカナ読みでオペランドと呼びます。 ややこしく感じかねないですが、演算(Operation)演算子(Operator)被演算子(Operand)というペア要素で構成されている、ということです。これは用語はともかくとして、わざわざ説明されずとも我々が小学校以来、直感的に理解している事実です。 1 + 2 というような二項の間の演算なら二項演算二項演算子だ、となるわけですが、 論理否定の演算子 !
!true // false!false // true
のように、一項演算子、または単項演算というものも普通に存在しています。 4.2. 関数(Function)と演算子(Operator)は本当は同じものであるちょうど、一項演算子(単項演算子)の話が出たので、関数(Function)と演算子(Operator)の関係について、ここですぐ整理してしまいたいと思います。 関数(Function)と演算子(Operator)は本当は同じものです。 単項演算Unary operation) の場合
単項演算とは、数学で、被作用子(オペランド)が一つだけであるような演算(つまり、入力が一つの演算)のことたとえば、論理否定は真理値に対する単項演算であり、自乗は実数に対する単項演算である。階乗 n! も単項演算である。与えられた集合 S に対する単項演算は、関数 S→S に他ならない。
たとえば、論理否定の演算子 !
!true // false!false // true
は、
const f = a => !a; f(true); // falsef(false); // true
と関数表記へそのまま置き換えることができ、一項演算子は1引数の関数(Unary function)と対応するのがわかります。
!(true) // falsef(true) // false
とすれば、単にシンボルの割当ての違いにすぎない、とはっきりとわかります。 二項演算Binary operattion) の場合
これは図の通り、二項演算Binary operattionx ◦ y とは2引数の関数=二項関数(Binary functionf(x, y) であるということです。 つまり、1+2という二項演算は、+(1, 2)という二項関数の糖衣構文(syntax sugar)にすぎない、ということです。 我々が小学校1年生の頃から扱って慣れ親しんでいた二項演算とは、中学に進級して初めて習う関数というものと正体は同じもので、関数のほうが概念としては統一的に全体を俯瞰することができます。 実際に、純粋関数型言語であるHaskellではこの事実に準じて実際に、二項演算子と関数表記の相互入れ替えが言語仕様として可能になっています。 中置と前置の切り替え
ここでは、二項演算子はオペランドの間に挟まる記法なので、中置演算子と書かれていますね。 JavaScriptも同じように出来れば良いな、とは思いますし出来ないわけがないですが、今のところ意識が低いので出来ていません。JavaScriptの仕様策定団体であるTC39はJavaScript関数型プログラミングコミュニティの意に反して非常に意識が低いのは間違いありません。ただし、稀に良いことはあって、 べき乗 (**)という二項演算子がES2016で導入されました。
Math.pow(2, 3) ==2 ** 3 Math.pow(Math.pow(2, 3), 5) == 2 ** 3 ** 5
これは、まさに、二項関数(Binary functionMath.pow(x, y)二項演算Binary operattionx ** yへ置き換えたものです。 4.3. 二項演算子(Binary operator)はとてつもなく強力な記法である まったく同じ意味のコードであっても、べき乗 (**)という新しい二項演算子を導入した結果、いかにコードが無駄なく簡潔になり、数学的な構造が読みやすくなったのかは明白です。 特にこのように連鎖してネスト構造になった場合、非常にシンプルに記述できます。これはバグの混入を事前に防ぐことができ、プログラミングの生産効率が飛躍的に向上することに直結します。 二項演算Binary operattionx ◦ y とは二項関数(Binary functionf(x, y) のことであり、概念としては、関数だけで統合できてしまうのですが、数式の表記方法としてはむしろ、x ◦ y のほうが圧倒的に優れており、なにより、これは繰り返しになりますが我々が小学校の算数の時間から徹底的にトレーニングされてきた慣れ親しんでいる表記でもあります。非常に直感的に理解可能です。 ”関数”型プログラミングとして、二項演算子という主要部品も根源的には関数という概念に統合される、という概念の統一感があります。それと同時に、関数型プログラミングで、実際にコードを書いていくのは、あくまで二項演算子を極限まで活用する、二項演算を主とする式(Expression)の組み立てとする、ということを本稿の一貫した理念とします。 実際にこの二項演算で式を組み立てて行く、という理念あるいは手法は、Haskellのような純粋関数型言語では、ほぼ自動的に達成されてしまうので、実際にHaskellのコードでは二項演算の式だらけで、非常に短いコードで高度なことを成し遂げているのだけれども、外部の人間からしてみると何をやっているのかよくわからない、ということが起こるでしょう。 JavaScriptの関数型プログラミングにおいて、この二項演算子を極限まで活用する、という理念あるいは手法は、あまり広くシェアされているとは観察されませんが、本稿を読みすすめるにつれて、その強力さは共有されていくと考えます。 5. オブジェクト指向のメソッドを関数型の二項演算子として置き換える5.1. 関数型&オブジェクト指向のハイブリッド言語としてのJavaScriptJavaScriptは関数型プログラマーであるブレンダン・アイク(Brendan Eich)によって生み出されました。本人ブログエントリによると、実際に彼はNetScapeから、Webブラウザ実装言語にSchemeというLisp系の関数型言語を実装するという約束で雇用されたのですが、その過程で、Javaとオブジェクト指向ブームが巻き起こり、煽りを受けた結果、企業のマーケティングの理由から上層部の指示によって、「Javaのように見える」オブジェクト指向の性格をもつプログラミング言語が要求されることとなりました。その結果、世に送り出されたのが、関数型言語とオブジェクト指向の両方の性質を併せ持つハイブリッド言語としてのJavaScriptです。 オブジェクト指向自体をどのように定義するのか、ということ自体が歴史的に右往曲折があって厄介な問題なのですが、とりあえず巷で言われているオブジェクト指向プログラミング、言語では、たとえばC++やJavaは、関数型プログラミングを想定した代物ではありえない、という一点において原理的に命令型プログラミングを踏襲するカテゴリに入ると考えてまったく問題ありません。 関数型プログラミングをある程度以上のレベルで習得してしまうと、原理的にはオブジェクト指向プログラミングは不要の(冗長な)手法となります。関数型プログラミングは、オブジェクト指向プログラミングを完全に置き換えます。 なぜならば、関数型プログラミングは、すでに数学世界に存在している式(数式)を組み立てる、という極めてシンプルで解釈のブレのない作業ですが、オブジェクト指向プログラミングは同じ作業をオブジェクトという本質的には必要はなかった(あまり出来の良くはない)人工的に発明された枠組みの中で、様々な複雑なルールをそのオブジェクトに付与した冗長なフォーマットに焼き直した上で、こういうデザインパターンが望ましい、アンチパターンだ、と設計の欠点の辻褄あわせをしているにすぎないからです。 5.2. オブジェクト指向時代の終焉 当時の熱狂的なオブジェクト指向ブームは2020年代においてはとっくに過ぎ去り、長期に渡る壮大な検証実験を経た結果、多くの問題が指摘されるようになってきました。関数型プログラミングと比較してオブジェクト指向批判の数々の記事がWeb検索でヒットします。日本語の良記事も多くありますし、やはり英語圏のものも多いのでGoogle翻訳などをして読むと良いかもしれません。 Object-Oriented Programming — The Trillion Dollar Disasterオブジェクト指向言語ーそれは何兆ドル規模の厄災 さらに、JavaScriptが必須であるWebのフロントエンド界隈では、壮大な実験が行われました。Reactです。
JavaScriptはES2015(ES6)時代になり、オブジェクト指向そのもののClass(クラス)が新たに導入されました。これに伴い、Reactでも、フレームワークの根幹となるコンポーネントをClassで表現するように標準化されました。 筆者などは「いくらJavaScriptが根強いオブジェクト指向ファンの要請から、後方互換性のようなクラスが導入されたからといって、Reactのようなメジャーな外部ライブラリまでそれに習うのは困ったことになった、時代の逆行だ」と、まったく歓迎していませんでした。案の定、オブジェクト指向のクラスを標準コンポーネントとして利用するというReactのアプローチは失敗し、実質クラス実装のコンポーネントは破棄し、関数型に近いHooksという仕組みが導入されることになりました。 以下はIntroducing Hooksからの引用です。
Classes confuse both people and machinesIn addition to making code reuse and code organization more difficult, we’ve found that classes can be a large barrier to learning React. You have to understand how this works in JavaScript, which is very different from how it works in most languages. You have to remember to bind the event handlers. Without unstable syntax proposals, the code is very verbose. People can understand props, state, and top-down data flow perfectly well but still struggle with classes. The distinction between function and class components in React and when to use each one leads to disagreements even between experienced React developers. クラスは人も機械も混乱させるコードの再利用やコードの整理が難しくなるだけでなく、クラスがReactを学ぶ上で大きな障壁になることがわかりました。これがJavaScriptでどのように動作するかを理解しなければならず、それはほとんどの言語で動作する方法とは大きく異なります。イベントハンドラをバインドすることを覚えなければなりません。不安定な構文の提案がなければ、コードは非常に冗長になります。人々は、プロップ、ステート、トップダウンのデータフローは完璧に理解できても、クラスには苦労します。Reactにおける関数コンポーネントとクラスコンポーネントの区別と、それぞれをいつ使用するかについては、経験豊富なReact開発者の間でも意見の相違が見られます。
To solve these problems, Hooks let you use more of React’s features without classes. Conceptually, React components have always been closer to functions. Hooks embrace functions, but without sacrificing the practical spirit of React. Hooks provide access to imperative escape hatches and don’t require you to learn complex functional or reactive programming techniques. これらの問題を解決するために、HooksはクラスなしでReactの機能をより多く使用できるようにします。概念的には、Reactのコンポーネントは常に関数に近いものでした。Hooksは関数を受け入れますが、Reactの実用的な精神を犠牲にすることはありません。Hooksは、命令的なエスケープハッチへのアクセスを提供し、複雑な関数型またはリアクティブプログラミング技術を学ぶ必要はありません。
オブジェクト指向については、おもにオブジェクト指向の習得に長い時間を費やしたオブジェクト指向ブームに生きたプログラマーによって強い擁護がなされてきたのですが、結局の所、われわれプログラマーコミュニティはエンジニアであり現実世界でその技術がうまく機能するかどうか?というのは生命線といえるものであり、実際にReactのような大規模なシェアをもつWebフレームワークでうまく行かなかった、という厳然たる事実は到底覆い隠されるものではありませんでした。 結果「より関数型的」なReactHooksが導入されたわけですが、
Hooksは関数を受け入れますが、Reactの実用的な精神を犠牲にすることはありません。Hooksは、命令的なエスケープハッチへのアクセスを提供し、複雑な関数型またはリアクティブプログラミング技術を学ぶ必要はありません。
とあるのは、ここも根源的な間違いを犯しており、ReactHooksはかなり複雑で習得コストが高く、筆者個人はまったく評価していません。実際に読者でも使いづらいと理解している人たちはきっと多いでしょうし、実際に品質に不満な第三者がさまざまなより本質的に関数型的なアプローチのサードパーティの代替物がリリースされています。 5.3. オブジェクト指向のメソッドと二項演算の関係とはいえ、JavaScriptはオブジェクト指向言語でもあり、オブジェクト指向は言語の根深い実装と直結しており、オブジェクトとメソッドという構造が言語の基本となっています。関数型プログラミングをこれから志向していこうとしても、絶対に避けられるものではありません。 では、JavaScriptにおいて切り離すことができないオブジェクト指向の表記は、関数型プログラミングで扱うとしてどのように解釈すべきでしょうか? JavaScriptはオブジェクト指向言語でもあり、オブジェクトとメソッドという構造が言語の基本となっています。 たとえば文字列はStringオブジェクトという構造で定義されていて、String.concat() というメソッドがあらかじめ実装されています。
concat() メソッドは、文字列引数を呼び出し文字列に連結して、新しい文字列を返します。
"Hello".concat("World") // "HelloWorld""Hello".concat(" ").concat("world!") // "Hello world!"
「へーStringオブジェクトにこんなconcatというオブジェクトがあるのか!初めて知った」という読者もいることでしょう。なぜならば我々はこんなオブジェクトメソッドのまわりくどい表記は無視して、シンプルにこう書くことを好むからですね。
"Hello" + "World" // "HelloWorld""Hello" + " " + "world!" // "Hello world!"
まったく同じ意味と動作のコードなのですが、どちらが書きやすく、読みやすく、デバッグもしやすいのかは一目瞭然です。 String.concat(String) = Stringというオブジェクト指向のコードがString + String = Stringという二項演算に置き換えられる、という事実から以下の事実が明らかになります。 オブジェクト指向のオブジェクトは、数学の集合(Sets)に相当し、二項演算の左側被演算子(Operand)になります。オブジェクト指向のメソッドは、数学の二項演算子(Binary operatior)に相当しており、オブジェクト指向のメソッド引数Parameterは、二項演算の右側被演算子(Operand)になります。 また、オブジェクトのメソッドチェーンは、二項演算の連鎖と同じものである、という事実をかんたんなJavaScriptのコードから認識できるでしょう。
"Hello" .concat(" ") .concat("world!") "Hello" + " " + "world!"
二項演算子(Binary operator)はオブジェクト指向のコードを置き換え可能な、強力な表記法です。 また、JavaScriptの配列データ構造を司るArrayオブジェクトにはArray.concat() というメソッドがあらかじめ実装されています。String.concat()まったく同じメソッド名であることに注目してください。
concat() メソッドは、2つ以上の配列を結合するために使用します。このメソッドは既存の配列を変更せず、新しい配列を返します。
const array1 = ["a", "b", "c"];const array2 = ["d", "e", "f"];const array3 = array1.concat(array2); // ["a", "b", "c", "d", "e", "f"]
同じメソッド名で、機能面でもまったく同じ概念であることが確認できます。 JavaScriptでは、文字列の結合(連結)のための二項演算子が加法演算子のシンボルである + を共有していることを考えると、
"Hello" + "World"; // "HelloWorld"
配列の結合(連結)のための二項演算子も、同じように加法のシンボルである + を共有していても別に問題なかったはずです。
["a", "b", "c"] + ["d", "e", "f"]; // ["a", "b", "c", "d", "e", "f"]
こんなふうに。しかしこれは "a,b,cd,e,f" という変な結果になります。TypeScriptなら未然にちゃんとエラーが出ます。
文字列構造であるStringsと配列データ構造であるArrayは異なるオブジェクトですが、同じ機能を目的とする同じメソッド名のconcatを共有しています。しかし前者では便利に直感的に二項演算子 + を使えていたのに、後者では使えません。 実は、ここには別段合理的な理由など存在せず、ただ単にJavaScript言語実装の気まぐれにすぎないのです。べき乗の二項演算子をES2016で新規導入したのと同じように今からでも新規実装できるような事案ではあるのでしょう。 なんとかならないでしょうか? 5.4. JavaScriptで独自の二項演算子(Custom operator)を定義するJavaScriptでは現在このような機能はありません。tc39/proposal-operator-overloadingという既存の演算子のオーバーロードの検討はあるにはありますが、まったく活発ではないようですし、残念ながらTC39にはあまり期待できません。 しかし、多少のハック的手法を許容するのであれば、なんとかはなります。それが本書の手法です。すべては二項演算子を活用するという根幹の目的を達成するためです。逆にこの最難関ポイントを突破することさえできれば、関数型プログラミングの見通しがクリアになり、視野は広がり、理解を深めることが容易になります。 では、はじめてみましょう。 シンプルなお馴染みの + という二項演算子を用いた、加法の二項演算 1 + 2 = 31 + 2 + 3 = 6 は、という二項演算子によって、JavaScriptのオブジェクトとしてNumberNumberいう集合同士からNumberが演算される、と代数的に定義されています。 これは、オブジェクト指向の世界に翻訳すると、Numberオブジェクト+(Number)というメソッドが実装されていてNumberを返すということと同じです。つまり、
Object.defineProperty( Number.prototype, "plus", { value: function (R) { // R is Right hand side or the Binary operator return this + R; // `this` is Left hand side of the Binary operator }});
と、Numberオブジェクトにplusメソッドを拡張してやることで、
(1).plus(2); // 3(1).plus(2).plus(3); // 6
とオブジェクト指向の表記で記述することも可能です。 さらに、plusではなく本当に + という記号で拡張定義することも可能です。
Object.defineProperty( Number.prototype, "+", { value: function (R) { // R is Right hand side or the Binary operator return this + R; // `this` is Left hand side of the Binary operator }});
この場合は、オブジェクト指向的な . (ドット)記法ではSyntaxErrorになるので、代わりにプロパティ/連想配列形式でうまく書けます。
(1)['+'](2); // 3(1)['+'](2)['+'](3); // 6
車輪の再発明をすることも可能です。 同様に、Arrayオブジェクト + という記号のプロパティでArray.concat() というメソッドを拡張してしまいます。
Object.defineProperty( Array.prototype, "+", { value: function (R) { // R is Right hand side or the Binary operator return this.concat(R); // `this` is Left hand side of the Binary operator }});
このコードでは、this.concat(R) と単純にArrayオブジェクトメソッドを + というオブジェクトプロパティにコピーしています。その結果、
array1.concat(array2);array1['+'](array2);
となります。
["a", "b", "c"] + ["d", "e", "f"]; // ["a", "b", "c", "d", "e", "f"]
と書くのは言語仕様的に無理でも、
["a", "b", "c"]['+'](["d", "e", "f"]); // ["a", "b", "c", "d", "e", "f"]
と書くことは可能になりました。 JavaScriptで独自の二項演算子(Custom operator)を定義することに成功しました。 Object.defineProperty()では、デフォルトの設定では、プロパティの値は変更することはできず、プロパティ列挙可能性がfalseになります。プロパティの列挙可能性と所有権
列挙可能プロパティは、内部の列挙可能enumerableフラグが true に設定されているプロパティです。これは、単純な代入や初期化で作成されたプロパティのデフォルトです (Object.defineProperty で追加したプロパティはデフォルトで列挙可能性が false になります)。
5.5. 既存のオブジェクトメソッドを二項演算子として捉え直す上記のとおり、オブジェクトメソッド表記が、オブジェクトプロパティ/連想配列形式で表記できる、というのは、逆方向に応用することも可能です。つまり、
array1.concat(array2);array1['concat'](array2);
と書き直せるということで、すでに確認したとおり、
オブジェクト指向のオブジェクトは、数学の集合(Sets)に相当し、二項演算の左側被演算子(Operand)になります。オブジェクト指向のメソッドは、数学の二項演算子(Binary operatior)に相当しており、オブジェクト指向のメソッドの引数Parameterは、二項演算の右側被演算子(Operand)になります。
という事実が、オブジェクトメソッド表記から一旦離れることにより、意識しやすくなります。もちろん、既にオブジェクトメソッド表記で簡潔に書けていることは間違いないので、無理に書き直す必要性はないでしょうが、あくまで二項演算子として扱うことを常に意識するためには役立つでしょう。 同様の原理で、Arrayオブジェクトには最初から.mapメソッドが実装されていることを考慮して、
const f = a => a + 1; [1, 2, 3].map(f); // [2, 3, 4][1, 2, 3]['map'](f); // [2, 3, 4]
というように、Array.map(f) メソッドというのは、配列Arrayと関数f との間の二項演算子 map である、と理解できます。 では、ここで課題である関数型のコードを見てみましょう。
const add = (a, b) => a + b;const sum = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce(add);
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce(add)
という、オブジェクト指向フォーマットで書かれた式は、
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]['reduce'](add)
二項演算であることを強く意識したフォーマットへ書き直すこともできて、 reduce は、 配列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 関数 add の間の二項演算を司る二項演算子であると代数的に理解できます。(代数学についてはあとで説明) まとめると、 加算演算子である + は、NumberとNumberからNumberを生み出す二項演算子です。Number + Number = Number1 + 2 = 3 文字列結合としての + は、とStringとStringからStringを生み出す二項演算子です。String + String = String"Helllo" + "World!" = "HelloWorld!" 配列結合としての 'concat' は、ArrayとArrayからArrayを生み出す二項演算子です。Array 'concat' Array = Array[1,2,3] ['concat'] ([4,5,6)] = [1, 2, 3, 4, 5, 6] 'map' は、ArrayとFuncttionからArrayを生み出す二項演算子です。Array 'map' Function = Array[1,2,3] ['map'] (f) = [2,3,4] 'reduce' は、ArrayとFuncttionからObjectを生み出す二項演算子です。Array 'reduce' Function = ObjectObject はFunctionの働きによって変わる。[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ['reduce'] (add) = 55 二項演算子 'reduce' についてはまだまだ謎につつまれているので、さらなる探求が続く章とともに必要です。 6. 式の組み立ては依存グラフの構成6.1. もっとも単純な関数型コードJavaScriptは、文(Statement)式(Expression)から構成されている。命令型プログラミングのコードは、forループなどの文(Statement)が中心、関数型プログラミングのコードは、それに対して、式(Expression)が中心。 関数型コードは式(Expression)である。関数型プログラミングは式を組み立てていく作業。 の中心となる素材が、関数(Function)二項演算子(Binary operator)。 関数(Function)演算子(Operator)は本当は同じものである。二項演算子(Binary operator)はとてつもなく強力な記法である。 これが一番の大枠のまとめです。この大枠に付随する、どうしても説明しておかなければならなかった事柄も説明出来たと思います。 では実際に、式を組み立てていくとして、もっとも単純なコードは何でしょうか? おそらくもっとも単純な式とは、
1
あるいは
"hello"
こんな感じでしょう。これがもっとも単純な関数型のコードです。 ではもう少し進んで、
1 + 2
より関数型コードらしくなりました。なぜならば、関数(Function)演算子(Operator)は本当は同じもの二項演算Binary operattionx ◦ y とは二項関数(Binary functionf(x, y)
であることはすでに確認済みなので、1 + 2 は実際に関数(Function)を扱う式(Expression)である関数型コードです。よく考えると、我々は全員、小学校の義務教育の範囲内ですでに関数型コードを書いていた、ということになります。 6.2. 少し複雑な関数型コードさらにもう少し複雑にしてみましょう。
1 + 2 + 3
この関数型コードは、数学の演算子のルールによって、実際には暗黙の了解で、
(1 + 2) + 3
と解釈されます。数学であってもJavaScriptコードであっても変わりはありません。 というより、関数型プログラミングの力の源泉は数式であり数学そのものであるので、もし「プログラミングと数学は違う」であるとか「JavaScriptは純粋関数型言語ではないし関数型プログラミングは無理だ」という愚言が呈された場合は、それはたいてい強い命令型プログラミングへの傾倒によるメンタルブロックによるものですし、「このコードの振る舞いは数学とは異なる」と示された場合、本当にそうなのか慎重に吟味する必要がありますし、もし本当にそれが数学の振る舞いと異なるJavaScriptパーツなのであれば、関数型コードを書く場合は重大な障害になりうると認識し回避策を熟慮したほうが良いです。 JavaScriptのグループ化演算子 ( ) は、式における演算の優先順位を制御します。これは数学の()の振る舞いと等価です。 このとき、式の構造は
このように構成されました。上のコードと対比させると、x=1, y=2, z=3 です念の為。 6.3. 依存グラフ(Dependency graph)を構成する式(関数型コード)を書いてコンピュータに解決してもらう 上記の図はグラフを構成しています。特に依存(関係)グラフ(Dependency graphなどと呼ばれているものです。
In mathematics, computer science and digital electronics, a dependency graph is a directed graph representing dependencies of several objects towards each other. It is possible to derive an evaluation order or the absence of an evaluation order that respects the given dependencies from the dependency graph. 数学やコンピュータサイエンス、デジタルエレクトロニクスの分野で、依存(関係)グラフは、複数のオブジェクトの相互依存関係を表す有向グラフ (directed graphである。依存関係グラフから、与えられた依存関係を尊重した評価順序や評価順序がないことを導き出すことができる。
依存関係グラフから、与えられた依存関係を尊重した評価順序や評価順序がないことを導き出すことができる。
とあるとおり、関数型のコードでは、演算子とともにグループ化演算子 ( ) を組み合わせることで依存グラフを構成し、演算はその依存グラフを解決する形で進んでいきます。 これは数学とほぼ全てのプログラミング言語において共通の仕様です。「ほぼ全て」と留保したのは、別に作ろうと思えば一般的な数学規則としての()の規則を無視したプログラミング言語を設計することはいくらだって可能だからです。しかしメジャーな言語たとえば、CやJava、JavaScript、Haskell、Pythonでは、このグループ化演算子()の振る舞いは同一でしょう。
1 + 2 + 3 + 4 + 5
あるいは演算ルールに則って暗黙に省略されてしまっているグループ化演算子 ( ) を明示して
((((1 + 2) + 3) + 4) + 5)
という式=関数型コードについて
依存関係の矢印の向きの解釈を逆にして表現すると、さらに依存グラフらしく表現すると、こうなります。
プログラマーが関数型コードの式を組み立てていくときは、このように二項演算子()を活用して依存グラフを構成します。 JavaScriptの実行環境は、式(Expression)によって構成された依存グラフを解析し、解決していくことによってコードを実行していきます。 依存(関係)グラフはソフトウェア開発分野でありふれた概念で、パッケージマネージャーやあるいはGitHubレポジトリなどに見られます。 依存関係グラフについて
GitHubの依存関係グラフ~ 開発ワークフロー全体をセキュアに
オープンソースの利用が加速する中で、プロジェクトにはおそらく何百もの依存関係が存在します。平均でリポジトリあたり203個のパッケージ依存関係が存在することがわかっています。アプリケーションにどのような依存関係があるのか、どうすれば実際に判断することができるのでしょうか。ドキュメントまたはStack Overflowからコードを自分のコードにコピペしすることも、依存関係と言えるでしょう。 依存関係とは何か、また、GitHubの依存関係グラフでコードどのような影響があるかを確認する方法について、そして依存関係を安全に維持するために何をすべきかについて、詳しく見ていきましょう。 依存関係の把握依存関係はソフトウェアの実行に必要なバイナリです。これには、アプリケーション開発時に必要なバイナリ(多くの場合、開発依存関係と呼ばれる)と、実行時にアプリケーションの一部として実際に使用されるバイナリの両方が含まれていると考えることができます。また、スタックの他の部分にも依存関係があります。たとえば、アプリケーションがオペレーティングシステム上で実行される、などです。ただし、ここでは分かりやすくするために、これについては除外します。 依存関係は、開発者がアプリケーションの一部として指定したときに環境に取り込まれます。これは通常、依存関係が宣言されるマニフェストファイル、または特定バージョンの依存関係が指定されるロックファイルの一部として実行されます。依存関係は推移的に含まれる場合もあります。つまり、特定の依存関係を指定していなくても、自分で指定した依存関係によって指定されている場合、結果的にその依存関係に依存することになります。
これが、ネットワーク効果を介して依存関係が急速に拡大する仕組みであり、アプリケーションが何に依存しているかを識別することが難しくなる理由ともなります。これを簡単に把握できるようにする一般的な方法は、依存関係を非巡回グラフとして提示し、レビューすることです。つまり、あるバイナリから別のバイナリへの依存の方向を示すことによって、一連の依存関係を表示します。ファミリーツリーと同様に、アプリケーションには直接引き込む依存関係があり(ファミリーツリーにおけるペアレントのようなもの)、その依存関係にも独自の依存関係(ペアレント関係の上位のようなもの)があります。ただし、類似しているのはそこまでです。複数の項目がすべて1つのバイナリにつながる可能性があり、また所有する依存関係の数に制限はありません。おそらく、”ペアレント関係のさらに上位の関係”よりもさらに前に遡ることができます。
6.4. 計算不可能な巡回グラフ 有向閉路グラフ (directed cycle graph)
これが、ネットワーク効果を介して依存関係が急速に拡大する仕組みであり、アプリケーションが何に依存しているかを識別することが難しくなる理由ともなります。これを簡単に把握できるようにする一般的な方法は、依存関係を非巡回グラフとして提示し、レビューすることです。
とありますが、なぜ依存関係は、非巡回グラフというものなっているのでしょうか? 非巡回グラフでないほうの巡回グラフというのはプログラミングでもあまり耳慣れない言葉ですが、プログラマーに限らず一般の人でも聞いたことのある言葉でいうならば要するに無限ループのことです。 グラフ理論では、有向閉路グラフ (directed cycle graph)などと呼びますが、別にここでは積極的にこの用語を覚える必要はありません。ただし、概念としては常識的に理解しておく必要はあるし、実際にこういうGitHubのドキュメントでも非巡回グラフであるとか、その下敷きとなる重要概念として紹介されていたりはするので、用語が出てきたら、ああアレのことか、とわかれば、プログラマとしてもかなりのアドバンテージにはなります。
有向閉路グラフ (英: directed cycle graph) は辺に向きのある閉路グラフであり、全ての辺は同じ向きになっている。閉路のない有向グラフは有向非巡回グラフ (英: directed acyclic graph) という。
これは計算可能性理論とも密接な関係があって、チューリングマシンの停止問題や、もっと一般的に循環論法と同じことである、というのは直感的にも理解できるのではないでしょうか? 以下は、循環論法の概念図ですが、巡回グラフとまったく同じ概念であることは一目で理解できるでしょう。
無限ループ、循環論法は、巡回グラフ、有向閉路グラフ (directed cycle graph)であり、計算不可能です。 そしてこれが
依存関係グラフから、与えられた依存関係を尊重した評価順序や評価順序がないことを導き出すことができる。
のうち「評価順序がないことを導き出す」という依存関係グラフのケースです。 チューリングマシンの停止問題でも
アラン・チューリングは1936年、停止性問題を解くアルゴリズムは存在しないことをある種の対角線論法のようにして証明した。 すなわち、そのようなアルゴリズムを実行できるチューリングマシンの存在を仮定すると「自身が停止するならば無限ループに陥って停止せず、停止しないならば停止する」ような別の構成が可能ということになり、矛盾となる。
とあるように、原理的に、関数型コードであっても命令型であっても、有向閉路グラフ (directed cycle graph)は計算不可能であるので、プログラミングのコードとして成立しません。 6.5. 計算可能な有向非巡回グラフ(DAG)ただし、再帰Recursion)あるいは、もっと一般的に、自己相似Self-similarity)あるいは、フラクタル構造Fractal
もしくは、自己言及Self-referencehttps://plato.stanford.edu/entries/self-reference/
というのは、OSのディレクトリツリー構造としても普通に使われています。
あるディレクトリは別のディレクトリを含む再帰構造になっています。 循環論法 関連項目
再帰プログラミング形式的には、「ある用語の定義を与える表現の中にその用語自体が登場」しており、一見すると循環定義そのもののような構造で書かれているが、再帰で辿っていける鎖の端に具体的な値を決定できる要素が最低でもひとつ見つけられるように記述しておけば、プログラムとして立派に作動する。決定できる要素が与えられないと再帰プログラムとして成立せず、まともに動作しない。)
つまり、無限ループは計算不能になって使い物にならないが、有限ループのグラフ構造なら計算可能だし、それは、巡回グラフにはなっていない、ということです。 命令型プログラミングでは、フローの制御文のForやWhileをある種のステートメントのパターンとして繰り返し構造を表現しますが、関数型プログラミングでは、このように自然界や工学的な構造として普通に現れる、再帰Recursion)構造を強く意識して、依存グラフとして構成し、式で表現します。 巡回グラフあるいは、有向閉路グラフ (directed cycle graph)になっていない、つまり、計算可能で無限ループになってないグラフのことは、有向非巡回グラフDirected acyclic graphと呼びます。
有向非巡回グラフ、有向非循環グラフ、有向無閉路グラフ(ゆうこうひじゅんかいグラフ、英: Directed acyclic graph, DAG)とは、グラフ理論における閉路のない有向グラフのことである。有向グラフは頂点と有向辺(方向を示す矢印付きの辺)からなり、辺は頂点同士をつなぐが、ある頂点vから出発し、辺をたどり、頂点vに戻ってこないのが有向非巡回グラフである[1][2][3]。 有向非巡回グラフは様々な情報をモデル化するのに使われる。有向非巡回グラフにおける到達可能性は半順序を構成し、全ての有限半順序は到達可能性を利用し有向非巡回グラフで表現可能である。順序づけする必要があるタスクの集合は、あるタスクが他のタスクよりも前に行う必要があるという制約により、頂点をタスク、辺を制約条件で表現すると有向非巡回グラフで表現できる。トポロジカルソートを使うと、妥当な順序を手に入れることができる。加えて、有向非巡回グラフは一部が重なるシーケンスの集合を表現する際の空間効率の良い表現として利用できる。また、有向非巡回グラフはイベント間の因果関係を表現することにも使える。さらに、有向非巡回グラフはデータの流れが一定方向のネットワークを表現することにも使える。
有向非巡回グラフDirected acyclic graph, DAG)は、依存グラフ(Dependency graphのなかで依存関係のサイクル(循環依存関係)が存在しない特殊なケースであり、関数型プログラミングあるいは本稿で、依存グラフというときには、有向非巡回グラフDirected acyclic graph)のことを意味します。原理的に計算可能なのはこちらでしかないからですね。
In a dependency graph, the cycles of dependencies (also called circular dependencies) lead to a situation in which no valid evaluation order exists, because none of the objects in the cycle may be evaluated first. If a dependency graph does not have any circular dependencies, it forms a directed acyclic graph, and an evaluation order may be found by topological sorting. 依存関係グラフにおいて、依存関係のサイクル(循環依存関係とも呼ばれる)は、サイクル内のどのオブジェクトも最初に評価することができないため、有効な評価順序が存在しないという状況を引き起こす。依存グラフに循環依存性がない場合、それは有向非巡回グラフ(DAG)を形成し、トポロジカルソートによって評価順序を見つけることができる。
有向非巡回グラフはイベント間の因果関係を表現することにも使える。
というのは、関数型プログラミングの文脈でいうと、関数型リアクティブプログラミング(FunctionalReactiveProgramming)つまりFRPのことです。 演算可能な式を二項演算子やグループ化演算子()を活用して代数的に構成することは、有向非巡回グラフDirected acyclic graph, DAG)を構成することであり、これはそのままイベント間の因果関係を表現することが可能です。 実際、前述のReactHooksでも取り扱っている対象であるReactコンポーネントの状態管理には、このDAGを使うFRPの原理で実装すべきでしたが、少なくと理念としては、
Hooksは関数を受け入れますが、Reactの実用的な精神を犠牲にすることはありません。Hooksは、命令的なエスケープハッチへのアクセスを提供し、複雑な関数型またはリアクティブプログラミング技術を学ぶ必要はありません。
とFacebookの担当技術者自身が明言してしまっているのは非常に残念なことです。 FRPについては関数型プログラミングを実世界のアプリケーションで活用するために必須なので本稿の後半では詳細に説明します。 6.6. 命令型プログラミングのフローをコントロールする文(Statement)は完全に不要だった 数式でやれば良いJavaScriptの実行環境は、式(Expression)によって構成された依存グラフを解析し、解決していくことによってコードを実行する能力が数学の原理原則としてデフォルトで最初から装備されているのであれば、命令型プログラミングのフローをコントロールする文(Statement)別の言葉では、制御構造Control flow)と呼ばれる仕組みってそもそも必要??となりますよね。 命令型プログラミングのこれはいったいなんだったのか?
有向非巡回グラフDirected acyclic graph, DAG)の依存グラフ(Dependency graphを構築すれば良いのです。 実際はまだ我々は積み残した課題、あるいは謎が残っています。上の命令形フロー文を式に置き換えた関数型コード
const add = (a, b) => a + b;const sum = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce(add);
あるいは、
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce(add)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]['reduce'](add)
この二項演算子'reduce'の振る舞いのです。 6.7. 二項演算'reduce'の振る舞いの謎 都合上、長いので、このサンプルコードを
[1, 2, 3, 4, 5]['reduce'](add)
へ置き換えます。 'reduce' は、ArrayとFuncttionからObjectを生み出す二項演算子です。Array 'reduce' Function = Object 'reduce'の二項演算の仕組みは、左側のオペランドとして与えられた配列集合Array[1, 2, 3, 4, 5]を素材として並び順に従い、右側のオペランドとして与えられた二項関数 add = (a, b) => a + b によって順次演算を行います。 つまり、
'reduce'とは、まさにこのとおりの依存グラフを構成する二項演算子だったのです。 これは日本語では畳み込み、英語ではfoldとして、広く知られています。
In functional programming, fold (also termed reduce, accumulate, aggregate, compress, or inject) refers to a family of higher-order functions that analyze a recursive data structure and through use of a given combining operation, recombine the results of recursively processing its constituent parts, building up a return value. Typically, a fold is presented with a combining function, a top node of a data structure, and possibly some default values to be used under certain conditions. The fold then proceeds to combine elements of the data structure's hierarchy, using the function in a systematic way. 関数型プログラミングにおいて、fold(reduce、accumulate、aggregate、compress、inventとも呼ばれる)とは、再帰的なデータ構造を分析し、与えられた結合操作を使用して、その構成部分を再帰的に処理した結果を再結合し、戻り値を構築する高階関数のファミリーを指します。通常、フォールドには、結合関数、データ構造のトップノード、および特定の条件で使用されるデフォルト値が提示されます。フォールドはその後、データ構造の階層の要素を結合し、関数を体系的に使用していきます。
つまり、Array.reduceあるいはFoldという関数は、
命令型プログラミングでは、フローの制御文のForやWhileをある種のステートメントのパターンとして繰り返し構造を表現しますが、関数型プログラミングでは、このように自然界や工学的な構造として普通に現れる、再帰Recursion)構造を強く意識して、依存グラフとして構成し、式で表現します。
の具体例だったのです。 結合操作(combining operation)、高階関数(higher-order functions )という用語については、かなり重要なので、これも独立した章で詳細に説明していきます。 6.8. 依存グラフを構成する二項演算を最大限に活用した式の組み立て命令型コード
let sum = 0;let x = 0; for (let i = 0; i < 10; i++) { x = x + 1; console.log(x); sum = sum + x;} console.log(sum);
は、関数型コードに数学の式として洗練された理念で置き換えが可能で、
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce(add)
あるいは
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]['reduce'](add)
という式を書くことは、
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10
という式を書くことと等価で、これらのをもってまったく同じ依存グラフを構成している、ということになります。 このように、コントロールフローの文や変数を追跡しながら命令型コードを書くのではなく、依存グラフを構成する二項演算を最大限に活用した式の組み立てに注力することで、バグの入り込む余地が極小の関数型コードを書くことが実現できます。 7. 命令型コードのif文switch文は場合分けの演算式にする7.1. 命令型コードのif文 JavaScriptは、文(Statement)式(Expression)から構成されている。命令型プログラミングのコードは、forループなどの文(Statement)が中心、関数型プログラミングのコードは、それに対して、式(Expression)が中心。 関数型コードは式である。関数型プログラミングは式を組み立てていく作業。式の組み立ては依存グラフの構成。JavaScriptの実行環境は、式(Expression)によって構成された依存グラフを解析し、解決していくことによってコードを実行。 の中心となる素材が、関数二項演算子。 関数(Function)演算子(Operator)は本当は同じもの二項演算子はとてつもなく強力な記法 これが一番の大枠のまとめです。ここで、命令型プログラミングのコードは、forループなどの文(Statement)が中心、と、命令型のサンプルコードに即して書いたのですが、他に大事な制御文として、条件分岐を司るif文があります。
if 文は、指定された条件が truthy ならば文を実行します。条件が falsy なら、もう一方の文を実行することができます。
よく見るフローチャートの条件分岐ではこのようなダイアグラムのシンボルで表現されていることが多いです。
上の図はそっくり、電車の線路の分岐と同じ概念ですね。
ちょうど電車がレールにそって、分岐器(ポイント)で進行方向が変わる、というようなことであり、その分岐されたそれぞれの行き先には、それぞれの命令文が用意されており、それを実行することになる、と。 for文によるルーブの中にもこの条件分岐の概念、つまりループの流れから脱出するための条件文が含まれており、全く同じことなのですが、このような条件分岐の文を書き連ねることによりコードの流れをコントロールしようとする命令型コードの問題点は、プログラマーがいちいち「電車に乗って分岐の流れに沿って進む必要がある」ことです。頭のなかで電車に乗って、ここがポイントだからこっちに流されて行って、と簡単なときはなんとかなるでしょうが、コードがどんどん複雑になってくると、まったくなんとかならない方が多く、本当にこのポイントでこちらに流されているのだろうか?この現在自分が居る位置は本当に正しいのだろうか?というシミュレーションの正しさの検証が必要となり、それはBUGの温床になっています。 実際にブレークポイントとかバグトラッカーというのは、関数型プログラミングで利用局面はない、とは言えないまでも、ほぼほぼこういう命令型の分岐やループを含むコードの流れを確認するために利用されており、これはデバッグにおいて大変な負担です。 関数型コードではこのようなフローチャートの条件分岐の先に待機している「文(statment)を実行」という流れを制御するのではなく、あくまで式(expression)の組み立てをするだけなので、このようなif文は使いません 7.2. 関数型コードのif式遠回りのようですが、一旦、JavaScriptではない関数型プログラミングの流れが強いRustでは、if文がどうなっているのか?と確認すると、ifは文ではなく、式になっています。 if/else(Rust By Example 日本語版)
これは純粋関数型言語であるHaskellでも同様です。 7.3. 条件 (三項) 演算子をつかうJavaScriptには幸か不幸かif文をそのまま置き換えるif式というものが存在しないので、代わりに条件 (三項) 演算子を利用します。これは実質if式なのです。
条件 (三項) 演算子は JavaScript では唯一の、3 つのオペランドをとる演算子です。条件に続いて疑問符 (?)、そして条件が真値であった場合に実行する式、コロン (:) が続き、条件がfalsyであった場合に実行する式が最後に来ます。この演算子は、 if 文のショートカットとしてよく用いられます。
要するに場合分けをする演算子です。条件式があり、その結果、ある値が決定されます。 C言語の流れを汲むJavaScriptの条件 (三項) 演算子の書式は<条件式> ? <真式> : <偽式> 3項を結ぶ必要上、 (?)と (:) の2つの記号を使うが、演算子としては1つの演算子。 1つの演算子ということはプラス演算子で1+1というような演算を行っているのと同じです。コードのフローの条件分岐ではなくて、値の場合分けという演算、計算をしているのです。 たとえばtrue ? 999 : 0 と書けば、<条件式>はtrueで真なので、この式の値は999になります。 もちろんfalse ? 999 : 0 ならば、値はとなります。 三項演算子は命令型のif文と比べて可読性が悪いという誤解がありますが、改行すればいいだけで、命令型のif文による条件分岐と関数型の条件演算子の式とを対比すると、 命令型 if文
let x; if (true) { x = "true" } else { x = "false" } // x == "true"
関数型 条件演算子の式
const x = true ? "true" : "false"; // x == "true"
とかなり条件演算子の式のほうがかなり簡潔に記述できます。 命令型では、まずxという後から中身になにか値を入れる変数をletでとりあえず準備しておいて、if文によってフローを分岐させる、このサンプルコードの場合、かんたんのため条件式がtrueと決め打ちされているので、分岐先の代入命令文により x = "true" と値が代入されています。破壊的代入を行っているわけで、実際このコードでletではなく破壊的代入を明示的に禁止するconstとするとエラーになります。 関数型ではフローをコントロールするのではなく、「値の場合わけ」をします。命令型コードのような x はとりあえず空で、後から命令文によってなにか値が代入される、というフローは存在しません。最初からconstで決め打ちです。決め打ちはしますが、どの値で決め打ちするのかを条件演算子によってその場で場合分けするのです。この場合かんたんのため条件式がtrueと決め打ちされているので、x = "true" と値が決定されます。 偶数と奇数を場合分けするコードは、
const number = 3 const result = (number % 2 === 0) ? "偶数" : "奇数"; console.log(result); //奇数
となり、すべて式の組み立てと宣言で構成されています。 7.4. より洗練され強力なパターンマッチングをつかう if文条件演算子の式で簡潔に書き直せることは理解できた、では、より複雑な条件を扱うswitch文はどうする?という懸念があることでしょう。これも関数型コードでは当然のようにで構築します。 例えば、switch文を利用して、HTMLのDOMのタグの種類を判別する以下のような命令型のコードがあります。
switch (type) { case 'text': case 'span': case 'p': return 'text'; case 'btn': case 'button': return 'button';}
これは、関数型のとして(オブジェクトメソッドとしての表記ではあるが)書き直せます。
const sanitize = name => match(name) .with('text', 'span', 'p', () => 'text') .with('btn', 'button', () => 'button') .otherwise(() => name);
sanitize('span'); // 'text'sanitize('p'); // 'text'sanitize('button'); // 'button'
こういう形式を、パターンマッチングPattern Matching)と呼びます。
いくつかの高水準プログラミング言語には、多分岐の一種で、場合分けと同時に構成要素の取り出しのできる言語機能があり、パターンマッチと呼ばれている。
ts-pattern
Write better and safer conditions. Pattern matching lets you express complex conditions in a single, compact expression. Your code becomes shorter and more readable. What is Pattern Matching?Pattern Matching is a technique coming from functional programming languages to declaratively write conditional code branches based on the structure of a value. This technique has proven itself to be much more powerful and much less verbose than imperative alternatives (if/else/switch statements) especially when branching on complex data structures or on several values. Pattern Matching is implemented in Haskell, Rust, Swift, Elixir and many other languages. There is a tc39 proposal to add Pattern Matching to the EcmaScript specification, but it is still in stage 1 and isn't likely to land before several years (if ever). Luckily, pattern matching can be implemented in userland. ts-pattern Provides a typesafe pattern matching implementation that you can start using today.
より良い、より安全な条件(condition)を書きましょう。パターンマッチングを使えば、複雑な複数の条件を、単一のコンパクトな式(expression)で表現できます。コードが短くなり、読みやすくなります。 パターンマッチングとは何ですか?パターンマッチングとは、関数型プログラミング言語に由来する技術で、値の構造に基づいて条件付きコードの分岐を宣言的に記述するものです。この技術は、特に複雑なデータ構造や複数の値で分岐する場合に、命令型の代替物(if/else/switch文)よりもはるかに強力で、はるかに冗長性が少ないことが証明されています。 パターンマッチングは、Haskell、Rust、Swift、Elixir、その他多くの言語で実装されています。EcmaScript仕様にパターンマッチングを追加するためのtc39プロポーザルがありますが、まだステージ1であり、この先数年は実装されそうにありません(仮に実装されることがあったとしても)。幸運なことに、パターンマッチングはユーザーランドで実装することができます。 ts-patternは、タイプセーフなパターンマッチングの実装を提供しており、今日から使い始めることができます。
素晴らしいですね。 上の関数型コードで大枠のconst sanitize = name =>というのは、ユーザが自由に定義した関数と引数です。 switch/case文に相当する主要なパーツはmatchwithotherwiseの3つです。
match(name) .with('text', 'span', 'p', () => 'text') .with('btn', 'button', () => 'button') .otherwise(() => name);
おそらく多くの読者にとっては初めて見るAPIなので何のことかよくわからないでしょう。 ここで役立つ強力なフレームワークが、二項演算/二項演算子を構築するという関数型コードの一貫した理念です。すでに我々が確認したとおり、オブジェクト指向の表記は以下のように二項演算に変換できます。
オブジェクト指向のオブジェクトは、数学の集合(Sets)に相当し、二項演算の左側被演算子(Operand)になります。オブジェクト指向のメソッドは、数学の二項演算子(Binary operatior)に相当しており、オブジェクト指向のメソッドの引数は、二項演算の右側被演算子(Operand)になります。
'map' は、ArrayとFuncttionからArrayを生み出す二項演算子です。Array 'map' Function = Array[1,2,3] ['map'] (f) = [2,3,4]という馴染み深いケースを思い出しましょう。 このAPIは、
Array.of(5) //[5] .map(f) .map(g) .reduce(h);
に非常に近い構造をしています。 オブジェクト指向の言葉で言うと、Array.of() メソッドは、 Arrayインスタンスを生成します。数学の言葉で言うと、Array.of() 単項演算子であり、配列集合を生み出します。5 → [5]そして 'map' は、Arrayである [5] とFuncttionである f からArrayを生み出す二項演算子です。 パターンマッチングの式に当てはめて考えると、
match(name) .with('text', 'span', 'p', () => 'text')
Array.of() が単項演算子であり、配列集合を生み出したのと同様に、match()は単項演算子であり、パターンマッチングの単位となる集合を生み出します。生み出された配列オブジェクトをArray型と表現するならば、パターンマッチングオブジェクトはPatternMatch型と表現しても良いでしょう。 この場合は、String型の引数として渡ってきたnamePatternMatch型に格上げされた、ということです。これが、二項演算の左側被演算子(Operand)になります。PatternMatch.with() メソッドあるいは単に 'with' は、二項演算子です。('text', 'span', 'p', () => 'text')が、二項演算の右側被演算子(Operand)になります。二項演算の生成物は、あたらしいPatternMatch型のです。 このように、命令型コードでは漫然とswtich/case文として処理していたものが、関数型コードでは二項演算子で式を構築する、というフレームワークに統合されます。 命令型で書いていたswtich/case文のコードは、関数型のコードでは 1 + 2 = 3 1 + 2 + 3 = 6 Array 'map' f = ArrayArray 'map' f 'map' f = ArrayArray 'reduce' f = Object という二項演算の式と同様に PatternMatch 'with' pattern = PatternMatchPatternMatch 'with' pattern 'with' pattern = PatternMatchPatternMatch 'otherwise' pattern = Object という二項演算の式を組み立てる作業となります。 そして、式の組み立ては依存グラフの構成であり、依存グラフ(Dependency graph)を構成する式(関数型コード)を書いてコンピュータに解決してもらうだけなので、その他の余計なことを考慮する必要は原理的にありません。 さて、この素晴らしいパターンマッチングAPIは、前述の通りJavaScript標準では実装されていないのですが、ts-patternというライブラリで提供されています。作者の紹介ブログはBringing Pattern Matching to TypeScript 🎨 Introducing TS-Pattern v3.0です。 https://www.npmjs.com/package/ts-pattern
npmでも目下人気急上昇で、JavaScriptにパターンマッチング(Pattern Maching)機能を提供するライブラリとしては品質は傑出しています。 #StateOfJS 2020: What do you feel is currently missing from JavaScript?2020サーベイ:JavaScriptに欠けていると感じるものは?では、
Pattern MachingはTop3となっており、現在のJavaScriptプログラマーにとって特段に需要が高い機能であることが確認できます。 関数型言語としては見做されることはないPythonですら最近では結構強力なパターンマッチングが実装されているようです。Python 3.10の新機能:「構造的パターンマッチ」とは 7.5. 【余談】TC39の没落では、JavaScript言語標準としてパターンマッチング(Pattern Maching)はどうなっているのでしょうか?tc39/proposal-pattern-matching
現在State:1で、多分あと数年はかかるでしょう。というかStage:4まで到達してその後完成するのか?それ以前にTC39プロポーザル版Pattern Matchingは、どのような実装になっているのでしょう?
うーん、なんか文(Statememt)になっていますね・・・元々Pattern Matchingとは関数型プログラミングが発祥であり、式(expression)であるのが普通なのに。ts-patternのコードでは純粋に簡潔に二項演算の式として構築できることとは対照的です。
const sanitize = name => match(name) .with('text', 'span', 'p', () => 'text') .with('btn', 'button', () => 'button') .otherwise(() => name);
これは何を意味するのか?というと、ts-patternライブラリの作者はPattern Matchingがなんたるかという本質とそれによってもたらされる効能、より良い、より安全な条件(condition)を書きましょう。パターンマッチングを使えば、複雑な複数の条件を、単一のコンパクトな式(expression)で表現できます。コードが短くなり、読みやすくなります。パターンマッチングとは、関数型プログラミング言語に由来する技術で、値の構造に基づいて条件付きコードの分岐を宣言的に記述するものです。この技術は、特に複雑なデータ構造や複数の値で分岐する場合に、命令型の代替物(if/else/switch文)よりもはるかに強力で、はるかに冗長性が少ないことが証明されています。を良く理解していてその強い理念と情熱をもって実装しているのに対して、本件のTS39プロポーザルの参加者は、需要がありそうだしやってみよう、標準化されたら自分の功績にもなるし、という功名心(全員がそうではないが筆者が直接やりとりしたこともあるアカウントの参加者(あまりレベルが高いプログラマとは見做せない)を見てたらおそらくそんなところ)で、さらに背景には「JavaScriptはあくまで命令型の文の集まりとして記述されるべきだ」という信念のようなものが共有されており、switch/case文との後方互換性を維持したかったのだろうと想像します。 いずれにせよ将来、仮にパターンマッチング(Pattern Maching)がTC39によって標準化されたとしても、関数型プログラミングを指向するのであれば、ts-patternライブラリを使い続けるべきでしょう。 筆者の体感から言うと、JavaScriptの策定団体であるTC39はもはやまともに機能していません。近頃特に酷くなってきています。私見ですが、プログラミング技術界隈は、設計理念を共有する一握りの極めて強力で機動性の高い小規模の技術者集団によって大きな進歩を成し遂げています。TC39のすべてとはとても言えませんが、多くのプロポーザルでは、まっとうな設計理念を持たず技術水準がけして高いとは見受けられず、名誉欲や承認欲求だけが強いコントロールフリークがあれこれ仕様に口出す行為を大変楽しんでおられて、高い理念と技術力があるライブラリ作者などの主張正論がもはや通らなくなってしまっています。船頭多くして船山に登るの状態です。その結果、当初、高い知見でコミュニティを牽引してきたプログラマが馬鹿らしくなって将来性のあるプロポーザルから離脱してしまっています。それがこのPattern Matchingプロポーザルにもよく反映されています。 #StateOfJS 2020: What do you feel is currently missing from JavaScript?2020サーベイ:JavaScriptに欠けていると感じるものは?2位は、Standard Libraryですが、これはまさにnodejsnpmのことですね。全部、外部の誰かが本来JavaScriptに必要な標準機能を提供しているわけです。V8 JavaScriptエンジン上に構築されたランタイム環境としてデファクトスタンダードとなったnodeJS
nodeから利用できるパッケージマネージャとライブラリエコであるnpm
このように外部で実現されたnpmライブラリをWebブラウザで利用するために、最初にBrowserifyが発明され、その後、Webpackなどが登場してきました。この混乱の背景にはブラウザ戦争による標準化の困難さがあったので、TC39の没落というのは公平ではない気がしますし、現在ではES Modules(esm)が登場したので状況は良い方向へ流動的です。もっとも大きな流れはnodeのオリジナルの開発者であるRyan Dahlが、Node.jsに関する10の反省点 - Ryan Dahl - JSConf EUとして、新しいJS/TSランタイムDenoをリリースしました。https://github.com/denoland/denohttps://deno.land/
Denoはnpmから脱却し、標準化されたESModuleを利用しています。もはやWebpack系の回り道は必要ありません。 2020サーベイの1位は、StaticTypingで圧倒的です。これはまさにTypeScriptの登場と人気ぶりに直結しています。JavaScriptに型(Type)をつけて欲しいという要望は昔から強く有りましたが実現することはありませんでした。遅々としたJavaScriptの進歩に業を似やした技術者達は、それぞれ独自のAltJSと呼ばれるJavaScriptの代替となる言語を開発しました。TypeScriptはそのうちMicrosoftのアンダース・ヘルスバーグ (Anders Hejlsberg )が設計し人気を博し現在主流となったAltJSです。TypeScript/背景
TypeScriptはECMAScript 2015において期待されている機能を先取りするようなものであるともいえる。ECMAScriptの提案にないがTypeScriptに独自に搭載された機能として、静的言語解析を可能にする静的型付け機能(使用するかどうかは選択可能)がある。
#StateOfJS 2020: What do you feel is currently missing from JavaScript?2020サーベイ:JavaScriptに欠けていると感じるものは?第3位のPattern Machingに続く4位はパイプラインオペレータ(Pipeline Operator)です。結構、僅差とは言えて、このあたりが突出しているようです。 パイプラインオペレータ(Pipeline Operator)も例にもれずTC39プロポーザルがあります。tc39/proposal-pipeline-operator
現在Stage:2で、最近昇格しましたが、Brief history of the JavaScript pipe operatorJavaScriptパイプオペレータの簡単な歴史でまとめられているように、2015年より、このひとつのオペレータのために議論が紛糾しており現在かなり悪い方向へ迷走中です。パイプラインオペレータ(Pipeline Operator)は、もともと関数型言語であるF#で広く活用されてきた数学的な意味での二項演算子なのですが、非常に愚かなことに、前述の「JavaScriptは命令型の文で書かれなければならない」それが多数派だからという信念の影響で、具体的にはAsync/Await(これ自体がであるPeomise.then()のSyntaxSugar(糖衣構文)である命令型のであり、同じ信念のもとに生み出された)との相性が悪い、との理由で長らく提案され続けていたF#Styleは却下され、現在HackStyleなる醜悪なものとなりStage2に進み、コミュニティからの大反発を食らっています。関数型プログラミングコミュニティはHackStyleの偽パイプラインオペレータ(Pipeline Operator)が仮に本当に実装されても受け入れることはないでしょう。これはパターンマッチング(Pattern Maching)とまったく同じ状況と言えなくもないですが、厄介なのは、ユーザーランドレベルのライブラリで上手く提供されて活用できるパターンマッチング(Pattern Maching)とは異なりパイプラインオペレータ(Pipeline Operator)は言語レベルでハードコーディングされた二項演算子なのです。 教訓: もはやTC39はアテにならない。静的型(StaticTyping)、パターンマッチング(Pattern Matching)、パイプラインオペレータ(Pipeline Operator)、それから演算子のオーバーロード(Operator overloading)、演算子のユーザー定義(Custom Operator)など関数型プログラミングを指向する上で本当に欲しい機能がJavaScriptのネイティブ実装として提供される希望はかなり少ないでしょう。しかし、オブジェクト指向への評価の没落と反比例するように関数型プログラミングへの注目度は年々高まり続けている、それはこういうパターンマッチング(Pattern Maching)という関数型由来の技術への需要の高まりという現象を見ても明らかです。 8. 関数型プログラミングで複雑性との闘いに勝利するためのたったひとつの方法8.1. ある式を別の式に交換する
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce(add)
あるいは
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]['reduce'](add)
という式を書く このように説明すると、命令型のコードはそんな配列データ[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]ベタ書きなどしていないし、100とか1000とか10万とか大きな数になればどう適応させるつもりなのか?コードの汎用性に欠けるのではないか?という指摘がきっとあることでしょう。 それは実は結構些細なことで、関数型コードはあくまでなので、その[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]を、具体的な値ではない、より汎用度の高いに交換してしまえば簡単に解決できる問題です。 目的の配列操作のやり方は複数あるようですが、ES6 - generate an array of numbers任意の数の自然数の配列を得られるnatural関数を用意して
const natural = n => [...Array(n).keys()] .map(a => a + 1);
const add = (a, b) => a + b;const sum = natural(10).reduce(add); console.log(sum); // 55
ベタ書きだった配列の式を natural(10) という別の式へ置き換えれば良いでしょう。 このように関数型プログラミングは、コード全体が式の列挙であり、それぞれの式の一部分別の式交換可能です。 同様に、
natural(10)['reduce'](add)
において、reduce は、 配列 natural(10) 関数 add の間の二項演算を司る二項演算子という事実を考えると、左辺オペランドであるnatural(10)を別の値に交換できたのと同様に、右辺オペランドであるaddも別の関数へ交換可能です。 たとえば、加算のかわりに乗算であるmultiplyに交換して、
const multiply = (a, b) => a * b;const product = natural(4).reduce(multiply);// 1 * 2 * 3 * 4console.log(product); // 24
とすることも自由に可能です。コードの堅牢性が担保されたまま、バグの心配をせずに可能です。 8.2. 「等しい(Equality)」という超強力な概念 厳密に正しさが担保されておりバグがないコードを書く超強力な手段として、式の構築にのみ注力する関数型コードを書くことが機能する背景には、式が「交換可能」である、という概念があります。 上の事例では、[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]という具体的な値が、より抽象的で汎用的な構造であった、natural(10)という値に交換できた、さらに、natural(4)という別の値に交換してもコードが壊れないことは事前によくわかっている、addという関数はmultiplyという別の関数に交換してもコードが壊れないことは事前によくわかっている、このあたりの「コードの堅牢性の担保」があるバグのでないコードを書くにあたって大変に強力で重要です。 「交換可能」ということは、数学的にもうちょっと突き詰めて考えてみると、それは「等しい(Eqaulity)」ということです。
In mathematics, equality is a relationship between two quantities or, more generally two mathematical expressions, asserting that the quantities have the same value, or that the expressions represent the same mathematical object. 数学では、2つの量、より一般的には2つの数学的表現の間の関係で、量が同じ値を持つこと、あるいは表現が同じ数学的対象を表すことを主張することを「等価性」という。
プログラミングでは、[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] natural(10)という値の等価性もあり、もうひとつ、natural(10)という値がnatural(4)あるいは、addという関数はmultiplyという別の関数に交換可能というのは、その型(Type)=集合が等しい、という原理があります。型(Type)=集合については、後述しますが、値が等しい、型(Type)が等しい式を構築する、というアプローチは、「コードの堅牢性の担保」があるバグのないコードを書くためのたったひとつの方法と言い切っても過言ではありません。 8.3. ソフトウェア危機に対する工学的アプローチの根本的な問題ソフトウェア危機ということがありました。
ソフトウェア危機(ソフトウェアきき、Software Crisis)とは、高性能化するハードウェアのコストは低下する一方、複雑化するソフトウェア開発のコストは上昇する傾向が続くことにより、将来的にソフトウェアの供給が需要を満たせなくなるという考え方である。この用語は、ソフトウェア工学がまだ十分に確立していなかった頃によく使われた。その根本には、正しく、可読性が高く、検証可能なコンピュータプログラムを書くことが困難であるという事情がある。
ソフトウェア危機の解決手法ソフトウェア危機は(少なくとも一部は)様々な手法や方法論の開発によって解決されてきつつある。 オブジェクト指向、カプセル化構造化プログラミングガベージコレクションアジャイルソフトウェア開発マルチスレッド・プログラミングソフトウェアコンポーネントソフトウェアプロトタイピングデザインパターン統合開発環境、RADバグ管理システム、バージョン管理システム反復型開発Model View Controllerしかしフレデリック・ブルックスが『銀の弾などない』で記している通り、「本質的な複雑性」に対して生産性を向上させるような技法は存在しないと言われている。
この、フレデリック・ブルックスによる『銀の弾などない』(No Silver Bulletというのは、巷でやたらに引用されています。
『銀の弾などない— ソフトウェアエンジニアリングの本質と偶有的事項』(ぎんのたまなどない ソフトウェアエンジニアリングのほんしつとぐうゆうてきじこう、英: No Silver Bullet - essence and accidents of software engineering)とは、フレデリック・ブルックスが1986年に著した、ソフトウェア工学の広く知られた論文である。 原論文は英語である。日本語では『銀の弾丸はない』と、翻訳されることもある。ブルックスは、「銀の弾丸」(Silver Bullet)として、魔法のように、すぐに役に立ちプログラマの生産性を倍増させるような技術や実践 (特効薬) は、今後10年間(論文が著された1986年の時点から10年の間)は現れないだろう、と記載した。 銀の弾とは、銀で作られた弾丸であり、西洋の信仰において狼人間、悪魔を撃退する際に用いるものとされていた。 ブルックスの警句は、非常に多く引用されており、生産性、品質、制御に適用されている。ブルックスは、自身の警句で述べているプログラマの生産性の限界は「本質的な複雑性」(essential complexity)についてのみ当てはまると述べているのであり、「偶有的な複雑性」(accidental complexity)に対する挑戦については支持している。ブルックスは、偶有的な複雑性については著しい改善(おそらく今後10年間で10倍以上)がみられるだろうと述べている。 ブルックスは、この論文で本質的な複雑性に対処するために、次のことを提案している(詳細は#提案を参照) 購入できるものをあえて構築しないようにするために大市場を利用する。ソフトウェア要件の確立に際し、開発循環計画の一部として、迅速なプロトタイピングを使用する。実行と使用とテストが行われるにつれて、より多くの機能をシステムに追加しながら、ソフトウェアを有機的(系統的)に成長させる。若い世代の素晴らしいコンセプトデザイナーを発掘し、育てる。
『銀の弾などない』(No Silver Bulletという主張内容については、2021年現在でもソフトウェア業界で広く引用されるわりには、現在の知見で本当にまだ権威として引用できるほどなのか?と考えてみると個人的にはまったく評価していません。 おそらくブルックスの主張はオブジェクト指向全盛にむけた時代においては、たしかにそれは宣伝されるほどには銀の弾丸ではなかった、という意味では正しいです。 オブジェクト指向をはじめとする、数学の範疇外での余剰の工学的発明は複雑さの元凶でしかないという意味では正しい。 デザインパターンModel View Controller というのも、まるでソフトウェア開発における冴えた知見の結晶のように持ち上げられることがありますが、これらは数学の範疇外の余剰の工学的発明で、本質的には複雑性を増しているにすぎず、筋の悪い間違った工学的アプローチです。 そして、ブルックスがいう「本質的な複雑性」とは数学のことなど語っているわけではまったくないので、彼の主張は的外れであると言えます。 8.4. 『銀の弾丸』等しくすれば組合せ爆発(Combinatorial explosion)を回避可能『銀の弾などない』(No Silver Bulletというのは誤りで、『銀の弾』は実は存在していて、それは「等しい(Equality)」という超強力な概念に他なりません。 ソフトウェア開発、あるいはプログラミングにおける「本質的な複雑性」(essential complexity)とは、ほんとうは数学的な概念で、それは、組合せ爆発Combinatorial explosionです。
組合せ爆発(くみあわせばくはつ、英: combinatorial explosion)は、計算機科学、応用数学、情報工学、人工知能などの分野では、解が組合せ(combination)的な条件で定義される離散最適化問題で、問題の大きさn に対して解の数が指数関数や階乗などのオーダーで急激に大きくなってしまうために、有限時間で解あるいは最適解を発見することが困難になることをいう。
念の為ですが、解決したい課題の対象が本質的に原理的に内在している組み合わせ爆発のことではなく、プログラマが書いてしまいがちで、不注意な、あるいは、不適切な工学的アプローチによるコード自体が本質的には回避可能であるのにもかかわらず、非合理的に、無用に、組みわせ爆発を起こしている、ということです。 無用な組合せ爆発を回避するためには、可能な限り要素を減らすこと、そして何より肝要なことは、本質的に等しいものは、等しいものとして徹底的に扱うこと、さらに必ずしも等しくなければならない要請はなくても、等しいものとして扱うことができるならば等しくする、ということです。 要素が1つしかなければ、組み合わせというのが原理的に存在しないので、組合せ爆発による問題など起こりようがありません。しかし、それぞれの要素に区別があるときは、指数関数的に組み合わせ爆発が起こり収集はつかなくなります。 数学の範疇外での余剰の工学的発明は複雑さの元凶でしかないのですが、裏を返すと「唯一正しい工学的アプローチ」『銀の弾丸』とは、組合せ爆発を回避する「等しくする」という数学的アプローチです。実際これはソフトウェア開発に限らない一般的な工学的な問題として死活問題なので、工業規格という人類の経験則による知見が存在しています。ISO規格(国際標準化機構)があり、JIS(日本)など各国に規格が決定されています。 身近なところでは、電気のコンセントプラグがあり、特にプログラマにとっては、USB規格があります。
ユニバーサル・シリアル・バス(英: Universal Serial Bus、略称:USB、ユーエスビー)は、コンピュータ等の情報機器に周辺機器を接続するためのシリアルバス規格の1つ。ユニバーサル(汎用)の名の示す通り、ホスト機器にさまざまな周辺機器を接続するためのペリフェラルバス規格であり、最初の規格となるUSB 1.0は1996年に登場した。現在のパーソナルコンピュータ周辺機器において、最も普及した汎用インターフェース規格である。
もちろん、Appleのように独自の規格、つまり自前機器のエコだけで完結すれば良いだろう、というのもひとつの主張ではありますが、最近排除されて消費者に歓迎された事案は記憶に新しいところです。EU スマートフォンの充電器をUSB Type-Cへ統一へ(iPhoneのLightningケーブル排除へ) USBなどを使っているとわかりますが、こういう「等しさ」が担保されている概念はBuilding blockになります。その定められた規格内に収まってさえおれば、それらは「等しい」と見做され、自由自在に組み合わせても壊れる心配がありません。LEGOもそれ自体がBuilding blockであり単一の接続規格で、自由自在に構築可能です。
そして、関数型コードの式においては、実際に、
const multiply = (a, b) => a * b;const product = natural(4).reduce(multiply);// 1 * 2 * 3 * 4console.log(product); // 24
reduceという二項演算子の左右オペランドはあらかじめ型(Type)が規定(規格化)されているので、規格に合致してさえおれば、別の値、式に置き換えても全く問題がないことは予め保証されています。逆に、TypeScriptで、タイプエラーが出るということは、間違った型、間違った規格の組み合わせを行ったということになります。 そして、繰り返しとなりますが、二項演算子というのは、関数のことなので、根源的には関数には厳密な型がある、ということにほかなりません。この事実については、後述する関数それ自体の解説の章で詳説します。 根源的には、コードの要素を等しく保つというアプローチで、コードをUSBデバイスやLEGOブロックのように自由自在に組み合わせてもコードは壊れないと担保される、バグのでないメンテナンス性の高いコードをプログラミング可能となります。 実際にこれだけJavaScriptに型付けするTypeScriptが絶大な支持を得ており、Rustなどモダンな言語でも型があたりまえな現状において、型(Type)の等しさを担保することは、銀の弾丸(SilverBullet)だ、と主張されることは滅多にありません。おそらく、ただ『銀の弾などない』(No Silver Bulletとさえ発言しておけば、有る一定の論争がある場所で曖昧な玉虫色の決着をつけることに有用な方便としてだけ乱用されており、そこそこ達観した知見のあるポーズを取ることができるからだろうと想像するしかありませんが、現代においては、あまり意味のない言及です。 型(Type)とは、等しくする効用の対象として顕著な事例ですが、「等しい(Equality)」という概念の超強力さはもっと一般的なもので他にも色々あるのでそれについては別に後述します。とりあえず当面は型のことを説明していきましょう。 9. 型(Type)は何故そんなに役に立つのか?そもそも型(Type)とは何か?9.1. JavaScriptコミュニティでは型(Type)が圧倒的に重要視されている #StateOfJS 2020: What do you feel is currently missing from JavaScript?2020サーベイ:JavaScriptに欠けていると感じるものは?では、
Static Typing静的型付け)が圧倒的な需要があります。 その根本的な理由は、
プログラミングでは、[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] natural(10)という値の等価性もあり、もうひとつ、natural(10)という値がnatural(4)あるいは、addという関数はmultiplyという別の関数に交換可能というのは、その型(Type)=集合が等しい、という原理があります。型(Type)=集合については、後述しますが、値が等しい、型(Type)が等しい式を構築する、というアプローチは、「コードの堅牢性の担保」があるバグのないコードを書くためのたったひとつの方法と言い切っても過言ではありません。
関数型コードの式においては、実際に、
const multiply = (a, b) => a * b;const product = natural(4).reduce(multiply);// 1 * 2 * 3 * 4console.log(product); // 24
reduceという二項演算子の左右オペランドはあらかじめ型(Type)が規定(規格化)されているので、規格に合致してさえおれば、別の値、式に置き換えても全く問題がないことは予め担保されています。逆に、TypeScriptで、タイプエラーが出るということは、間違った型、間違った規格の組み合わせを行ったということになります。
根源的には、コードの要素を等しく保つというアプローチで、コードをUSBデバイスやLEGOブロックのように自由自在に組み合わせてもコードは壊れないと担保される、バグのでないメンテナンス性の高いコードをプログラミング可能となります。
ということです。9.2. 巷によくある、よくわからない、型(Type)の説明 多くのプログラマーは多かれ少なかれ「どうやらプログラミングの世界には型(Type)があって、それをコードの中で明示的に宣言するかしないかが、ときどき大きな議論になっているようだ」というところから型(Type)の概念に遭遇することでしょう。
JavaScriptは動的型付け言語(dynamically typed language)です。
動的型付け(どうてきかたづけ、英: dynamic typing)とは、プログラミング言語で書かれたプログラムにおいて、変数や、サブルーチンの引数や返り値などの値について、その型を、コンパイル時などそのプログラムの実行よりも前にあらかじめ決めるということをせず、実行時の実際の値による、という型システムの性質のことである。 また、そのような性質の言語を、動的型付き言語(どうてきかたつきげんご、英: dynamically typed language)という。これに対し、型は実行前に決まる、というのが静的型付けである。型推論を利用していて、構文上は型の記述が省略可能な言語もあるが、そういった言語も静的型付けである(MLなど)。
TypeScriptは静的型付け言語(statically typed language)です。
静的型付け(せいてきかたづけ、英: static typing)は、プログラミング言語で書かれたプログラムにおいて、変数や、サブルーチンの引数や返り値などの値について、その型が、コンパイル時など、そのプログラムの実行よりも前にあらかじめ決められている、という型システムの性質のことである。 また、そのような性質の言語を、静的型付き言語(せいてきかたつきげんご、英: statically typed language)という。これに対し、型は実行時の実際の値による、というのが動的型付けである。型推論を利用していて、構文上は型の記述が省略可能な言語もあるが、そういった言語も静的型付けである(MLなど)
では、その型(Type)というものは何なのか?Wikipediaなどで調べてみると、 データ型
データ型(データがた、data type)とはコンピュータプログラミングや計算機科学において、データ値の性質を定義するための表現であり、コンパイラやインタプリタにそのデータ値の用法を宣言するための機能である。単に型(かた、type)とも呼ばれる。データ型は、関数や演算子に対するデータ値の適用の可否や、変数に対するデータ値の代入や束縛の可否を判定して、式やステートメントによる計算実行を制限する。データ型を基準にした計算可能性を規則化する形式手法は、型システムと呼ばれている。データ型は型理論視点の値の集合と同義になり、式・関数・変数の評価後の代数としても用いられる。ほとんどのプログラミング言語は、整数型・浮動小数点型・論理型・文字型といった基本データ型を備えており、また一定のデータ構造に付与される複合データ型を備えている。言語によってはポインタ・シンボル・式・関数にもデータ型は付与される。
型システム
型システム(英: type system)は、計算機科学およびコンピュータプログラミング分野において、主にデータを扱うための数々の規則の集合から成立した形式体系であり、プログラム内の様々な要素に型(type)と呼ばれる特性を付与する仕組みである。型を付与される対象としては基本的なデータ値のほか、変数、式、関数、オブジェクト、モジュールなどが挙げられる。型の付与は、データの分類を成立させてデータ型を表現するほか、それらデータ型の規則的な集合を表わしたデータ構造の形式的分類も成立させる。型システムの意義は、プログラム要素を分類し、各分類の識別法則の一貫性および分類間関係の整合性を保証して、フォールトアヴォイダンスの視点からプログラムエラーの発生を抑止することにある。型システムは数学基礎論または数理論理学で扱われる型理論に基づいて構築されたコンピュータプログラミング用の形式手法である[1]。型とはデータ型」も参照プログラミング言語はさまざまな値を扱う。代表的かつ最も原始的なものは数値や文字列だが、一般的に有限の資源制約があるコンピュータにとって都合のよい内部表現が使われ、例えば数値には32ビットや64ビットといった固定サイズの整数型や浮動小数点数型が、文字列には特定の文字コード集合によって符号化された整数値の羅列(文字配列)が使われることが多い。文字列の表現には最後の文字(番兵)に0を使用するゼロ終端文字列(ヌル終端文字列)が使われることもあれば、長さ情報を別途整数値で保持する複合データ構造が使われることもある。三角関数は浮動小数点数を引数にとり浮動小数点数を返す。先頭の文字を大文字にする関数は文字列を引数にとり文字列を返す。ユーザーからの入力を数値として扱うためには、文字列を解釈して数値を返す関数が必要である。ここで、3.14 や "hoge" といった値について「浮動小数点数」や「文字列」といった種類に分類して扱っているが、同じ種類の値であれば同じ操作(演算)が可能である。この「値の種類」が型(データ型)である。
初学者というか多くの、ほとんどのプログラマーが型(Type)とは何か?と概念を習得したいときに、この説明でわかるとは到底思えません。もしかしたら説明している当事者が事象の表面をなぞって見せているだけで本当のことは何もわかっていない危険性が大きいです。 9.3. わかりやすい、型(Type)の説明 型(Type)とは集合Set)のことです。
数学における集合 (しゅうごう、英: set, 仏: ensemble, 独: Menge) とは、大雑把に言えばいくつかの「もの」からなる「集まり」である。集合を構成する個々の「もの」のことを (げん、英: element; 要素) という。 数学において、集合とは要素の集まりである。集合を構成する要素は、数、記号、空間上の点、直線、その他の幾何学的形状、変数、さらには他の集合など、あらゆる種類の数学的対象となりうる。集合は有限個の要素を持つこともあれば、無限個の要素を持つこともある。2つの集合が等しいのは、正確に同じ要素を持つ場合に限られる。
ブリタニカ百科事典
集合とは、数学や論理学において、対象物(要素)の集まりのことで、数学的なもの(例えば、数や関数)である場合もあれば、そうでない場合もある。集合の直感的なアイデアは、おそらく数のそれよりも古いものである。例えば、動物の群れのメンバーは、袋の中の石と一致させることができるが、どちらの集合のメンバーも実際には数えられない。この概念は無限にまで広がる。例えば、1から100までの整数の集合は有限であるが、すべての整数の集合は無限である。
Type theory versus set theory型理論 vs 集合論
Alternately, we could change our terminology so that what we have been calling “types” are instead called “sets”.あるいは、これまで「型」と呼んでいたものを「集合」と呼ぶように用語を変更することもできます。 Thus, words like “type” and “set” and “class” are really quite fungible. This sort of level-switch is especially important when we want to study the mathematics of type theory,このように、「型」や「集合」や「クラス」などの言葉は、実はかなり代替可能です。このようなレベルの切り替えは、型理論の数学、つまり型理論の数学を研究するときには特に重要です。
以上は、nLab記事からの引用文です。
nLab は、数学・物理学・哲学の研究レベルの内容について扱ったウィキである。nLabはMathOverflowにおいて、質問前にチェックするべき標準的なオンラインの数学文献の1つとしてリストされている。多くの質問と解答が、nLabを背景資料として用いている。また、nLabはバイエズがアメリカ数学会(American Mathematical Society)に投稿した数学ブログのレビュー記事の中で言及されている2つのウィキのうちの1つである。
Types as Sets集合としての型
One of the most important techniques in Elm programming is to make the possible values in code exactly match the valid values in real life. This leaves no room for invalid data, and this is why I always encourage folks to focus on custom types and data structures.In pursuit of this goal, I have found it helpful to understand the relationship between types and sets. It sounds like a stretch, but it really helps develop your mindset! Elmプログラミングで最も重要なテクニックの一つは、コード内の値を実際に有効な値と正確に一致させることです。これでは無効なデータを残す余地がないので、私は常にカスタム型やデータ構造に注目することを推奨しています。この目的を追求するにあたって、型(types)と集合(sets)の関係を理解しておくことが役立つことを私は発見しました。余計なことのように聞こえますが、それは貴方のマインドセットの形成に本当に役立つのです!
集合(Sets)型(Type)は値の集合(Set)と捉えることができる Bool は集合 { True, False }Color は集合 { Red, Yellow, Green }Int は集合 { ... -2, -1, 0, 1, 2 ... }Floatは集合 { ... 0.9, 0.99, 0.999 ... 1.0 ... }String は集合 { "", "a", "aa", "aaa" ... "hello" ... } つまり、型(Type)の文脈で x : Bool というとき、それは、x は集合 { True, False } の要素というのと同じです。
型(Type)をこのような集合(Set)として考えることは、ある言語が人によって「簡単」「制限がある」「間違いやすい」と感じる理由を説明するのにも役立ちます。例えば Java - BoolやStringといったプリミティブな値があります。そこから、異なる型のフィールドを固定的に持つクラスを作ることができます。これはElmのレコードによく似ていて、カーディナルを掛け合わせることができます。しかし、足し算をするのはなかなか難しい。サブタイピングを使えばできますが、かなり手の込んだ作業になります。つまり、Elmでは簡単なResult Bool Colorも、Javaではかなり大変なのです。5のカーディナリティを持つ型を設計するのは非常に難しく、しばしばトラブルの価値がないように思われるため、Javaを「制限的」と感じる人もいると思います。 JavaScript - ここでも、BoolやStringといったプリミティブな値があります。ここから、動的なフィールドセットを持つオブジェクトを作成することができ、カーディナリティを増やすことができます。これはクラスを作るよりもはるかに軽量です。しかし、Javaのように、加算を行うことは特に簡単ではありません。例えば、Maybe Intをシミュレートするには、{ tag: "just", value: 42 } や { tag: "nothing" }のようなオブジェクトでMaybe Intをシミュレートすることができますが、これは実際にはカーディナリティの掛け算です。これでは、実際に有効な値のセットと正確に一致させるのはかなり困難です。このように、カーディナリティが(∞×∞×∞)の型を設計するのは非常に簡単で何でもカバーできるので、JavaScriptを「簡単」と感じる人がいる一方で、カーディナリティが5の型を設計するのは実際には不可能で、無効なデータのためのスペースがたくさんあるので、JavaScriptを「エラーが多い」と感じる人もいるのではないでしょうか。 面白いことに、いくつかの命令型言語にはカスタム型があります。Rustがその良い例です。Rustでは、CやJavaから得られる直感に基づいて、これらをenumと呼んでいます。そのため、RustではElmと同じように簡単にカーディナリティを追加することができ、同じような利点があります。 ここで言いたいのは、型の「追加」は一般的に非常に過小評価されているということです。「集合としての型」として考えることで、ある言語設計がなぜある種のフラストレーションを生むのかを明確にすることができます。
これは、TypeScriptではなく、Elm言語作者によるドキュメントなのですが、型(Type)の知見を深めたいとき、示唆に富んでいる内容なので、Google翻訳なりを活用しながらでも全文を読むことをお勧めします。 9.4. スーパーイディオム分野にまたがって、基本、同じ概念を違う用語で表現しています。
型理論Type theory型(type)関数(function)/ 写像(map)
集合論Set theory集合(set)始集合(domain)と終集合(codomain) 写像(map)
解析学Analysis定義域(domain)と値域(range)と終域(codomain)関数(function)
代数学Algebra集合(set)/ 被演算子(operand)演算子(operator)
オブジェクト指向Object-oriented programmingオブジェクト(object)メソッド(method)
圏論Category theory対象(object)/ 圏(category)射(morphism)
情報処理・プログラミングデータ(data)処理(operation)
このなかで、最も根底となる、すべての概念の包括的な枠組みが圏論Category theory)で、これまで永らく数学の基礎であった集合論でさえも圏論の言葉で再定義されます(集合の圏category of sets)における射(morphism)関数(function)になる)。 型(Type)とは集合Set)のこと。関数(Function)演算子(Operator)は本当は同じもの。オブジェクト指向のオブジェクトは、数学の集合(Sets)に相当し、二項演算の左側被演算子(Operand)になります。オブジェクト指向のメソッドは、数学の二項演算子(Binary operatior)に相当しており、オブジェクト指向のメソッドの引数は、二項演算の右側被演算子(Operand)になります。 と既に明確にしたとおり、本稿の方針は、二項演算子(Binary operatior)を最大限に活用することなので、このなかでどの分野の言葉をもってコードを書いていくのか?となると、当然、代数学Algebraになります。 しかし同時に明確にしたとおり、その二項演算子は、関数(Function)と同じものであり、さらに集合論写像(map)と同じものであるので、なにはともあれ、そこの基礎を固める必要があります。 10. 関数(Function)10.1. 関数の具体例 関数Functionは、2つの型(Type)集合Setの間の一方向の関係として定義されています。矢印があるのは一方向の関係である、ということを如実に示しています。 ここでは、都市(首都)という2つの集合の間の一方的な関係を定義しています。 「別に何も一方向じゃなくても良いんじゃない?双方向の矢印でも・・・」という自由な発想はもちろんアリですが、そうなると、二項関係Binary relation)という、もっと一般的な(ゆるい)数学的概念になります。 二項関係にはいろんな種類があるのですが、そのうち、あくまでこういう一方向で答えがバシッと出てくる保証がある関係を関数関係あるいは単に関数Functionと呼ぶわけです。逆方向については何の保証もありません。逆方向にについて何か保証がある関係の二項関係というのはもちろん存在しています。 例えば、色がついている自由な図形という集合があります。色関数で、の集合の要素には、必ずひとつに対応しますが、逆方向に対応することは保証されていない、そういう一方通行の関係が関数です。
こういうのは、しばしばブラックボックスのように抽象化されて、 単項関数(Unary function)なら、
一項演算子は1引数の関数(Unary function)と対応するのがわかります。
!(true) // falsef(true) // false
単項演算Unary operation)とおなじものであり、 二項関数(Binary function)なら、
これは、二項演算Binary operation)となります。 つまり、関数は一方通行で、必ずなんらかの答えが出てくる、というのは、二項演算 1 + 2 = 3 と必ず答えが出てくる、という事実と直結しています。 こういう二項演算というものは算数の計算なのだから答えが出てくるのが当たり前、という日常生活の刷り込みがありますが、そもそも演算の対象となっている集合(被演算子)というのは、わかりやすい、型(Type)の説明で示したとおり、別に日常生活でいうところの数字である必要もなく、このようにでも都市でもなんでも良いのですから、いかなる概念であっても、関数で一方向へ演算している、ということになります。 首都関数を使う