これがモナドに貢献する私の試みです-あなたがおそらく他のどこにも見つけたことがない初心者です。
モナドは、関数型プログラミングにおける非常に構成可能な単位(プログラミングの構成要素の一種)です。
(IMO、コンテキストと合理化なしで「モナド法」を導入することは、概念を理解するための役に立たない分類と危険にすぎません。心配しないでください。私はこの記事の後半で仕事をします。)
ほとんどの場合、オブジェクト、関数、リストなど、さまざまな種類のプログラミングの構成要素があります。
プログラミングのブロックの種類を持っていることは自然の法則であり、実用的な目的のための柔軟なプログラミングには避けられないようですが、実際には、ブロックの種類を持っていることはプログラミング環境汚染の主な原因の1つです。
さまざまなブロックを使用してブロックを構築することは、複雑な作業です。プログラマーは、あらゆる状況でさまざまなブロックの中から非常に賢明にブロックを選択する必要があり、長いスパンでは失敗します。
したがって、状況に応じてブロックの種類を選択することはお勧めしません。代わりに、普遍的に標準化されている特定の事前に選択されたブロックを常に使用することをお勧めします。
実際、この知恵は最近のPCの世界では一般的です。
USB(Universal Serial Busの略)は、パーソナルコンピュータとその周辺機器間の接続、通信、および電源供給のためのケーブル、コネクタ、およびプロトコルを定義するために開発された業界標準です。
適切に設計された、普遍的に標準化されたビルディングブロックを取得すると、多くの問題が解消されます。
- オブジェクトは(以前は)1つです。
- 機能は1つです。
- モナドがその1つです。
- 仕様
- 実装
- 検証
1.OOP
オブジェクト指向プログラミング(OOP)は、「オブジェクト」の概念に基づくプログラミングパラダイムであり、フィールドの形式でデータを含めることができます。これは、属性と呼ばれることもあります。プロシージャの形式のコード。多くの場合、メソッドとして知られています。オブジェクトの機能は、オブジェクトのプロシージャが、関連付けられているオブジェクトのデータフィールドにアクセスし、多くの場合変更できることです(オブジェクトには「this」または「self」の概念があります)。OOPでは、コンピュータープログラムは、相互に作用するオブジェクトから作成することによって設計されます。OOP言語にはかなりの多様性がありますが、最も人気のあるものはクラスベースです。つまり、オブジェクトはクラスのインスタンスであり、通常はクラスのタイプも決定します。
普遍的に標準化されたビルディングブロックとしてオブジェクトを選択し、プログラマーはメンバーの値と関数を含む基本クラスを準備し、ブロックのバリエーションを取得するために、継承が使用されます。
OOPのアイデアは、実際の物理オブジェクトを使用して説明されることが多く、パラダイム自体は数学的抽象化に弱いです。
たとえば、関数(またはメソッド)はオブジェクトに従属し、関数はファーストクラスのオブジェクトである必要はありません。これは当然のことです。パラダイムは元々、適切に設計された普遍的に標準化されたビルディングブロックとしてオブジェクトを選択したからです。
標準化されたビルディングブロックとして機能がオブジェクトに従属するエンティティであり、両方の役割が厳密に異なるという観点は、物理的な世界での工学的な意味から来ています。プログラミングが実際に存在する数学的抽象化ではありません。
OOPの根本的な問題は、オブジェクトが適切に設計された、普遍的に標準化されたビルディングブロックではないことが判明したことです。関数型プログラミングまたはモナドは、強力な数学的背景を持つより良い代替手段です。
2.関数型プログラミング
関数型プログラミングとは、関数を構成することです。
それは簡単だと言っていますが、これはプログラミングの歴史のかなりの成果です。
プログラミングの長い歴史を学ぶのではなく、個人的な歴史を共有したいと思います。
私はバージョン1.0からC#(OOP)プログラマーでしたが、全体的には満足していましたが、何か非常に悪いと感じましたが、それが何であるかはわかりませんでした。
私は後にJavaScriptプログラマーになり、初期の頃は次のように書いていました。
function add1(a) {
return a + 1;
}
ある日、「JavaScriptでは関数も値だ」というWeb記事を読みました。
この事実は私にとって非常に驚くべきことであり、私のプログラミングスキルへの突破口です。
それまでは、値が値であり、関数が関数であることは明らかです。どちらも、異なる領域ではまったく異なるエンティティです。
もちろん、C#1.0はすでにデリゲートを実装しており、イベントの内部メカニズムに関するものであることを少し理解しています。結局のところ、C#は主要なOOP言語であり、少なくともバージョン1.0では、関数型プログラミングにとっては非常に醜いものでした。
JavaScriptでは、関数も値です。JavaScriptの関数はファーストクラスのオブジェクトなので、他の関数を引数として受け取るか、結果として返すことができる関数を定義できます。
だから、今、私はこれを書きます:
const add1 = x => x + 1;
const add2 = x => x + 2;
[1, 2, 3].map(add1); //[2,3,4]
[1, 2, 3].map(add2); //[3,4,5]
また
const plus = (x) => (y => x + y);
plus(1)(5); //6
実際、これは私がC#プログラミングでひどく必要としていたものであり、私が感じた非常に間違ったものでした。
これは関数合成と呼ばれ、プログラミングの制約を解放するための真の秘密です。
つまり、JavaScriptの関数は一流のオブジェクトであり、よく設計された普遍的に標準化されたビルディングブロックのようです。これからは、これを「高度に構成可能なユニット」と呼びましょう。
関数はBEFORE => AFTER
です。
基本的な考え方は、関数を作成することです。
写像の合成に焦点を当てるときは、のさまざまな合成だけを気にしますBEFORE => AFTER
。
機能合成に焦点を当てるときは、コードの上から下に流れるフローチャートや、場合によってはループするフローチャートを忘れてください。
フローチャートコーディングは命令型プログラミングと呼ばれ、一般的に言えば、バグが多く複雑すぎます。OOPはこのスタイルになる傾向があります。
一方、関数型プログラミングは自動的にプログラミングスタイルをDeclarative_programmingに導き、一般的に言えば、バグがなく、デバッグも簡単ではありません。
フローの追跡と制御は困難ですが、構成の追跡と制御はかなり簡単です。プログラマーはフローを制御するのではなく、関数を作成する必要があります。
3.モナド
ちなみに、ここではHaskellコードは使用しません。
ほとんどの人にとって、モナドのことを理解する上での大きな障害は
- モナドを学ぶためには、初心者はHaskellのコードと用語に慣れている必要があります。
- Haskellのコードと用語に慣れるためには、初心者はモナドを学ぶ必要があります。
これは「鶏が先か卵が先か」です。問題。必ず避けてください。
そうは言っても、この記事の冒頭で述べたように、モナドの知識を共有するために、最初に「モナド法」を引用することもばかげているようです。
人々は彼らがすでに知っていることに基づいてのみ学ぶことができます。
それでは、JavaScriptコードに戻りましょう。
関数は非常に構成可能な単位のように見えますが、これはどうですか?
console.log("Hello world!");
これは最も単純なJSコードの1つであり、確かに関数です。
ChromeBrowserでF12キーを押し、開発者コンソールにコードをコピーして貼り付けます。
Hello world!
undefined
OK、コードは「Helloworld!」を表示するタスクを実行しました。ただし、コンソールでは、console.log
関数の戻り値はですundefined
。
関数を作成するために、状況は不快です。不快な機能。
一方、快適な機能があります。次のコードを調べてみましょう。
const add1 = x => x + 1;
[1, 2, 3].map(add1); //[2,3,4]
JavaScriptの配列は、関数型プログラミングの世界ではかなりうまく動作します。
[1, 2, 3].map(add1) //[2,3,4]
示す:
Array
Function=>
Array
関数の入力と出力は同じタイプです:Array
。
数学的構造は全体を通して同一ですBEFORE => AFTER
。
一貫性とアイデンティティの性質は美しいです。
USBインターフェースとの興味深い類似性は、当然のことながらアイデアを導きます:
Array
Function=>
Array
Function=>
Array
Function=>
Array
..。
JavaScriptコードの場合:
[1, 2, 3]
.map(add1) //[2,3,4]
.map(add1) //[3,4,5]
.map(add1);//[4,5,6]
コードは、配列レルムに入ると、出口は常に配列レルムになることを示唆しているため、ある意味で出口はありません。
配列レルムは自己完結型の世界であるため、関数型プログラミングで代数のようなことを行うことができます。
私たちが持っているとき:
Array.map(F).map(F).map(F)...
JavaScript配列固有の構文を考えると、たとえば、 Babel.map(F)
などのトランスパイラーを利用することで、より簡潔な構文に置き換えることができます。
したがって、に置き換え.map(F)
ます*F
:
Array*F*F*F...
これは代数のように見えます。
非常に構成可能なユニットを取得することで、プログラマーは代数のようなコードを書くことができます。これは重要であり、非常に真剣に研究する価値があることを意味します。
代数では、
a
= 0+a
= 0+0+a
= 0+0+0+a
また
a
= 1*a
= 1*1*a
= 1*1*1*a
0
+(加算)演算では、
a + 0 = a //right identity
0 + a = a //left identity
1
*(乗算)演算では、
a ∗ 1 = a //right identity
1 ∗ a = a //left identity
単位元と呼ばれます。
代数では、
1 + 2 + 3 = 1 + 2 + 3
(1+2) + 3 = 1 + (2+3)
3 + 3 = 1 + 5
6 = 6
結合法則と呼ばれます
number + number = number
number * number = number
string + string = string
"Hello" + " " + "world" + "!"
= "Hello world" + "!"
= "Hello "+ "world!"
は連想的であり、単位元は""
です。
では、関数型プログラミングの単位元とは何ですか?
何かのようなもの:
identityF * f = f = f * identityF
関数型プログラミングの結合法則はどのようなものですか?
const add1 = x => x + 1;
const add2 = x => x + 2;
const add3 = x => x + 2;
何かのようなもの:
add1 * add2 * add3
= (add1 * add2) * add3
= add1 * (add2 * add3)
また
(add1)(add2)(add3) = (add1)(add2)(add3)
((add1)(add2))(add3) = (add1)((add2)(add3))
(add3)(add3) = (add1)(add5)
(add6) = (add6)
関数型プログラミングは、すべて関数の合成に関するものです。
関数型プログラミングに必要なのは
function * function = function
もちろん、JavaScript(または他の言語)では、すべての言語の構文の制限のため、上記の正確な形式を書くことはできません。
実際、「代数JavaScript仕様」(JavaScriptの一般的な代数的構造の相互運用性に関する仕様)を持つことができます。

では、JavaScript配列はいわゆるモナドですか?
いいえ、でも閉じます。JavaScript配列はFunctorとして分類できます。
モナドはファンクターの特別な形式であり、いくつかの追加の性質があります(より多くのルールが適用されます)。
ファンクターは今でも非常に構成可能なユニットの1つです。
だから私たちはモナドが何であるかに近づいています。さらに進んでみましょう。
これで、JavaScript配列は、少なくともある程度は代数を実行できる、非常に構成可能なユニットの1つであることがわかりました。
では、配列以外のJavaScript値についてはどうでしょうか。関数はどうですか?
代数JavaScript仕様を研究して従うと、FunctorやMonadを含むさまざまな構成可能なユニットの実装を簡単に試みることができますが、ポイントは何ですか?
結局のところ、それらは数学構造の分類表にすぎず、仕様に従うことは盲目的に意味がありません。
4.仕様
重要なのは、レルムが自己完結型である、非常に構成可能なユニットを取得することです。これが満たすべき唯一の仕様です。
そこで、問題の確立は次のとおりです。
自己完結型のレルムを生成するMath構造を実装し、それがどのように行われるかを確認します。
何でも問題ありません。最初から始めますが、参照するのに適したモデルはすでにあります。
JavaScript配列
Array.map(F).map(F).map(F)...
M
配列レルムの代わりに、次のように元のレルム を作成しましょう。
M.map(F).map(F).map(F)...
Array.map
簡潔な構文ではなく、M
それ自体が関数 だと思います。
M(F)(F)(F)...
まあ、普遍的に標準化されている特定の事前に選択されたブロックを常に使用することは良い規律です。それが開始のアイデアなので、おそらく、次のようにF
する必要がありますM
。
M(M)(M)(M)...
うーん、これはどういう意味ですか?
だから、これが私のクレイジーなアイデアです。
関数型プログラミングでは、すべての関数もファーストクラスのオブジェクトであり、それが画期的なことです。したがって、値/オブジェクト/関数をとして解釈するM
と、別のブレークスルーが発生します。
これは、「すべての値は配列です!」と言うようなクレイジーです。
正確には、JavaScriptの領域にある場合はクレイジーですが、Arrayの自己完結型の領域にある場合は正当です。
したがって、元のM
レルムがすべての裸の値/オブジェクト/関数を次のように扱うように設計しますM
たとえば、M
レルムでは、naked value:5
が見つかった場合、。として解釈されM(5)
ます。
言い換えると、レルム内にある限り、プログラマーは暗黙的にとして解釈されるため、M
記述する必要はありません。M(5)
5
M(5)
したがって、M
領域では:
5
= M(5)
= M(M(5))
= M(M(M(5)))
...
その結果、私M
はある程度透明でありM
、領域内の単位元である必要があることを発見しました。
私が強調してきたように、関数型プログラミングはすべて関数を構成することです。
関数の合成は、関数型プログラミングに関連しています。
M
関数を構成するために柔軟に作成する必要があります 。
const add1 = x => x + 1;
M(10)(add1); //11
M(10)(add1)(add1); //12
M(10)(add1)(add1)(add1); //13
const add2 = M(add1)(add1);
M(10)(add2); //12
const add3 = M(add2)(add1);
M(10)(add3); //13
また、高階関数の構成:
const plus = (x) => (y => x + y);
M(plus(1)(5)); //6
M(5)(M(1)(plus)); //6
const plus1 = M(1)(plus);
M(5)(plus1)(; //6
5.実装
これがの実装ですM
:
const compose = (f, g) => (x => g(f(x)));
const isMonad = (m) => !(typeof m.val === "undefined");
const M = (m = []) => {
const f = m1 => {
try { //check type error
return M(M(m1).val(m));
} catch (e) {
return M(compose(m, M(m1).val)); // f-f compose
};
};
f.val = m;
return isMonad(m)
? m
: f;
};
M.val = m => m;
ロギング機能:
const log = (m) => (typeof m !== 'function')
? (() => {
console.log(m);
return m;
})()
: err();
テストコード:
const err = () => {
throw new TypeError();
};
const log = (m) => (typeof m !== 'function')
? (() => {
console.log(m);
return m;
})()
: err();
const loglog = M(log)(log);
M("test")(loglog);
M("------")(log);
M([1])(log);
M(M(M(5)))(log)
M(99)(M)(log)
M("------")(log);
M([1, 2, 3])(([a, b, c]) => [a + 1, b + 1, c + 1])(log)
M("------")(log);
const add1 = a => (typeof a == 'number')
? a + 1
: err();
M(10)(add1)(log); //11
M(10)(add1)(add1)(log); //12
M(10)(add1)(add1)(add1)(log); //13
const add2 = M(add1)(add1);
M(10)(add2)(log); //12
const add3 = M(add2)(add1);
M(10)(add3)(log); //13
M("------")(log);
const plus = (x) => (y => x + y);
M(plus(1)(5))(log); //6
M(5)(M(1)(plus))(log); //6
const plus1 = M(1)(plus);
M(5)(plus1)(log); //6
M("------")(log);
const map = (f) => (array => array.map(f));
const map1 = M(add1)(map);
M([1, 2, 3])(log)(map1)(log);
//===
M("left identity M(a)(f) = f(a)")(log);
M(7)(add1)(log) //8
M("right identity M = M(M)")(log);
console.log(M) //{ [Function: M] val: [Function] }
console.log(M(M)) //{ [Function: M] val: [Function] }
M("identity")(log);
M(9)(M(x => x))(log); //9
M(9)(x => x)(log); //9
M("homomorphism")(log);
M(100)(M(add1))(log); //101
M(add1(100))(log); //101
M("interchange")(log);
M(3)(add1)(log); //4
M(add1)(f => f(3))(log); //4
M("associativity")(log);
M(10)(add1)(add1)(log); //12
M(10)(M(add1)(add1))(log); //12
出力:
test
test
------
[ 1 ]
5
99
------
[ 2, 3, 4 ]
------
11
12
13
12
13
------
6
6
6
------
[ 1, 2, 3 ]
[ 2, 3, 4 ]
left identity M(a)(f) = f(a)
8
right identity M = M(M)
{ [Function: M] val: [Function] }
{ [Function: M] val: [Function] }
identity
9
9
homomorphism
101
101
interchange
4
4
associativity
12
12
わかりました、働きました。
M
関数型プログラミングで非常に構成可能なユニットです。
6.検証
それで、これはいわゆるモナドですか?
はい。
https://github.com/fantasyland/fantasy-land#monad
モナド
Monad仕様を実装する値は、ApplicativeおよびChain仕様も実装する必要があります。1. (左のアイデンティティ)と同等ですM.of(a).chain(f)
2. f(a)
(右のアイデンティティ)と同等ですm.chain(M.of)
m
左の単位元M(a)(f)= f(a)
M(7)(add1) //8
M(add1(7)) //8
正しい単位元M=M(M)
console.log(M) //{ [Function: M] val: [Function] }
console.log(M(M)) //{ [Function: M] val: [Function] }
応募可能
Applicative仕様を実装する値は、 Apply仕様も実装する必要があります。1. (同一性)と同等v.ap(A.of(x => x))
2. (準同型)と同等3. (交換)と同等v
A.of(x).ap(A.of(f))
A.of(f(x))
A.of(y).ap(u)
u.ap(A.of(f => f(y)))
身元
M(9)(M(x => x)) //9
準同型
M(100)(M(add1)) //101
M(add1(100)) //101
交換
M(3)(add1) //4
M(add1)(f => f(3)) //4
鎖
チェーン仕様を実装する値は、適用仕様も実装する必要があります。1. (結合法則)m.chain(f).chain(g)
と同等ですm.chain(x => f(x).chain(g))
連想性
M(10)(add1)(add1) //12
M(10)(M(add1)(add1)) //12