表現力豊かなJavaScript:高階関数

内容







Tsu-liとTsu-suは、新しいプログラムの規模を誇っていました。 「二十万行」とツリは言った、「「コメントは数えません!」ツスは答えました:「Pf-f、私はほぼ百万行です」 マスターユンマは、「私の最高のプログラムは500行かかる」と語った。 これを聞いて、ツリとツスは悟りを経験しました。



ゆんまマスター、プログラミング本



プログラムをビルドするには、2つの方法があります。1つは明らかにエラーが発生しないほど単純にする方法、もう1つは明らかなエラーが発生しないほど複雑にする方法です。



アンソニー・ホア、1980年チューリング・レクチャー





大きなプログラムはコストのかかるプログラムであり、作成された時間だけではありません。 通常、大きなサイズは複雑さを意味し、複雑さはプログラマを混乱させます。 混乱したプログラマーは、プログラムに間違いを犯します。 大きなプログラムとは、バグを隠す場所があり、見つけるのが難しいことを意味します。



導入部から2つの例に簡単に戻りましょう。 1つ目は自給自足で、6行かかります。



var total = 0, count = 1; while (count <= 10) { total += count; count += 1; } console.log(total);
      
      







2番目は2つの外部関数に基づいており、1行を占有します。



 console.log(sum(range(1, 10)));
      
      







間違いに遭遇する可能性が高いのはどれですか?



合計と範囲の定義のサイズを追加すると、2番目のプログラムも大きくなり、最初のプログラムより大きくなります。 しかし、私はそれが正しいと思われます。



これは、ソリューションの表現が解決される問題に直接関係するためです。 数値間隔の合計は、サイクルとカウンターではありません。 これらは量とギャップです。



この辞書の定義(合計および範囲関数)には、ループ、カウンター、およびその他のランダムな詳細が含まれます。 しかし、プログラム全体よりも単純な概念を表現しているため、正しく実行するのが簡単です。



抽象化



プログラムのコンテキストでは、これらの「辞書」定義はしばしば抽象化と呼ばれます。 抽象化は詳細を隠し、より高い、またはより抽象的なレベルでタスクについて話す機会を与えてくれます。



エンドウ豆スープの2つのレシピを比較します。



ボウルにサービングごとに乾燥エンドウ豆のカップを追加します。 エンドウ豆を覆うように水を加えます。 少なくとも12時間はそのままにしておきます。 水からエンドウ豆を取り除き、鍋に入れます。 サービングごとに水4カップを追加します。 フライパンを閉じて、エンドウ豆を2時間煮ます。 サービングごとにタマネギの半分を取ります。 ナイフで細かく切り、豆に加えます。 サービングごとにセロリの1つの茎を取る。 ナイフで細かく切り、豆に加えます。 サービングごとにニンジンを取る。 ナイフで細かく切り、豆に加えます。 さらに10分煮る。



2番目のレシピ:



サービングごと:1カップの乾燥エンドウ豆、半分のタマネギ、セロリの茎、ニンジン。

エンドウを12時間浸します。 サービングごとに水4カップで2時間煮込みます。 野菜をカットして追加します。 さらに10分煮る。



2番目の方法はより短く簡単です。 しかし、料理に関連する概念をもう少し知っておく必要があります-浸漬、シチュー、カッティング(および野菜)。



プログラミングするとき、必要な単語がすべて辞書にあるという事実に頼ることはできません。 このため、最初のレシピのパターンに滑り込むことができます。彼らが表現するより高いレベルの概念に気付くことなく、コンピューターにすべての小さなステップを次々と指示します。



プログラマーの2番目の性質は、概念が新しい言葉を思いついて抽象化することを求めたときに気付く能力であるべきです。



抽象配列トラバーサル



以前に使用した単純な関数は、抽象化の構築に適しています。 しかし、時には十分ではありません。



前の章で、このサイクルを数回見ました。



 var array = [1, 2, 3]; for (var i = 0; i < array.length; i++) { var current = array[i]; console.log(current); }
      
      







コードは、「配列内の各要素について、それをコンソールに出力します」と言います。 しかし、回避策を使用します-iをカウントし、配列の長さをチェックし、追加の変数currentを宣言するための変数を使用します。 彼は非常にハンサムではないだけでなく、潜在的な間違いの基礎にもなっています。 長さを書き込む代わりに変数iを誤って再利用したり、変数iとcurrentを混同したりする可能性があります。



関数に抽象化しましょう。 これを行う方法を考えられますか?



配列を走査し、console.logの各要素を呼び出す関数を記述するのは非常に簡単です



 function logEach(array) { for (var i = 0; i < array.length; i++) console.log(array[i]); }
      
      







しかし、コンソールに要素を表示する以外のことをする必要がある場合はどうでしょうか? 「何かをする」ことは関数として表すことができ、関数は単なる変数なので、このアクションを引数として渡すことができます。



 function forEach(array, action) { for (var i = 0; i < array.length; i++) action(array[i]); } forEach(["", "", ""], console.log); // →  // →  // → 
      
      







多くの場合、定義済みの関数をforEachに渡すことはできませんが、その場で関数を作成できます。



 var numbers = [1, 2, 3, 4, 5], sum = 0; forEach(numbers, function(number) { sum += number; }); console.log(sum); // → 15
      
      







ループ本体がブロックで記述された、古典的なforループのように見えます。 ただし、現在は本体は関数内にあり、forEach呼び出しブラケット内にもあります。 したがって、中括弧と括弧の両方で閉じる必要があります。



このテンプレートを使用して、現在の配列要素の変数名(数値)を設定できます。配列から手動で選択する必要はありません。



一般的に、私たちは自分自身で書く必要さえありません。 これは配列の標準的な方法です。 配列は作業中の変数として既に渡されているため、forEachは引数を1つだけ受け入れます。各要素に対して実行する必要がある関数です。



このアプローチの便利さを示すために、前の章の関数に戻りましょう。 配列を通過する2つのサイクルが含まれます。



 function gatherCorrelations(journal) { var phis = {}; for (var entry = 0; entry < journal.length; entry++) { var events = journal[entry].events; for (var i = 0; i < events.length; i++) { var event = events[i]; if (!(event in phis)) phis[event] = phi(tableFor(event, journal)); } } return phis; }
      
      







forEachを使用して、少し短く記録します。



 function gatherCorrelations(journal) { var phis = {}; journal.forEach(function(entry) { entry.events.forEach(function(event) { if (!(event in phis)) phis[event] = phi(tableFor(event, journal)); }); }); return phis; }
      
      







高階関数



他の関数を操作する関数(引数として使用するか返す関数)は、 高階関数と呼ばれます 。 関数が単なる変数であることを既に理解している場合、そのような関数の存在について特別なことはありません。 この用語は数学に由来します。数学では、関数と他の意味の違いがより厳密に認識されます。



高階関数を使用すると、値だけでなくアクションを抽象化できます。 彼らは違います。 たとえば、新しい関数を作成する関数を作成できます。



 function greaterThan(n) { return function(m) { return m > n; }; } var greaterThan10 = greaterThan(10); console.log(greaterThan10(11)); // → true
      
      







他の機能を変更する機能を作成できます。



 function noisy(f) { return function(arg) { console.log("calling with", arg); var val = f(arg); console.log("called with", arg, "- got", val); return val; }; } noisy(Boolean)(0); // → calling with 0 // → called with 0 - got false
      
      







新しいタイプのプログラムフロー制御を作成する関数を作成することもできます。



 function unless(test, then) { if (!test) then(); } function repeat(times, body) { for (var i = 0; i < times; i++) body(i); } repeat(3, function(n) { unless(n % 2, function() { console.log(n, "is even"); }); }); // → 0 is even // → 2 is even
      
      







このような場合、第3章で説明したレキシカルスコープルールが役立ちます。 最後の例では、変数nは外部関数への引数です。 内側の関数は外側に囲まれているため、nを使用できます。 このような内部関数の本体は、それらを取り巻く変数にアクセスできます。 これらは、通常のループおよび条件式で使用されるブロック{}の役割を果たすことができます。 重要な違いは、内部関数内で宣言された変数が外部環境に分類されないことです。 通常、これは最高の場合のみです。



引数を渡す



前に宣言した、別の関数に引数を渡すノイズの多い関数は、完全に便利ではありません。



 function noisy(f) { return function(arg) { console.log("calling with", arg); var val = f(arg); console.log("called with", arg, "- got", val); return val; }; }
      
      







fが複数のパラメーターを取る場合、最初のパラメーターのみを受け取ります。 内部関数(arg1、arg2など)に多数の引数を追加して、それらすべてをfに渡すことは可能ですが、どれだけの量で十分かはわかりません。 さらに、関数fはarguments.lengthで正しく動作できませんでした。 常に同じ数の引数を渡すため、最初にいくつの引数が与えられたかはわかりません。



このような場合、JavaScriptの関数にはapplyメソッドがあります。 引数の配列(または配列の形式のオブジェクト)が彼に渡され、彼はこれらの引数で関数を呼び出します。



 function transparentWrapping(f) { return function() { return f.apply(null, arguments); }; }
      
      







この関数は役に立たないが、興味のあるテンプレートを示している-それによって返される関数は、受け取ったすべての引数をfに渡すが、それ以上は何もしない。 これは、引数オブジェクトに格納されている独自の引数をapplyメソッドに渡すことで発生します。 applyメソッドの最初の引数(この場合はnullに設定)は、メソッド呼び出しをエミュレートするために使用できます。 次の章でこの問題に戻ります。



ジョンソン



JavaScriptには、何らかの方法で配列要素に関数を適用する高次関数が広く普及しています。 forEachメソッドは、これらの関数の中で最も原始的なものの1つです。 配列メソッドとして、関数の他の多くのオプションにアクセスできます。 それらを知るために、別のデータセットで遊んでみましょう。



数年前、誰かが多くのアーカイブを調べて、私の姓の歴史に関する本全体を作成しました。 騎士、海賊、錬金術師を見つけることを期待して、私はそれを開けました...しかし、それは主にフランドルの農民で満たされていたことが判明しました。 娯楽のために、私は直接の先祖に関する情報を抽出し、コンピューターで読むのに適した形式で設計しました。



ファイルは次のようになります。



 [ {"name": "Emma de Milliano", "sex": "f", "born": 1876, "died": 1956, "father": "Petrus de Milliano", "mother": "Sophia van Damme"}, {"name": "Carolus Haverbeke", "sex": "m", "born": 1832, "died": 1905, "father": "Carel Haverbeke", "mother": "Maria van Brussel"}, …    ]
      
      







この形式はJSONと呼ばれ、JavaScript Object Notationを意味します。 データストレージおよびネットワーク通信で広く使用されています。



JSONは、配列とオブジェクトの記述方法がJavaScriptに似ていますが、いくつかの制限があります。 すべてのプロパティ名は二重引用符で囲む必要があり、単純な値のみが許可されます。関数呼び出し、変数、計算を含まないものは許可されません。 コメントも許可されていません。



JavaScriptは、データをこの形式からこの形式に変換するJSON.stringifyおよびJSON.parse関数を提供します。 最初は値を受け取り、JSONで文字列を返します。 2番目はそのような行を取り、値を返します。



 var string = JSON.stringify({name: "X", born: 1980}); console.log(string); // → {"name":"X","born":1980} console.log(JSON.parse(string).born); // → 1980
      
      







ANCESTRY_FILE変数はここから入手できます 。 JSONファイルが文字列として含まれています。 それをデコードして、言及された人の数を数えましょう。



 var ancestry = JSON.parse(ANCESTRY_FILE); console.log(ancestry.length); // → 39
      
      







配列をフィルター処理する



1924年に若い人を見つけるには、次の機能が役立ちます。 テストに失敗した配列の要素を除外します。



 function filter(array, test) { var passed = []; for (var i = 0; i < array.length; i++) { if (test(array[i])) passed.push(array[i]); } return passed; } console.log(filter(ancestry, function(person) { return person.born > 1900 && person.born < 1925; })); // → [{name: "Philibert Haverbeke", …}, …]
      
      







testという引数が使用されます-これは検証計算を実行する関数です。 各要素に対して呼び出され、それによって返される値は、この要素が返された配列に入るかどうかを決定します。



ファイルには、1924年に若い祖父、祖母、いとこの3人がいました。



フィルター関数は、既存の配列から要素を削除するのではなく、検証された要素のみを含む新しい要素を作成することに注意してください。 これは、渡された配列を損なわないため、純粋な関数です。



forEachと同様に、フィルターは標準の配列メソッドの1つです。 この例では、内部で何を行うかを示すためだけに、このような関数を説明しました。 これからは、単純に使用します。



 onsole.log(ancestry.filter(function(person) { return person.father == "Carel Haverbeke"; })); // → [{name: "Carolus Haverbeke", …}]
      
      







マップを使用した変換



祖先の配列をフィルタリングすることによって取得された、人々を表すオブジェクトのアーカイブがあるとします。 しかし、読みやすい名前の配列が必要です。



mapメソッドは、すべての要素に関数を適用し、返された値から新しい配列を作成することにより、配列を変換します。 新しい配列の長さは入力と同じですが、その内容は新しい形式に変換されます。



 function map(array, transform) { var mapped = []; for (var i = 0; i < array.length; i++) mapped.push(transform(array[i])); return mapped; } var overNinety = ancestry.filter(function(person) { return person.died - person.born > 90; }); console.log(map(overNinety, function(person) { return person.name; })); // → ["Clara Aernoudts", "Emile Haverbeke", // "Maria Haverbeke"]
      
      







興味深いことに、少なくとも90歳以上の人は、1920年代の若い人たちと同じです。 これは私の記録の最新世代です。 どうやら、薬は真剣に改善されました。



forEachやフィルターと同様に、mapも配列の標準的な方法です。



reduceを使用した合計



配列を扱うもう1つの一般的な例は、配列内のデータに基づいて単一の値を取得することです。 1つの例は、すでによく知られている数字のリストの合計です。 もう1つは、誰よりも先に生まれた人を検索することです。



このタイプの高次の操作は、reduce(reduction(または、fold、folding)と呼ばれます)。 一度に1つの要素の配列を折り畳むように想像できます。 数値を合計するとき、ゼロから開始し、各要素について、加算を使用して現在の合計と結合しました。



reduce関数のパラメーターは、配列に加えて、結合関数と初期値です。 この関数は、フィルターやマップよりも明確性が低いため、細心の注意を払ってください。



 function reduce(array, combine, start) { var current = start; for (var i = 0; i < array.length; i++) current = combine(current, array[i]); return current; } console.log(reduce([1, 2, 3, 4], function(a, b) { return a + b; }, 0)); // → 10
      
      







標準の配列の削減方法は、もちろん同じように機能しますが、さらに便利です。 配列に少なくとも1つの要素が含まれる場合、start引数を省略できます。 このメソッドは、配列の最初の要素を開始値として受け取り、2番目から作業を開始します。



reduceで知られる最古の祖先を見つけるために、次のように書くことができます:



 console.log(ancestry.reduce(function(min, cur) { if (cur.born < min.born) return cur; else return min; })); // → {name: "Pauwels van Haverbeke", born: 1535, …}
      
      







コンポーザビリティ



高階関数を使用せずに、先の例(生年月日が最も早い人を検索する)をどのように記述できますか? 実際、コードはそれほどひどいものではありません。



 var min = ancestry[0]; for (var i = 1; i < ancestry.length; i++) { var cur = ancestry[i]; if (cur.born < min.born) min = cur; } console.log(min); // → {name: "Pauwels van Haverbeke", born: 1535, …}
      
      







変数が少し多く、2行長くなりますが、これまでのところ、コードはかなり明確です。



関数を組み合わせる必要がある場合、高階関数は本当に開きます。 たとえば、セット内の男性と女性の平均年齢を見つけるコードを記述します。



 function average(array) { function plus(a, b) { return a + b; } return array.reduce(plus) / array.length; } function age(p) { return p.died - p.born; } function male(p) { return p.sex == "m"; } function female(p) { return p.sex == "f"; } console.log(average(ancestry.filter(male).map(age))); // → 61.67 console.log(average(ancestry.filter(female).map(age))); // → 54.56
      
      







(加算をプラス関数として定義する必要があるのはばかげていますが、JavaScriptの演算子は値ではないため、引数として渡すことはありません)。



アルゴリズムを大規模なサイクルに埋め込む代わりに、性別の決定、年齢のカウント、数値の平均化など、関心のある概念に従ってすべてが配布されます。 それらを順番に適用して結果を取得します。



わかりやすいコードの場合、これは素晴らしい機会です。 もちろん、明快さは無料ではありません。



価格



エレガントなコードと美しい虹の幸せな土地に、Inefficiencyという名前の派手なモンスターがいます。



配列を処理するプログラムは、明確に分離された一連のステップとして最も美しく表現されます。各ステップは、配列で何かを実行し、新しい配列を返します。 しかし、これらすべての中間配列を階層化することは高価です。



同様に、関数をforEachに渡して配列を通過させるのは便利で理解しやすいです。 ただし、JavaScriptで関数を呼び出すことは、ループよりも高価です。



これは、プログラムの読みやすさを向上させる多くの手法にも当てはまります。 抽象化は、コンピューターのクリーンな作業と作業する概念の間にレイヤーを追加します。その結果、コンピューターはより多くの作業を行います。 これは厳密なルールではありません-効率を犠牲にすることなく抽象化を追加できる言語があり、JavaScriptでさえ、経験豊富なプログラマーが抽象的で高速なコードを書く方法を見つけることができます。 しかし、この問題は一般的です。



幸いなことに、ほとんどのコンピューターはめちゃくちゃ速いです。 データセットが大きすぎない場合、または実行時間を人の観点から見て十分に高速にする必要がある場合(たとえば、ユーザーがボタンを押すたびに何かを実行するため)-動作する美しいソリューションを作成してもかまいません0.5ミリ秒、または非常に最適化されており、1/10ミリ秒で機能します。



このコードが呼び出される頻度を大まかに計算すると便利です。 ループ内にループがある場合(直接、または内部でループとも機能する関数のループ内の呼び出しを介して)、コードはN * M回実行されます。ここで、Nは外側のループの繰り返し数、Mは内側のループの繰り返し数です。 内側のループにP回繰り返される別のサイクルがある場合、すでにN * M * P-を取得します。 これは大きな数につながる可能性があり、プログラムの速度が低下すると、多くの場合、問題は最も内側のループ内の小さなコードに縮小されます。



グレートグレートグレートグレートグレート...



私の祖父、フィリバートハーバーベケは、データファイルに記載されています。 彼から始めて、私の祖先である最年長のパウエル・ファン・ハベルベック、私の直接の祖先を探して家族を追跡できます。 今、私は彼からのDNAの割合を計算したい(理論上)。



祖先の名前からそれを表すオブジェクトに移動するには、名前と人に一致するオブジェクトを作成します。



 var byName = {}; ancestry.forEach(function(person) { byName[person.name] = person; }); console.log(byName["Philibert Haverbeke"]); // → {name: "Philibert Haverbeke", …}
      
      







タスクは、各レコードで父親を見つけて、パウエルまでの歩数を計算するだけではありません。 家族の歴史では、いとこの間でいくつかの結婚がありました(井戸、小さな村など)。 この点で、ある場所の家系図の枝は他の場所とつながっているので、1/2 G(Gはパウエルと私の間の世代数)よりも多くの遺伝子を取得します。 この式は、各世代が遺伝基金を2つに分割するという仮定に基づいています。



データを左から右に順番に結合することで配列が単一の値に削減される場合、reduceを使用して類推するのが合理的です。 ここでも特異点を取得する必要がありますが、遺伝の線に従う必要があります。 そして、それらは単純なリストではなく、ツリーを形成します。



特定の人の祖先のこれらの値を組み合わせて、この値を考慮します。 これは再帰的に実行できます。 ある種の人が必要な場合は、両親に必要な値を計算する必要があり、そのために祖先などの計算が必要になります。 , , - . , . – , , .



, , reduceAncestors «» .



 function reduceAncestors(person, f, defaultValue) { function valueFor(person) { if (person == null) return defaultValue; else return f(person, valueFor(byName[person.mother]), valueFor(byName[person.father])); } return valueFor(person); }
      
      







valueFor . . person f, .



, , .



 function sharedDNA(person, fromMother, fromFather) { if (person.name == "Pauwels van Haverbeke") return 1; else return (fromMother + fromFather) / 2; } var ph = byName["Philibert Haverbeke"]; console.log(reduceAncestors(ph, sharedDNA, 0) / 4); // → 0.00049
      
      







, , 100% ( ), 1. .



0.05% 16 . , , . , 3 , - .



reduceAncestors. ( ) ( ) . , , 70 .



 function countAncestors(person, test) { function combine(person, fromMother, fromFather) { var thisOneCounts = test(person); return fromMother + fromFather + (thisOneCounts ? 1 : 0); } return reduceAncestors(person, combine, 0); } function longLivingPercentage(person) { var all = countAncestors(person, function(person) { return true; }); var longLiving = countAncestors(person, function(person) { return (person.died - person.born) >= 70; }); return longLiving / all; } console.log(longLivingPercentage(byName["Emile Haverbeke"])); // → 0.145
      
      







, . , reduceAncestors – .





bind, , , , .



, . isInSet, , . filter , isInSet, , isInSet .



 var theSet = ["Carel Haverbeke", "Maria van Brussel", "Donald Duck"]; function isInSet(set, person) { return set.indexOf(person.name) > -1; } console.log(ancestry.filter(function(person) { return isInSet(theSet, person); })); // → [{name: "Maria van Brussel", …}, // {name: "Carel Haverbeke", …}] console.log(ancestry.filter(isInSet.bind(null, theSet))); // → … same result
      
      







bind , isInSet theSet, , bind.



, null, – , apply. .



まとめ



– , JavaScript. « » , , .



– forEach, - , filter – , , map – , , reduce – .



apply, . bind .







reduce concat , .



 var arrays = [[1, 2, 3], [4, 5], [6]]; //    // → [1, 2, 3, 4, 5, 6]
      
      









, ( ). average, .



– , , . byName, .



 function average(array) { function plus(a, b) { return a + b; } return array.reduce(plus) / array.length; } var byName = {}; ancestry.forEach(function(person) { byName[person.name] = person; }); //    // → 31.2
      
      









, 90 . . . , , 100 : Math.ceil(person.died / 100).



 function average(array) { function plus(a, b) { return a + b; } return array.reduce(plus) / array.length; } //    // → 16: 43.5 // 17: 51.2 // 18: 52.8 // 19: 54.8 // 20: 84.7 // 21: 94
      
      







groupBy, . , , , .



Every some


every some. , , , true false. , && true, true, every true, true . , some true, true . , – , some true , .



every some, , , .



 //    console.log(every([NaN, NaN, NaN], isNaN)); // → true console.log(every([NaN, NaN, 4], isNaN)); // → false console.log(some([NaN, 3, 4], isNaN)); // → true console.log(some([2, 3, 4], isNaN)); // → false
      
      






All Articles