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 は、間違いなくモナドの理解を向䞊させるのに圹立ちたした。

参照資料



本




蚘事ず文曞




映像


Source: https://habr.com/ru/post/J238171/


All Articles