モナドで地獄を避ける

プログラマーとして、私たちは「プログラミング地獄」に陥ることがあります。これは、通常の抽象化では何度も繰り返される問題の解決に失敗する場所です。







この記事では、そのような問題、それらを解決するために使用される構文構造、そして最後にモナドを使用してこれらの問題を均一に解決する方法について説明します。







ヌルチェック地獄



この問題は、いくつかの部分関数(値を返さない可能性がある関数)を順番に実行する必要がある場合に発生します。







このような関数は通常、深く埋め込まれ、過度の構文ノイズを伴うコードの読み取りが困難になります。







var a = getData(); if (a != null) { var b = getMoreData(a); if (b != null) { var c = getMoreData(b); if (c != null) { var d = getEvenMoreData(a, c) if (d != null) { print(d); } } } }
      
      





組み込みソリューション:Elvis Operator



これは特別な構文(?。)であり、部分関数の呼び出し間を移動するのに役立ちます。 残念ながら、オブジェクト指向スタイルのレコードとメソッドへのアクセスにあまりにも結びついています。







 var a = getData(); var b = a?.getMoreData(); var c = b?.getMoreData(); var d = c?.getEvenMoreData(a); print(d);
      
      





たぶんモナド



関数がMaybe型(Optionと呼ばれることもあります)を明示的に返す場合、do表記を使用してこれらの関数をチェーンできます(Maybe / Optionが単項であるという事実を使用)。







 do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData ac print d
      
      





サイクルの地獄



この問題は、複数の依存データセットを調べる必要がある場合に発生します。 nullをチェックするときと同じように、コードは多くの構文ノイズで深くネストされます







 var a = getData(); for (var a_i in a) { var b = getMoreData(a_i); for (var b_j in b) { var c = getMoreData(b_j); for (var c_k in c) { var d = getMoreData(c_k); for (var d_l in d) { print(d_l); } } } }
      
      





組み込みソリューション:リストの有効化



この問題に対するよりエレガントな解決策は、リストインクルージョンと呼ばれる特別な構文構成の導入で見つかりました。これはSQLに非常に似ています( たとえば、C#では類似度が最大Transl。に達します )。







 [ print(d) for a in getData() for b in getMoreData(a) for c in getMoreData(b) for d in getEvenMoreData(a, c) ]
      
      





リストモナド



リストがモナドであることに注意し、do記法を使用すると、追加の構文構成要素なしで同じエレガントなソリューションを書くことができます。







 do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData ac print d
      
      





コールバック地獄



地獄の最も有名な、そしておそらく最も痛みを伴うサークル。 ここでは、非同期を実装するために制御の反転が必要です。これは、深く埋め込まれたコードと構文ノイズ、エラー処理の追跡の困難性、および他の多くの痛みにつながります。







 getData(a => getMoreData(a, b => getMoreData(b, c => getEvenMoreData(a, c, d => print(d), err => onErrorD(err) ) err => onErrorC(err) ), err => onErrorB(err) ), err => onErrorA(err) )
      
      





組み込みソリューション:async / await



この困難を克服するために、別の特別な構文が発明されました-async / await。 通常、このアプローチはtry / catch構文を使用してエラー処理を委任しますが、それ自体が別の問題につながります。







 async function() { var a = await getData var b = await getMoreData(a) var c = await getMoreData(b) var d = await getEvenMoreData(a, c) print(d) }
      
      





組み込みソリューション:約束



Promisesは別の可能なソリューションです(先物/タスクも)。 添付ファイルの問題は部分的に解決されていますが、いくつかの場所でプロミスの結果を使用すると、そのような値のレキシカルスコープを手動で作成する必要があります。 これにより、複数の場所で使用される変数ごとに1レベルの添付ファイルが作成されます。 また、promiseを直接使用すると、構文はasync / awaitを使用するほどきれいに見えません







 getData().then(a => getMoreData(a) .then(b => getMoreData(b)) .then(c => getEvenMoreData(a, c)) .then(d => print(d) );
      
      





モナド継続



以前の2つの問題と同じアプローチを使用してこの問題を解決できることは、もはや驚きではありません(約束がモナドを形成することに注意してください)。







 do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData ac print d
      
      





地獄ステータス転送



副作用がなくても、純粋な機能の世界には困難があります。 場合によっては、関数間でパラメーターを超過することが問題になることがあります。







 let (a, st1) = getData initalState (b, st2) = getMoreData (a, st1) (c, st3) = getMoreData (b, st2) (d, st4) = getEvenMoreData (a, c, st3) in print(d)
      
      





組み込みソリューション:命令型言語



この問題は、暗黙的な状態を使用して解決できます。これにより、すべてのパラメーターを明示的に渡すことなく、関数が相互に通信できるようになります。 残念ながら、命令型モデルを使用すると、コードの理解が大幅に複雑になります。 通常、状態の​​ライフサイクルとサイズには静的な境界はありません。







 a = getData(); b = getMoreData(a); c = getMoreData(b); d = getEvenMoreData(a, c); print(d)
      
      





モナド州



このモナドを使用すると、外部リンクのない純粋な機能状態を使用できます。これにより、状態のシリアル化やエクスカーションなどの機能の実装など、Reduxのようなライブラリで行うような多くの便利な操作を使用できます。







Stateモナドは、計算ライフサイクルの状態を制限し、プログラムの理解を容易にします。







 do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData ac print d
      
      





おわりに



モナドは、一定の方法で多くの問題を解決できます。 追加の構文を使用して言語の設計と文法を複雑にする代わりに、モナドライブラリを使用してそれらを解決できます。モナドライブラリは、他の多くの問題を解決するように適応できます。








All Articles