PMの翻訳のエラー/タイプミス/不正確さについてのコメントに感謝します。
著者から
過去数週間、私はモナドを理解しようとしてきました。 私はまだHaskellを勉強しており、率直に言って、私はそれが何であるかを知っていたと思いましたが、小さなライブラリを書きたいと思ったとき-トレーニングのために-私はモナドの
bind (>>=)
どのように働くか理解していますがそして
return
ですが、状態がどこから来たのかわかりません。 だから、おそらく、私はこれがすべてどのように機能するか理解していない。 その結果、Javascriptを例として使用してモナドを再学習することにしました。 Y Combinatorを推測したときの計画は同じでした。最初のタスク(ここでは明示的に不変の状態との相互作用です)を取り、元のコードを段階的に変更しながらソリューションに進みました。
Javascriptを選択したのは、簡潔な構文またはさまざまなセマンティクス(ラムダ式、演算子、組み込み関数のカリー化)のおかげでHaskellが有用に隠すすべてを書くことを強制するためです。 そして最後に、私は比較するのが上手なので、CoffeeScriptとSchemeでもこの問題を解決しました。コードスニペットへのリンクは次のとおりです。
- CoffeeScript: gist.github.com/936519
- スキーム: gist.github.com/936695
制限事項
この記事では、自分自身を状態のモナドに限定します。これは、一般的にモナドとは何かを理解するのに十分です。 制限事項は次のとおりです。
- 可変状態なし
Haskellは可変状態を持たないため、モナドを使用します。
- 明らかな条件なし
可変状態がない場合、結果の状態をどこにでも強制的に渡す必要があります。 そのようなコードを書いたり読んだりするのは楽しいことではありませんが、モナドはこのさをすべて隠します(少し後でわかるでしょう)。
- コードの重複なし
この発言は前の発言と連動しますが、重複するコードを削除することは新しい高さを探索するための強力なツールであるため、私はまだ個別に行います。
ムノガブカフニアシル
記事は非常に豊富であることが判明したため、追加の資料を追加するといいと思い、以下に説明するすべての手順を含む短いビデオを録画しました。 これはtl; drバージョンに似ており、説明されているすべてのトランジションを表示するのに役立ちますが、ビデオを見るのは記事を読んでいないとあまり役に立ちません。
HDで利用できるVimeoで直接視聴することをお勧めします。
実験ウサギ
スタックは簡単に理解できる構造になっており、通常の実装では状態変更を使用するため、スタックをテスト対象として使用します。 Javascriptスタックの通常の実装方法は次のとおりです。
var stack = []; stack.push(4); stack.push(5); stack.pop(); // 5 stack.pop(); // 4
Javascriptの配列には、スタックに表示されると予想されるすべてのメソッド
push
および
pop
ます。 私が嫌いなのは、彼らが彼らの状態を変えるということです。 まあ、少なくともこの記事では好きではありません。
説明する各ステップは機能しています。 ブラウザコンソールを開いてこのページを更新するだけです。行
5 : 4
グループがいくつか表示されます。 ただし、記事の本文では、前の手順と比較した変更点のみを引用します。
明示的な状態処理を備えたスタック
状態の変更を避けるための明らかな解決策は、変更ごとに新しい状態オブジェクトを作成することです。 Javascriptでは、これは次のようになります。
// .concat() .slice() - , , , var push = function (element, stack) { var newStack = [element].concat(stack); return newStack; }; var pop = function (stack) { var value = stack[0]; var newStack = stack.slice(1); return { value: value, stack: newStack }; }; var stack0 = []; var stack1 = push(4, stack0); var stack2 = push(5, stack1); var result0 = pop(stack2); // {value: 5, stack: [4]} var result1 = pop(result0.stack); // {value: 4, stack: []}
ご覧のとおり、
pop
と
push
は結果のスタックを返します。
pop
は、スタックの最上部からの値も返します。 後続の各スタック操作では、以前のバージョンのスタックが使用されますが、戻り値の表現の違いにより、これはそれほど明確ではありません。 戻り値を正規化することにより、コードの複製を強化できます。
var push = function (element, stack) { var value = undefined; var newStack = [element].concat(stack); return { value: value, stack: newStack }; }; var pop = function (stack) { var value = stack[0]; var newStack = stack.slice(1); return { value: value, stack: newStack }; }; var stack0 = []; var result0 = push(4, stack0); var result1 = push(5, result0.stack); var result2 = pop(result1.stack); // {value: 5, stack: [4]} var result3 = pop(result2.stack); // {value: 4, stack: []}
これは、前述のコードの複製と同じです。 複製。明示的な状態転送も意味します。
継続転送のスタイルでコードを書き換えます
次に、これらの中間結果を関数呼び出しに置き換えます。単純な変数よりも関数やパラメーターを抽象化する方が簡単だからです。 これを行うには、スタック操作の結果に渡された継続を単に適用
bind
ヘルパー関数を作成します。 つまり、続編をスタック操作にバインドします。
var bind = function (value, continuation) { return continuation(value); }; var stack0 = []; var finalResult = bind(push(4, stack0), function (result0) { return bind(push(5, result0.stack), function (result1) { return bind(pop(result1.stack), function (result2) { return bind(pop(result2.stack), function (result3) { var value = result2.value + " : " + result3.value; return { value: value, stack: result3.stack }; }); }); }); });
finalResult
で返される式全体の値は、単一の
push
または
pop
操作の値と同じ型です。 一貫したインターフェースが必要です。
push
アンドpop
次に、
bind
引数を隠し
bind
渡すため、スタック引数を
push
および
pop
から切り離す必要があります。
これを行うには、 カリー化と呼ばれる別のラムダ計算のトリックを使用します 。 言い換えれば、関数の使用の先延ばしと呼ぶことができます。
ここで、
push(4, stack0)
を呼び出す代わりに、
push(4)(stack0)
を呼び出します。 Haskellでは、関数が既にカリー化されているため、この手順は必要ありません。
var push = function (element) { return function (stack) { var value = undefined; var newStack = [element].concat(stack); return { value: value, stack: newStack }; }; }; var pop = function () { return function (stack) { var value = stack[0]; var newStack = stack.slice(1); return { value: value, stack: newStack }; }; }; var stack0 = []; var finalResult = bind(push(4)(stack0), function (result0) { return bind(push(5)(result0.stack), function (result1) { return bind(pop()(result1.stack), function (result2) { return bind(pop()(result2.stack), function (result3) { var value = result2.value + " : " + result3.value; return { value: value, stack: result3.stack }; }); }); }); });
中間スタックを渡すためのbind
準備
前の部分で述べたように、
bind
を明示的なスタックで引数に渡したいと思います。 これを行うには、まず、
bind
が最後のパラメーターとしてスタックを取るようにしますが、カリー化された関数の形式、つまり
bind
がスタックを引数として取る関数を返すようにします。 また、
push
と
pop
部分的に適用されるようになりました。つまり、スタックを直接渡すことはなくなり、
bind
がこれを実行するようになりました。
var bind = function (stackOperation, continuation) { return function (stack) { return continuation(stackOperation(stack)); }; }; var stack0 = []; var finalResult = bind(push(4), function (result0) { return bind(push(5), function (result1) { return bind(pop(), function (result2) { return bind(pop(), function (result3) { var value = result2.value + " : " + result3.value; return { value: value, stack: result3.stack }; })(result2.stack); })(result1.stack); })(result0.stack); })(stack0);
最後にスタックを削除します
ここで、
bind
を変更して
stackOperation
関数の戻り値を解析し、そこからスタックを
stackOperation
スタックを
stackOperation
関数である継続に
stackOperation
ことにより、中間スタックを非表示にできます。 また、戻り値
{ value: value, stack: result3.stack }
を匿名関数でラップする必要があります。
var bind = function (stackOperation, continuation) { return function (stack) { var result = stackOperation(stack); var newStack = result.stack; return continuation(result)(newStack); }; }; var computation = bind(push(4), function (result0) { return bind(push(5), function (result1) { return bind(pop(), function (result2) { return bind(pop(), function (result3) { var value = result2.value + " : " + result3.value; // We need this anonymous function because we changed the protocol // of the continuation. Now, each continuation must return a // function which accepts a stack. return function (stack) { return { value: value, stack: stack }; }; }); }); }); }); var stack0 = []; var finalResult = computation(stack0);
残りのスタックを非表示にします
前の実装では、いくつかの中間スタックを隠していましたが、最終値を返す関数に別の中間スタックを追加しました。 別のヘルパー関数
result
記述することにより、このスタックトレースを非表示にできます。 さらに、これにより、保存している状態のビューが非表示になり
stack
と
stack
2つのフィールドを持つ構造体。
var result = function (value) { return function (stack) { return { value: value, stack: stack }; }; }; var computation = bind(push(4), function (result0) { return bind(push(5), function (result1) { return bind(pop(), function (result2) { return bind(pop(), function (result3) { return result(result2.value + " : " + result3.value); }); }); }); }); var stack0 = []; var finalResult = computation(stack0);
これは、まさにHaskellの
return
関数が行うことです。 計算結果をモナドにラップします。 私たちの場合、それはスタックがとるクロージャーで結果をラップします。これは正確に可変状態を持つ計算のモナドです-その状態をとる関数です。 言い換えれば、
result/return
値は、渡される値の周りの状態で新しいコンテキストを作成するファクトリ関数として説明できます。
状態を内部にする
push
関数と
pop
関数によって返される構造を認識するために継続する必要はありません。これは実際にモナドの内部を表します。 したがって、
bind
を変更して、必要な最小データのみをコールバックに転送します。
var bind = function (stackOperation, continuation) { return function (stack) { var result = stackOperation(stack); return continuation(result.value)(result.stack); }; }; var computation = bind(push(4), function () { return bind(push(5), function () { return bind(pop(), function (result1) { return bind(pop(), function (result2) { return result(result1 + " : " + result2); }); }); }); }); var stack0 = []; var finalResult = computation(stack0);
スタック計算を実行する
スタック上の操作を組み合わせることができるため、これらの計算を実行して結果を使用する必要があります。 これは一般にモナド評価と呼ばれます。 Haskellでは、変数状態計算モナドは、それを計算するための3つの関数
runState
、
evalState
および
execState
ます。
この記事の目的上、
State
サフィックスを
Stack
置き換えます。
// Returns both the result and the final state. var runStack = function (stackOperation, initialStack) { return stackOperation(initialStack); }; // Returns only the computed result. var evalStack = function (stackOperation, initialStack) { return stackOperation(initialStack).value; }; // Returns only the final state. var execStack = function (stackOperation, initialStack) { return stackOperation(initialStack).stack; }; var stack0 = []; console.log(runStack(computation, stack0)); // { value="5 : 4", stack=[]} console.log(evalStack(computation, stack0)); // 5 : 4 console.log(execStack(computation, stack0)); // []
最終的な計算値
evalStack
が必要な場合は、
evalStack
が必要です。 モナド計算を開始し、最終状態を破棄して計算値を返します。 この関数を使用して、モナドコンテキストから値を引き出すことができます。
モナドから脱出できないと聞いたことがあるなら、これはIOモナドのような少数の場合にのみ当てはまると言えます。 しかし、これは別の話です。主なことは、ステートフルコンピューティングのモナドから抜け出すことができるということです。
完了
あなたがまだ私と一緒にいるなら、私はこれがJavascriptのモナドのように見えると言うでしょう。 Haskellほどクールで読みやすいものではありませんが、私ができる最善の方法です。
モナドは、書くべき内容をほとんど示していないため、かなり抽象的な概念です。 基本的に、彼女は、いくつかの引数(可変状態のモナドの場合は状態)と2つの追加の関数
result
と
bind
を取る関数を作成する必要があると言います。 1つ目は、作成した関数のファクトリーとして機能し、2つ目は、モナドに関する必要なデータのみを外部の世界に提供し、モナドによって計算された値を受け取る継続を使用して、状態を渡すなどのすべての退屈な作業を行います。 モナドの中にあるべきものはすべて内部に残ります。 OOPと同じように、モナドのゲッター/セッターを作成することもできます。
プロトコルの場合、Haskellでの
computation
は次のようになります。
computation = do push 4 push 5 a <- pop b <- pop return $ (show a) ++ " : " ++ (show b)
Haskellで見栄えが良くなる主な理由は、
do
記法の形式で構文レベルでモナドをサポートする
do
です。 それは、Javascriptの場合よりも見栄えの良いバージョンの単なる砂糖です。 Haskellは、演算子のオーバーライドと簡潔なラムダ式のサポートのおかげで、より読みやすいモナドの実装を実装できます。
computation = push 4 >>= \_ -> push 5 >>= \_ -> pop >>= \a -> pop >>= \b -> return $ (show a) ++ " : " ++ (show b)
Haskellでは、
>>=
はJavaScriptで
bind
と呼ばれ、
return
は
result
と呼ばれます。 はい、Haskellでの
return
はキーワードではなく関数です。 その他の場合、
return
は
unit
「オーム」です。 ブライアンマリックは、Clojureのモナドに関する動画で
>>=
決定者を呼び出しました。 パッチャー、もちろん、彼は
return
を呼び出しまし
return
。
JavaScriptの小さな砂糖
実際、ヘルパー関数
sequence
を使用して、JavaScriptでモナド計算を行う方がはるかに優れてい
sequence
。 Javascriptの動的な性質により、
sequence
は任意の数の引数を取ることができます。これらの引数は、連続して実行する必要があるモナド演算であり、最後の引数では、モナドアクションの結果に対して実行する必要があるアクションです。 モナド計算の未定義の結果はすべて、このコールバックに転送されます。
var sequence = function (/* monadicActions..., continuation */) { var args = [].slice.call(arguments); var monadicActions = args.slice(0, -1); var continuation = args.slice(-1)[0]; return function (stack) { var initialState = { values: [], stack: stack }; var state = monadicActions.reduce(function (state, action) { var result = action(state.stack); var values = state.values.concat(result.value); var stack = result.stack; return { values: values, stack: stack }; }, initialState); var values = state.values.filter(function (value) { return value !== undefined; }); return continuation.apply(this, values)(state.stack); }; }; var computation = sequence( push(4), // <- programmable commas :) push(5), pop(), pop(), function (pop1, pop2) { return result(pop1 + " : " + pop2); } ); var initialStack = []; var result = computation(initialStack); // "5 : 4"
Real World Haskellの本の著者は、モナドとプログラム可能なセミコロンを比較しています。 この場合、ソフトウェアエミュレートされたコンマがあり
sequence
。これは、
sequence
内のモナドアクションを分離するために使用したためです。
遅延計算としてのモナド
モナドがコンピューティングを呼び出すことをよく耳にしました。 最初は理由がわかりませんでした。 彼らは言うことができる、彼らは異なることを計算するため、彼らは言うが、いや、誰も言う:「モナドは計算する」、彼らは通常、「モナドは計算する」と言う。 ドラフト記事を完成させた後、これが何を意味するのかをようやく理解しました(まあ、または理解したと思います)。 これらの一連のアクションと値は、指示されるまで何も計算しません。 これは、初期状態での呼び出し後に実行できる、部分的に適用された関数の単純な大きなチェーンです。 以下に例を示します。
var computation = sequence( push(4), push(5), pop(), pop(), function (pop1, pop2) { return result(pop1 + " : " + pop2); } );
このコードは、実行後に何かを計算しますか? いや
runStack
、
evalStack
または
execStack
を使用して実行する必要があります。
var initialStack = []; evalStack(computation, initialStack);
push
と
pop
はある種のグローバルな値に作用しているように見えますが、実際には、この値が渡されると常に待機します。 これを計算のコンテキストとして使用するのは、OOPのようです。 私たちの場合、これはカリー化と部分的なアプリケーションを使用して実装され、各式の新しいコンテキストも指します。 また、OOPでコンテキストが暗黙的と呼ばれる場合、モナドを使用すると、さらに暗黙的(存在する場合)になります。
モナド(および一般的な関数型プログラミング)の利点は、簡単に組み合わせ可能なブロックが得られることです。 そして、これはすべてカレーのおかげです。 2つのモナドアクションが連続して実行されるたびに、実行を待機している新しい関数が作成されます。
var computation1 = sequence( push(4), push(5), pop(), pop(), function (pop1, pop2) { return result(pop1 + " : " + pop2); } ); var computation2 = sequence( push(2), push(3), pop(), pop(), function (pop1, pop2) { return result(pop1 + " : " + pop2); } ); var composed = sequence( computation1, computation2, function (a, b) { return result(a + " : " + b); } ); console.log( evalStack(composed, []) ); // "5 : 4 : 3 : 2"
これは、スタックで操作を実行するときにはほとんど役に立たないように思えるかもしれませんが、たとえば、パーサーコンビネータのライブラリを設計するとき、非常に役立ちます。 これにより、ライブラリ作成者は、パーサーモナドにいくつかのプリミティブ関数のみを提供でき、ライブラリユーザーは必要に応じてこれらのプリミティブを混合し、最終的に組み込みDSLにアクセスできます。
終わり
この記事がお役に立てば幸いです。 そのスペル(および翻訳- 約Per )は、間違いなくモナドの理解を向上させるのに役立ちました。