JavaScriptを使用したMonadの理解

元の記事-JavaScriptを使用した Monadの 理解Ionuț G. Stan )。

PMの翻訳のエラー/タイプミス/不正確さについてのコメントに感謝します。



著者から



過去数週間、私はモナドを理解しようとしてきました。 私はまだHaskellを勉強しており、率直に言って、私はそれが何であるかを知っていたと思いましたが、小さなライブラリを書きたいと思ったとき-トレーニングのために-私はモナドのbind (>>=)



どのように働くか理解していますがそしてreturn



ですが、状態がどこから来たのかわかりません。 だから、おそらく、私はこれがすべてどのように機能するか理解していない。 その結果、Javascriptを例として使用してモナドを再学習することにしました。 Y Combinatorを推測したときの計画は同じでした。最初のタスク(ここでは明示的に不変の状態との相互作用です)を取り、元のコードを段階的に変更しながらソリューションに進みました。



Javascriptを選択したのは、簡潔な構文またはさまざまなセマンティクス(ラムダ式、演算子、組み込み関数のカリー化)のおかげでHaskellが有用に隠すすべてを書くことを強制するためです。 そして最後に、私は比較するのが上手なので、CoffeeScriptとSchemeでもこの問題を解決しました。コードスニペットへのリンクは次のとおりです。







制限事項



この記事では、自分自身を状態のモナドに限定します。これは、一般的にモナドとは何かを理解するのに十分です。 制限事項は次のとおりです。







ムノガブカフニアシル



記事は非常に豊富であることが判明したため、追加の資料を追加するといいと思い、以下に説明するすべての手順を含む短いビデオを録画しました。 これは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 )は、間違いなくモナドの理解を向上させるのに役立ちました。



参照資料











記事と文書







映像






All Articles