関数型JavaScriptプログラミング





まとめ



いくつかの高次関数を取り、それらの関数の部分的なアプリケーションを追加し、マップで折り畳みを調整し、DOMを操作するためのJavascript DSLを取得します。



人間の言葉で

純粋で明快なJavaScriptによる関数型プログラミングの簡単で明快な紹介。



Haskell Through Thornsとは異なりすべてが噛まれ(多すぎる場合もあります)、棚に置かれます。



記事を読むことで、実生活でのAFの適用不可能性の神話が明らかになります。 同じ問題の解決策をさまざまな観点から見ることができます。 写真のように。







機能



簡単な関数定義から始めましょう。

function add(a,b){ console.log(a+b); };
      
      







同じコードを異なる方法で書くことができます。

 var add = function(a,b){ console.log(a + b); };
      
      







Javascriptの大きな利点の1つは、その中の関数が本格的なオブジェクトであることです。 真のファーストクラス市民。

たとえば、Javaとは異なり、関数はオブジェクトとは別に存在できません。



上記の関数は副作用を伴います。つまり、外界の状態を変更します。 これは、 console.log()を使用して表現されます。



次に、純関数の例を考えてみましょう。

 var add = function(a,b){ return a + b; };
      
      







純粋な関数は副作用を引き起こしません。 いくつかのデータをそれらに転送すると、データが返されます。 分析は非常に簡単です。 テストが簡単です。 外部依存関係を確認する必要はありません。 したがって、ほとんどの場合、副作用のある関数よりも純粋な関数の方が適しています。

しかし、一方で、純粋な関数のみで構成されるプログラムは実用的な意味を持ちません。 彼女は何も読んだり出力したりしません。

したがって、純粋な機能を副作用のある機能から分離するようにプログラムを記述し、生活を簡素化することは論理的です。



したがって、関数型プログラミングの最初のルール- 私たちはファイトクラブについては語りません-純粋な関数使用しています。



高階関数



さらに進んでいます。 関数はオブジェクトなので、他の関数に渡すことができます。 また、高階関数とは、関数を返す関数、または関数をパラメーターとして受け取る関数です。



以下は、関数も返す関数の簡単な例です。



 function makeAdder(base){ return function(num){ return base + num; } }
      
      







そして、その使用例。

 var add2 = makeAdder(2); add2(3); //5 add2(7); //9
      
      







シンプルで明白。



そして、これは高次関数のかなりよく知られた例です

 var el = document.getElementById("btn"); el.addEventListener("click", function (event){ });
      
      







パラメータとしてのaddEventListenerは関数を受け取ります。 つまり、 addEventListenerは高階関数です。



そして、イベントが発生するとハンドラー関数が呼び出されます。



おそらく、もう1つのオプションがより身近なものです。



 $("input[type=submit]").on("clink", function(event){ // ... });
      
      







または、jQueryを使用してハンドラーを記述することもできます。



だからもう一度定義:

FVPは、関数を返すか、パラメーターとして関数を受け入れる関数です。



サイクル





古い知人。

サイクルとは、標準の正面ソリューションを意味します。 このようなもの



 for(var i =0; i<n; ++1){ // }
      
      







またはそのような

 while(n--){ // ... } // ...
      
      







なぜループを使用するのですか? いくつかの標準的なユースケースを見て、ループが常に最良の解決策ではないことを見てみましょう。



最初のオプションは、配列とリストを走査することです



 for(var i =0; l< arr.length; i<l; ++i){ console.log(arr[i]); }
      
      







通常、このバイパスは副作用と組み合わせて使用​​されます。 通常、これらの効果は単純なコンソール出力よりも少し便利です。



2番目のオプション-リストからデータを取得する



 var names = []; for (var i =0; l= tweeps.length; i< l; ++i) { names.push(tweeps[i].name); }
      
      





この場合、twitterユーザーのリスト。

ループを使用して、ユーザー名のリストを取得します



別の使用例は、リスト内のデータ集約です。



 var html = ""; for(var i =0; l = items.length, i<l, i++){ html += '<li>' + items[i] + '</li>'; } this.list.innerHTML = html;
      
      







つまり、リストデータを集計し、出力で異なるデータ構造を取得します。



foreach



ループが常に最良のソリューションであるとは限らないと言いましたが、代替手段は何ですか?



同様のサイクルを置き換えることができるものは何ですか?



 for (var i =1; l = arr.length; i< l; ++i){ console.log(arr[i]); }
      
      







たとえば、 foreach



 Array.prototype.forEach arr.forEach(function(item){ console.log(item); });
      
      







手でリストを実行する代わりに、

配列メソッド。 各要素を処理する関数を渡し、必要な結果を取得します。



しかし、これの根本的な違いは何ですか?

 for (var i =1; l = arr.length; i< l; ++i){ console.log(arr[i]); }
      
      







そしてこれ



 arr.forEach(function(item){ console.log(item); });
      
      









残念ながら、JSで関数を記述するための構文は十分に冗長なので、書かれたテキストの量を大幅に節約することはできませんでした。



しかし、何か他のものがあります。 コードを見ると、各実装でどのような注意が払われているかがわかります。



最初のセクションでは、サイクル自体のメカニズムに焦点を当てます。 数値を取得し、1ずつ増やし、インデックスで配列要素を取得し、アクションを実行します。



2番目の例は理解しやすいです。 リストの各要素で何かをします。



2番目の例では、抽象化のレベルがはるかに高くなっています。 そして、反対側から問題の解決にアプローチすることができます。



したがって、ループを使用できるものについては:





地図



JavaScriptにある別の関数を見てみましょう。



 var names = []; for( var i =0, l= tweeps.length, i< l; ++i){ names.push(tweeps[i].name); }
      
      







これは、リスト変換に対応する抽象化です。

マップを使用すると、この問題をはるかに簡単に解決できます。



  //Array.prototype.map var names = tweeps.map(function (tweep){ return tweep.name; });
      
      







サイクルの説明から一時変数を取り除きました。 直接かつ明確なコード。 また、処理機能は十分に短いため、すべてを1行に収めることができます。



 var names = tweeps.map(function(t){return t.name;});
      
      







私は1行でコードを書くのが好きではありません。 しかし、1行でいくつのアイデアを表現できるかは、APIの表現力について語っています。

Twitterでメンションを探してください。



 var str = "mentioned by"; for(var i =0; l= tweeps.length; i < l; ++i){ str += tweeps[i].name; if(i< tweeps.length-1) {str += ", "} }
      
      







かなり不器用な例。 インデックスの作成と配列要素の取得でエラーが発生する場合があります。

この例で実際に行うことを分析しましょう。







マップ結合を使用した書き換え



 var str = "mentioned by " + tweeps.map(function(t){ return t.name; }).join(", ");
      
      







ミスをする機会はずっと少なくなりました。

しかし、それはもっと良くできますか? :)

オブジェクトのプロパティにアクセスするために使用する別の高次関数を紹介しましょう。



彼女の小道具と呼ぼう



 function prop(name){ return function (object){ return object[name]; } }
      
      







一見、まったく意味がありません。 名前をそれに渡します

そして、必要なフィールドを引き出すオブジェクトが転送される関数を返します。



ある種の紛らわしい説明が出ました。 実際のタスクでこの関数を使用してみましょう。



 var str = "Mentioned by " + tweeps.map(prop ("name")).join(", ");
      
      







それで、別のワンライナー。 かなり良い表現力。 そして、 prop関数はそれほど役に立たないわけではありません。



減らす



これは、for、foreach、whileなどの類似の構造のstructures祖母です。 この関数はfoldとも呼ばれます。



プリミティブな例からもう一度始めましょう。

 var totalLength = 0; for(var i=0; i< buffers.length; i++){ total.Length += buffers[i].length; }
      
      







バッファの長さを合計するだけです。

どのステップに従うべきですか?





Luke関数を使用します。



まず、 mapを使用してバッファー長を含むリストを取得します

 var totalLength = buffers. map(function (buffer) {return buffer.length; })
      
      







2番目のステップでは、 reduceを適用して合計を取得します。

 var totalLength = buffers. map(function (buffer) {return buffer.length; }). reduce(function(sum, curr){return sum+curr;}, 0);
      
      







reduceに慣れていない場合は、非常に簡単に機能します。 バッテリー機能がそれに送信され、各要素とバッテリー機能の初期値に適用されます。



繰り返しますが、複雑すぎます。 reduceを単純なリストに適用するとどうなるかを見てみましょう。



 [10, 5, 15, 10, 10].reduce(function(sum, curr){return sum+curr;}, 0); // [10, 5, 15, 10, 10] // sum curr // => 0, 10 => 10 // => 10, 5 => 15 // => 15, 15 => 30 // => 30, 10 => 40 // => 40, 10 => 50
      
      







したがって、reduceを使用すると、リストアイテムを簡単に追加できます。



しかし、すでに似たようなものがありました。 比較してください。



 function (prev, curr){return prev + curr;}
      
      





そして

 function add(a,b){ return a+b; }
      
      





したがって、バッファーの合計長を計算するために関数を単純化できます。



 var totalLength = buffers. map(function (buffer) {return buffer.length; }). reduce(add, 0);
      
      







今より明確ですか? reduceは、 add関数を使用してリストのすべての要素を単純に合計します。 合計の初期値はゼロです。 何がもっと簡単だろうか?



しかし、単純化はこれで終わりではありません。 比較する



 function (buffer) {return buffer.length; }
      
      





そして

 prop("length")
      
      







パンツが回って......

 var totalLength = buffers. map(prop("length")). reduce(add, 0);
      
      







エレガントなショーツ。



もちろん、これを1行で書くことができます



 var totalLength = buffers.map(prop("length")).reduce(add, 0);
      
      







ループの代わりに畳み込み(縮小)を使用すると、異なる抽象化レベルで考えることができます。 各要素のレベルではなく、リストに対して操作を実行します。



非同期呼び出し





ただし、リストを要約するためにリデュース別名フォールドを使用することは、非常に単純化された例です。 このアイデアははるかに強力です。 別の例を見てみましょう。



ブラウザでJavascriptを使用する際の問題の1つは、すべてが同じスレッドで実行されるため、コールバックを使用する必要があることです。



チャレンジ。



つまり、次のような関数を作成する必要があります。



 combine(["/jquery.js", "/underscore.js", "/backbone.js"], function(content){ // content    ,    . });
      
      







結合関数の実装を書きましょう。 最初-正面アプローチ。

 function combine(scripts, callback){ var data []; for(var i =0; l = scripts.length; i< l; ++i){ // .... } }
      
      







スクリプトの場合、jQuery.ajaxを使用することは論理的です。



 function combine(scripts, callback){ var data []; for(var i =0; l = scripts.length; i< l; ++i){ jQuery.ajax({ url: scripts[i], success : function(response){ // .... } }); } }
      
      





このようなコードは、サーバーへのリクエストが非同期に送信されるため、ブラウザーの速度を低下させることはありません。 つまり、実行すると、3つの並列クエリが実行されます。



スクリプトを正常にダウンロードするためのハンドラを作成します。



 function combine(scripts, callback){ var data []; for(var i =0; l = scripts.length; i< l; ++i){ jQuery.ajax({ url: scripts[i], success : function(response){ data[i] = response; if(data.length === scripts.length){ callback(data.join("")); } } }); } }
      
      







機能の準備ができているようです。 しかし、2つあります。

第一に、ugい、そして第二に-それは動作しません。



ここで何が問題になりますか? JavaScriptスコープを使用。 この言語では、スコープはブロックベースではなく機能的です。 つまり、3つの関数すべてで変数iの同じ値が表示されます。 サーバーからの応答が来る前にサイクルが機能するため、3つの機能はすべてi == 3で機能します。

この問題は標準的な方法で解決されます-ループ変数の値をキャッシュします。 しかし、このコードがより美しくなったとは言えません。



 function combine(scripts, callback){ var data []; for(var i =0; l = scripts.length; i< l; ++i){ (function (i){ jQuery.ajax({ url: scripts[i], success : function(response){ data[i] = response; if(data.length === scripts.length){ callback(data.join("")); } } }); }(i)); } }
      
      







ほとんど動作します。 クロージャーとトリッキーな変数を取り除くために、foreachを使用できます



 function combine(scripts, callback){ var data []; scripts.forEach(function(script,i){ jQuery.ajax({ url: scripts[i], success : function(response){ data[i] = response; if(data.length === scripts.length){ callback(data.join("")); } } }); }); } }
      
      







もちろん良いのですが、それでも怖いです。 ところで、コードはまだ正しく動作しません。 これは健全な状態に追加できますが、これにより開発とその後のサポートにさらに困難が生じます。



継続合格スタイル



頭痛を取り除くために、ライブラリを使用します



github.com/caolan/async



仕事では、CPSなどを使用します。



それは実際よりもずっと悪く聞こえます。 これは、別の関数をパラメーターとして受け取る関数であり、最初の関数が終了すると、retrunの代わりにパラメーター関数を呼び出します。



目的の結果が得られるようにjQuery.ajaxをラップします。



 function ajax(url, callback){ jQuery.ajax({url: url, success: callback}); }
      
      







この関数はパラメーターとしてコールバックを受け取りますが、エラーハンドラーについては説明しませんでした。 実際のコードである必要がありますが、表示を簡単にするために、忘れてしまいます。

非同期ライブラリを使用するとどうなりますか? 次のようになります。



 function combine(scripts, callback){ async.map(scripts, ajax, function(contents){ callback(contents.join("")); }); }
      
      







非同期の世界で機能する既製のマップ関数があります。 ところで、現在の実装では、前の例とは異なり、スクリプトを接着するための正しい順序が提供されます。



何と比較してください:

 function combine(scripts, callback){ var data []; for(var i =0; l = scripts.length; i< l; ++i){ (function (i){ jQuery.ajax({ url: scripts[i], success : function(response){ data[i] = response; if(data.length === scripts.length){ callback(data.join("")); } } }); }(i)); } }
      
      







mapはすでにプログラムを書くための自然な方法なので、上記のコードを書くことは決してありません。 非同期環境にマップを適応させる方法を考えます。 非同期ライブラリがなければ、非同期マップを自分で作成します。



機能的なアプローチにより、物事を見るのがずっと簡単になります。 より美しいソリューションを実装します。



関数の部分的な使用





関数型プログラミングから生まれた別のアイデアであり、正しく調理できれば非常に便利です。



例として、DOM要素を作成します。

(翻訳者の注意:cull.domは、プロジェクトの1つのために作成した著者のライブラリです。ただし、その中の機能は明白でシンプルです。)



 var ul = cull.dom.el("ul"); //document.createElement("ul") ul.nodeType === 1 // true
      
      







プロパティ属性を設定することもできます。



 var ul = cull.dom.el("ul", {className: "bands"});
      
      







そして子供を示す



 var li = cull.dom.el("li", "Tom Waits"); var ul = cull.dom.el("ul", {className: "bands"}, li);
      
      







相互に内部で使用すると、HTML向けの何らかのDSLを取得できます。



 va ul = cull.dom.el("ul", className:"bands"}, cull.dom.el("li", "Tom Waits"));
      
      







それでは、関数の部分的な使用について説明しましょう。 最初の例の1つを覚えていますか?



 function makeAdder(base){ return function(num){ return base + num; } }
      
      





2つの数値を合計する関数を返します。 もちろん、必要に応じて、名前付き関数を使用できます。



 function makeAdder(base){ return function(num){ return add(base, num); } }
      
      







そして今、 makeAdder関数がadd関数を取り、その引数の1つをキャプチャすることがわかります。 引数の1つが定数である加算関数を取得します



 var add2 = cull.partial(add, 2); add2(5); //7
      
      







興味深い機会がありました-DSLを作成してDOM要素をさらに美しくすることです。



 var ul = cull.partial(cull.dom.el, "ul"); var li = cull.partial(cull.dom.el, "li");
      
      







そして、このようなHTMLリストを作成できます



 var list = ul({className: "bands"}, [li("Diamanda Galas"), li(" "), li("John Zorn")]);
      
      







私のように、文字列変数のレベルでのプログラミングが嫌いなら、これはあなたの人生を簡素化する素晴らしい方法です。 これで、コード補完などの便利な機能が追加されました。 また、コードは通常のHTMLに非常に似ています。

そして、私たちのアプローチは非常に美しいため、事前にドキュメントのすべての要素に対して関数を作成できます。



 ["a", "br", "code", "div", ...].forEach(function(tagName){ cull.dom.el[tagName] = cull.partial(cull.dom.el, tagName); });
      
      







したがって、各HTML要素に対して関数を作成します。

もちろん、名前空間は必ずしも完全に使用できるとは限らないため、さらに簡略化します。



 var e = cull.dom.el; var list = ul({className: "bands"}, [e.li("Pan Sonic"), e.li(" "), e.li("Muslimgauze")]);
      
      







現在、グローバル変数と関数に縛られていません。これは良いことです。



機能構成





簡単なアプリケーションの別の例-アンケートです。





各ブロックに答える必要があります。 各ブロックにはいくつかの質問が含まれています。 1つのブロックに答えた後、次のブロックに進みます。



各ブロックは、質問モード、結果モード、または非アクティブのパネルとして表すことができます。







各パネルには異なるフィールドがあります。 文字列、数値、日付。

フィールドには、編集モードまたは結果モードの2つのモードがあります。



機能的なアプローチを使用してこの問題にどのようにアプローチできるかを見てみましょう。



お気に入りのプロップ機能を覚えてますか?



 tweeps.map(prop("name"));
      
      







彼女には双子の兄弟機能があります



 tweeps.map(func("to.String"));
      
      





オブジェクトに適用できる関数を返します。



アンケートの各ブロックの結果を計算しましょう



 buildSummary: function(){ return div(this.components.map(func("buildSummary"))); }
      
      







原則は明白であるべきです。 アンケートの各ブロックに対してbuildSummary関数によって作成された要素があるdivを返します。



この例では、各コンポーネント自体がその結果を表示する方法を知っています。 ただし、パネルは結果を特定の方法で表示する必要がある場合があります。



したがって、 buildSummarygetSummaryの 2つの関数を作成できます。



1つは、htmlタグを含む完全なプレゼンテーションを作成することです。

2番目-必要な結果を含むオブジェクトを返します。



そして、結果の巧妙な処理が必要になるとすぐに、すべての美しさが崩れ始めました。



 buildSummary: function(){ var div = document.createElement("div"); for(var i =0; l=this.components.length; i<l; ++i) { p = document.CreateElement("p"); p.innerHTML = this.components[i].getSummary().text; div.appendChild(p); } return div; }
      
      







ただし、このコードを改善するのに十分な機能指向を既に備えています。 最初の明らかな改善点は、foreachを適用することです。



 buildSummary : function(){ var div = document.createElement("div"); this.components.forEach(function(component){ var p = document.createElement("p"); p.innerHTML = component.getSummary().text; div.appendChild(p); }); return div; }
      
      







ループ変数を取り除きましたが、 mapを使用することは可能ですか?



 buildSummary : function(){ return div(this.components.map(function(component){ var p = document.createElement("p"); p.innerHTML = component.getSummary().text; return p; })); }
      
      







短いが理想からはほど遠い。 この式の主な問題:

 component.getSummary().text;
      
      







問題は、1つではなく、3つのことが起こっていることです。

  1. getSummary()による結果の取得
  2. テキストプロパティの取得
  3. 結果をpタグでラップする




いくつかのマップ関数はどうですか?



 buildSummary: function() { return div(this.components. map(function(component){ return component.getSummary(); }).map(function(summary){ return summary.text; }).map(function(text){ var p = document.createElement("p"); p.innerHTML = text; return p; })); }
      
      







機能的なスタイルはありますが、恐ろしく見えます。 読書は非常に不便です。



しかし、もう一度コードを見てみましょう。 ここには何がありますか?



 return component.getSummary();
      
      







ここでは、オブジェクトメソッドを呼び出します。 しかし、このための特別な関数funcを作成しました。



 buildSummary: function() { return div(this.components. map(func("getSummary")). map(function(summary){ return summary.text; }).map(function(text){ var p = document.createElement("p"); p.innerHTML = text; return p; })); }
      
      







そしてここに?



 function(summary){ return summary.text; }
      
      







オブジェクトのプロパティにアクセスできます。 このために、便利な機能もあります。



 buildSummary: function() { return div(this.components. map(func("getSummary")). map(prop("text")). map(function(text){ var p = document.createElement("p"); p.innerHTML = text; return p; })); }
      
      





最後のセクションが残った。

 function(text){ var p = document.createElement("p"); p.innerHTML = text; return p; }
      
      







ここで、DOM要素を作成し、その内部プロパティを設定します。 DSLにも似たようなものがありますよね?



 buildSummary: function() { return div(this.components. map(func("getSummary")). map(prop("text")). map(p)); }
      
      





今ではほとんど美しいです。 ただし、注意点が1つあります。 リストで3つのパスを行います。 場合によっては、これは正常かもしれませんが、全体的にいくぶん最適ではありません。 何ができますか?



機能の構成を使用します。 1つの関数に3つの関数を実行させたいのです。



 var summarize = compose( [p, prop("text"), func("getSummary")]);
      
      







作成をどのように実装しますか?



部分的に。 まず、多くのコードを記述しないように同義語を作成しましょう。



 var callGetSummary = func("getSummary"); var getText = prop("text"); var summarize = compose([p, getText, callGetSummary]);
      
      







すべてがシンプルで明白です。 さらに進んでいます。 サマリ関数を呼び出すとどうなるか見てみましょう。



最初のステップ



 var callGetSummary = func("getSummary"); var getText = prop("text"); var summarize = compose([p, getText, callGetSummary]); // summarize(obj); // => callGetSummary(obj)
      
      





オブジェクトはリストの最後の関数、つまりgetSummaryに転送されます。 タイプsummaryのオブジェクトを返します。 そして、このオブジェクトは次の関数getTextに渡されます



第二段階



 var callGetSummary = func("getSummary"); var getText = prop("text"); var summarize = compose([p, getText, callGetSummary]); // summarize(obj); // => getText(callGetSummary(obj))
      
      







2番目のステップの結果として、 textプロパティに含まれる文字列を取得します。 その後、行はpOM DOMオブジェクトを作成する関数に分類されます。



第三段階



 var callGetSummary = func("getSummary"); var getText = prop("text"); var summarize = compose([p, getText, callGetSummary]); // summarize(obj); // => p(getText(callGetSummary(obj)))
      
      







これは、パラメーターが関数から関数に順番に渡される場合の単純な構成の例です。パラメーターが各関数に渡されるときに構成を作成でき、出力は結果のリストになります。または何か他のもの。



それでは、辛抱強い例に戻りましょう。



 builSummary: function() { var summarize = compose( [p, prop("text"), func("getSummary")]); return div(this.components.map(summarize)); }
      
      







最初に、結果を計算するための関数を作成しました。そして、マップを適用しました。

同時に、summary関数はどのオブジェクトで動作するかを絶対に知らないことに注意してくださいこれらは、compose関数を介してのみ接続する3つの異なる抽象化です。したがって、集計を別のエンティティに作成できます。



 var summarize = compose( [p, prop("text"), func("getSummary")]); // ... builSummary: function() { return div(this.components.map(summarize)); }
      
      







かっこよくてきれいに見えますが、パフォーマンスはどうですか?



パフォーマンスの問題





for — 5M

forEach — 1,5M

reduce — 1.5M



DOM — 50K



, DOM. , , — . .



おわりに







(map, reduce).

.

.



PS cjohansen.no/talks/2012/javazone

PPS ?



All Articles