第3版が利用可能です。ここから読んでください

第5章
高階関数

子梨と子思は、自分たちの最新のプログラムの規模を自慢し合っていた。「コメントを除いて20万行だ!」と子梨は言った。子思は「ふん、私のプログラムはもうすぐ100万行になる」と答えた。老師元馬は言った。「私の最高のプログラムは500行だ」。これを聞いて、子梨と子思は悟りを開いた。

老師元馬、『プログラミングの書』

ソフトウェア設計を構築するには2つの方法がある。1つは、明らかに欠陥がないほどシンプルにする方法であり、もう1つは、明らかに欠陥がないほど複雑にする方法である。

C.A.R. Hoare、『1980 ACMチューリング賞講演』

大規模なプログラムはコストのかかるプログラムであり、それは構築に時間がかかるからだけではない。規模はほとんどの場合、複雑さを伴い、複雑さはプログラマーを混乱させる。混乱したプログラマーは、プログラムに間違い(バグ)を導入する傾向がある。また、大規模なプログラムは、これらのバグが隠れるための多くのスペースを提供するため、バグを見つけるのが難しくなる。

導入部の最後の2つのプログラム例に簡単に立ち戻ってみよう。最初のプログラムは自己完結型で、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)));

どちらにバグが含まれている可能性が高いか?

sumrange の定義のサイズを数えると、2番目のプログラムも大きく、最初のプログラムよりもさらに大きい。しかしそれでも、私は2番目のプログラムの方が正しい可能性が高いと主張する。

解決策が、解決される問題に対応する語彙で表現されているため、正しい可能性が高くなる。数値の範囲の合計は、ループやカウンターに関するものではない。範囲と合計に関するものである。

この語彙の定義(関数 sumrange)には、依然としてループ、カウンター、その他の付随的な詳細が含まれる。しかし、プログラム全体よりも単純な概念を表現しているため、正しく理解しやすい。

抽象化

プログラミングのコンテキストでは、これらの種類の語彙は通常、抽象化と呼ばれる。抽象化は詳細を隠し、より高い(またはより抽象的な)レベルで問題について話す能力を提供する。

例として、豆のスープのこれら2つのレシピを比較してみよう。

乾燥エンドウ豆を1人あたり1カップ容器に入れる。エンドウ豆がよく覆われるまで水を加える。エンドウ豆を少なくとも12時間水に浸しておく。エンドウ豆を水から取り出し、調理鍋に入れる。1人あたり4カップの水を加える。鍋に蓋をして、エンドウ豆を2時間弱火で煮る。玉ねぎを1人あたり半分取る。ナイフで玉ねぎを切る。エンドウ豆に加える。セロリを1人あたり1本取る。ナイフでセロリを切る。エンドウ豆に加える。ニンジンを1人あたり1本取る。ニンジンを切る。ナイフで!エンドウ豆に加える。さらに10分間煮る。

そして2番目のレシピ

1人あたり:乾燥スプリットエンドウ豆1カップ、みじん切りにした玉ねぎ半分、セロリ1本、ニンジン1本。

エンドウ豆を12時間水に浸す。4カップの水(1人あたり)で2時間弱火で煮る。野菜を刻んで加える。さらに10分間煮る。

2番目のレシピは短く、解釈しやすい。ただし、料理に関連する単語(水に浸す弱火で煮る刻む、そしておそらく野菜)をもう少し理解する必要がある。

プログラミングを行う場合、必要なすべての単語が辞書に載っているとは限らない。そのため、最初のレシピのパターンに陥る可能性がある。つまり、コンピューターが実行する必要がある正確な手順を1つずつ、それらが表現する高レベルの概念を無視して、苦労して作成してしまう。

概念が新しい単語に抽象化されるべき時を認識することは、プログラマーにとって第二の天性になる必要がある。

配列走査の抽象化

これまでに見てきたように、普通の関数は抽象化を構築するための良い方法である。しかし、時には不十分な場合がある。

前の章では、このタイプの for ループが何度か登場した。

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

for (let i = 0; i < array.length; i++) {

これは、「配列の各要素について、コンソールにログを記録する」と言おうとしている。しかし、カウンター変数 i、配列の長さに対するチェック、現在の要素を選択するための追加の変数宣言を含む、回りくどい方法を使用している。少し目障りであることに加えて、これは潜在的なミスの余地を多く提供する。誤って i 変数を再利用したり、lengthlenght と綴りを間違えたり、i 変数と current 変数を混同したりする可能性がある。

そこで、これを関数に抽象化してみよう。何か方法を思いつくか?

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

まあ、配列を調べて、すべての要素に対して console.log を呼び出す関数を記述するのは簡単だ。

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

forEach(["Wampeter", "Foma", "Granfalloon"], console.log);
// → Wampeter
// → Foma
// → Granfalloon

function logEach(array) {

しかし、要素をログに記録する以外のことをしたい場合はどうだろうか?「何かをする」ことは関数として表現でき、関数は単なる値なので、アクションを関数値として渡すことができる。

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

function forEach(array, action) {

(一部のブラウザでは、このように console.log を呼び出すことはできない。この例が機能しない場合は、console.log の代わりに alert を使用できる。)

多くの場合、事前定義された関数を forEach に渡すのではなく、その場で関数値を作成する。

forEach(["Wampeter", "Foma", "Granfalloon"], word => {

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;
}

これは、本体が下にブロックとして記述された、古典的な for ループによく似ている。ただし、本体は関数値の内側と、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;
}

このパターンを使用すると、現在の要素(number)の変数名を指定できるため、配列から手動で選択する必要がなくなる。

実際、forEach を自分で書く必要はない。配列の標準メソッドとして利用できる。配列はすでにメソッドが作用する対象として提供されているため、forEach は必須の引数を1つだけ取る。各要素に対して実行される関数である。

numbers.forEach(number => {

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

これがどれほど役立つかを説明するために、前の章の関数を振り返ってみよう。これには、配列を走査するループが2つ含まれている。

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 dominantDirection(text) {

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

forEach を使用すると、わずかに短くなり、かなりきれいになる。

高階関数

引数として取る、または返すことによって、他の関数で動作する関数は、高階関数と呼ばれる。関数が通常の値であるという事実をすでに受け入れている場合、そのような関数が存在するという事実に特に注目すべき点はない。この用語は数学に由来し、関数と他の値の区別がより真剣に受け止められている。

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

高階関数は、値だけでなく、アクションを抽象化することを可能にする。それらはいくつかの形式で提供される。たとえば、新しい関数を作成する関数を持つことができる。

function greaterThan(n) {

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

そして、他の関数を変更する関数を持つことができる。

function noisy(f) {

新しいタイプの制御フローを提供する関数を記述することさえできる。

function unless(test, then) {

第3章で説明したレキシカルスコープ規則は、このように関数を使用する場合に役立つ。前の例では、n 変数は外側の関数の parameters である。内側の関数は外側の関数の環境内に存在するため、n を使用できる。このような内側の関数の本体は、周囲の変数にアクセスできる。通常のループや条件付きステートメントで使用される {} ブロックと同様の役割を果たすことができる。重要な違いは、内側の関数内で宣言された変数が、外側の関数の環境にならないことだ。そして、それは通常良いことだ。

[
  {"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"},
   and so on
]

このフォーマットはJSON(「ジェイソン」と発音します)と呼ばれ、JavaScript Object Notationの略です。Web上では、データの保存と通信のフォーマットとして広く使用されています。

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

JavaScriptには、JSON.stringifyJSON.parseという、データをこのフォーマットに変換したり、このフォーマットから変換したりする関数が用意されています。前者はJavaScriptの値を受け取り、JSONエンコードされた文字列を返します。後者はそのような文字列を受け取り、それがエンコードする値に変換します。

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という名前の引数(関数値)を使用して、計算の「ギャップ」を埋めます。test関数は各要素に対して呼び出され、その戻り値によって、要素が返される配列に含まれるかどうかが決まります。

ファイルには、1924年に生存していて若かった人が3人いました。私の祖父、祖母、そして大叔母です。

filter関数は、既存の配列から要素を削除するのではなく、テストに合格した要素のみを含む新しい配列を構築することに注意してください。この関数は*純粋*です。与えられた配列を変更しません。

forEachと同様に、filterも配列の標準メソッドです。この例では、内部で何をしているかを示すためだけに、関数を定義しました。これからは、代わりにこのように使用します。

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

mapによる変換

何らかの方法でancestry配列をフィルタリングすることによって生成された、人を表すオブジェクトの配列があるとします。しかし、読みやすい名前の配列が必要です。

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歳まで生きた人々は、私たちが以前に見たのと同じ3人です。1920年代に若かった人々であり、これは私のデータセットの中で最も最近の世代です。医学は大きく進歩したと思います。

forEachfilterと同様に、mapも配列の標準メソッドです。

reduceによる集計

配列に対する計算のもう1つの一般的なパターンは、配列から単一の値を計算することです。繰り返し例として挙げている、数値の集合の合計は、このインスタンスです。別の例としては、データセットの中で最も早い出生年を持つ人を見つけることが挙げられます。

このパターンを表す高階関数は、*reduce*(または*fold*と呼ばれることもあります)と呼ばれます。配列を一度に1つの要素ずつ折りたたむと考えることができます。数値を合計する場合、数値ゼロから始め、各要素について、2つを加算することによって現在の合計と組み合わせます。

reduce関数の引数は、配列とは別に、結合関数と開始値です。この関数は、filtermapほど単純ではないため、注意深く読んでください。

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

もちろんこの関数に対応する標準の配列メソッドreduceには、追加の利便性があります。配列に少なくとも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

plusを関数として定義する必要があるのは少しばかげているように思えますが、JavaScriptの演算子は関数とは異なり値ではないため、引数として渡すことはできません。)

ロジックを大きなループに絡ませる代わりに、性別を決定し、年齢を計算し、数値の平均を求めるという、私たちが関心を持っている概念にきちんと構成されています。これらを1つずつ適用して、求めている結果を得ることができます。

これは、明確なコードを書くための*素晴らしい*方法です。残念ながら、この明瞭さには代償が伴います。

代償

エレガントなコードと美しい虹の幸せな国には、*非効率性*と呼ばれる邪魔をするモンスターが住んでいます。

配列を処理するプログラムは、配列に対して何かを行い、新しい配列を生成する、きれいに分離されたステップのシーケンスとして最もエレガントに表現されます。しかし、これらすべての中間配列を構築するには、いくらかコストがかかります。

同様に、関数をforEachに渡して、そのメソッドに配列の反復処理を任せるのは便利で読みやすいです。しかし、JavaScriptでの関数呼び出しは、単純なループ本体と比較してコストがかかります。

そして、プログラムの明瞭さを向上させるのに役立つ多くのテクニックも同様です。抽象化は、コンピューターが行っている生のものと、私たちが扱っている概念との間にレイヤーを追加するため、マシンがより多くの作業を実行する原因となります。これは鉄則ではありません。非効率性を追加することなく抽象化を構築するためのより良いサポートを備えたプログラミング言語があり、JavaScriptでさえ、経験豊富なプログラマーは、依然として高速な抽象コードを記述する方法を見つけることができます。しかし、それは頻繁に発生する問題です。

幸いなことに、ほとんどのコンピューターは非常に高速です。適度なデータセットを処理している場合、または人間の時間スケールでのみ発生する必要があること(たとえば、ユーザーがボタンをクリックするたびに)を行っている場合、0.5ミリ秒かかるきれいなソリューションを書いたか、0.1ミリ秒かかる超最適化されたソリューションを書いたかは*問題ではありません*。

プログラムの一部が何回実行されるかを大まかに把握しておくと役立ちます。ループ内にループがある場合(直接、または外側のループが内側のループを実行することになる関数を呼び出すことによって)、内側のループ内のコードは*N*×*M*回実行されます。ここで、*N*は外側のループが繰り返される回数、*M*は外側のループの各反復内で内側のループが繰り返される回数です。その内側のループに*P*ラウンドを行う別のループが含まれている場合、その本体は*M*×*N*×*P*回実行されます。これは大きな数になる可能性があり、プログラムが遅い場合、問題は多くの場合、内側のループ内にあるコードのほんの一部に起因する可能性があります。

高祖父母の、高祖父母の…

私の祖父であるPhilibert Haverbekeは、データファイルに含まれています。彼から始めて、私の家系をたどることで、データの中で最も古い人物であるPauwels van Haverbekeが私の直系の祖先であるかどうかを確認できます。そして、もし彼がそうであれば、理論的に彼とどれだけのDNAを共有しているかを知りたいと思います。

親の名前からその人物を表す実際のオブジェクトに移動できるように、最初に名前と人物を関連付けるオブジェクトを構築します。

var byName = {};
ancestry.forEach(function(person) {
  byName[person.name] = person;
});

console.log(byName["Philibert Haverbeke"]);
// → {name: "Philibert Haverbeke", …}

さて、問題は、fatherプロパティをたどり、Pauwelsに到達するのにいくつのプロパティが必要かを数えるほど単純ではありません。家系図には、人々が再従兄弟と結婚したケースがいくつかあります(小さな村など)。これにより、家系図の枝がいくつかの場所で再結合します。つまり、Pauwelsと私の間の世代数を*G*とすると、私はこの人物と1/2*G*を超える遺伝子を共有していることになります。この公式は、各世代が遺伝子プールを2つに分割するという考えに基づいています。

この問題を考えるための合理的な方法は、reduce との類似性を見ることです。reduce は、値を左から右へ繰り返し結合することで、配列を単一の値に凝縮します。この場合も、データ構造を単一の値に凝縮したいのですが、家系に沿った方法で行う必要があります。データの *形状* は、フラットなリストではなく、家系図のようなものです。

この形状を縮小したい方法は、特定の人物の値を、その祖先の値を組み合わせて計算することです。これは再帰的に行うことができます。人物 *A* に関心がある場合、*A* の両親の値を計算する必要があり、それはさらに *A* の祖父母の値を計算する必要があることを意味します。原則として、これは無限の人々を見る必要があることを意味しますが、データセットは有限なので、どこかで停止する必要があります。縮小関数にデフォルト値を与えることを許可します。これは、データに含まれていない人々に使用されます。この場合、リストに載っていない人は、私たちが見ている祖先とDNAを共有していないという仮定に基づき、その値は単純にゼロです。

人物、特定の人物の両親からの値を組み合わせる関数、およびデフォルト値が与えられると、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)は、単一の人物を処理します。再帰の魔法により、この関数は単に自分自身を呼び出して、この人物の父親と母親を処理することができます。結果は、人物オブジェクト自体とともに f に渡され、この人物の実際の値が返されます。

これを使用して、私の祖父が Pauwels van Haverbeke と共有した DNA の量を計算し、それを 4 で割ることができます。

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

Pauwels van Haverbeke という名前の人は、明らかに Pauwels van Haverbeke と 100% の DNA を共有しているため(データセットに同名の複数人はいません)、この関数はこの人物に対して 1 を返します。他の人はすべて、両親が共有する量の平均を共有します。

つまり、統計的に言えば、私はこの 16 世紀の人物と約 0.05% の DNA を共有しています。これは統計的な近似値であり、正確な量ではないことに注意してください。これはかなり少ない数ですが、私たちが保有する遺伝物質の量(約 30 億塩基対)を考えると、私という生物学的機械には、Pauwels に由来する側面がまだ残っている可能性があります。

reduceAncestors に頼らずにこの数を計算することもできました。しかし、一般的なアプローチ(家系図の凝縮)を具体的なケース(共有 DNA の計算)から分離することで、コードの明瞭さが向上し、プログラムの抽象的な部分を他のケースに再利用できます。たとえば、次のコードは、70 歳を超えて生きた人の既知の祖先の割合を(血統別に、つまり複数回カウントされる可能性があります)求めます。

function countAncestors(person, test) {
  function combine(current, fromMother, fromFather) {
    var thisOneCounts = current != person && test(current);
    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.129

データセットにはかなり恣意的な人々の集合が含まれていることを考えると、このような数字はあまり真剣に受け止めるべきではありません。しかし、このコードは、reduceAncestors が家系図データ構造を操作するための有用な語彙を提供していることを示しています。

バインディング

すべての関数が持つ bind メソッドは、元の関数を呼び出す新しい関数を生成しますが、一部の引数は既に固定されています。

次のコードは、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 の呼び出しは、theSet を最初の引数として、続いてバインドされた関数に与えられた残りの引数を使用して isInSet を呼び出す関数を返します。

例では null を渡している最初の引数は、apply の最初の引数と同様に、メソッド呼び出しに使用されます。これについては、次の章 で詳しく説明します。

まとめ

関数値を他の関数に渡せることは、単なる仕掛けではなく、JavaScript の非常に便利な側面です。これにより、「ギャップ」のある計算を関数として記述し、これらの関数を呼び出すコードに、不足している計算を記述する関数値を提供することで、ギャップを埋めさせることができます。

配列は、多くの便利な高階メソッドを提供します。配列の各要素に対して何かを実行するための forEach、一部の要素を除外した新しい配列を作成するための filter、各要素が関数を通過した新しい配列を作成するための map、およびすべての配列要素を単一の値に結合するための reduce です。

関数には、引数を指定する配列を使用して関数を呼び出すために使用できる apply メソッドがあります。また、関数の部分的に適用されたバージョンを作成するために使用される bind メソッドもあります。

演習

フラット化

reduce メソッドと concat メソッドを組み合わせて、配列の配列を、入力配列のすべての要素を持つ単一の配列に「フラット化」します。

var arrays = [[1, 2, 3], [4, 5], [6]];
// Your code here.
// → [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;
});

// Your code here.

// → 31.2

ancestry 配列のすべての要素が有用なデータを生成するわけではないため(母親の生年月日がわからない限り年齢差を計算できません)、average を呼び出す前に何らかの方法で filter を適用する必要があります。hasKnownMother 関数を定義し、最初にフィルタリングすることで、最初のパスとして実行できます。あるいは、map を呼び出して、マッピング関数で年齢差を返すか、母親が不明の場合は null を返すことから始めることができます。次に、配列を average に渡す前に、filter を呼び出して null 要素を削除できます。

歴史的な平均寿命

データセットで 90 歳以上生きたすべての人々を調べたとき、データの最新世代のみが表示されました。その現象を詳しく見てみましょう。

世紀ごとに、祖先データセットの人々の平均年齢を計算して出力します。人は、死亡年を 100 で割り、Math.ceil(person.died / 100) のように切り上げることで、世紀に割り当てられます。

function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}

// Your code here.

// → 16: 43.5
//   17: 51.2
//   18: 52.8
//   19: 54.8
//   20: 84.7
//   21: 94

この例の要点は、コレクションの要素を何らかの側面でグループ化すること、つまり、祖先の配列を各世紀の祖先を持つ小さな配列に分割することです。

グループ化プロセス中に、世紀名(数字)を人物オブジェクトまたは年齢の配列に関連付けるオブジェクトを保持します。どのカテゴリが見つかるかを事前に知らないため、オンザフライで作成する必要があります。各人物について、世紀を計算した後、その世紀が既に既知かどうかをテストします。そうでない場合は、配列を追加します。次に、人物(または年齢)を適切な世紀の配列に追加します。

最後に、for/in ループを使用して、個々の世紀の平均年齢を出力できます。

ボーナスポイントとして、グループ化操作を抽象化する関数 groupBy を記述します。これは、配列と、配列内の要素のグループを計算する関数を引数として受け取り、グループ名をグループメンバーの配列にマッピングするオブジェクトを返す必要があります。

すべてと、いくつか

配列には、標準メソッド everysome も付属しています。どちらも、配列要素を引数として呼び出されたときに true または false を返す述語関数を採用します。&& は両側の式が true の場合にのみ true 値を返すのと同様に、every は述語が配列の *すべて* の要素に対して true を返す場合にのみ true を返します。同様に、some は、述語が要素の *いずれか* に対して true を返すとすぐに true を返します。必要な要素よりも多く処理することはありません。たとえば、some が配列の最初の要素に対して述語が成り立つことを発見した場合、それ以降の値は調べません。

これらのメソッドのように動作する 2 つの関数 everysome を記述しますが、配列をメソッドではなく最初の引数として取ります。

// Your code here.

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

関数は、章の始めの forEach定義と同様のパターンに従うことができますが、述語関数が false または true を返すとすぐに(正しい値で)返さなければなりません。ループの後に別の return ステートメントを配置して、関数が配列の最後に達したときにも正しい値を返すようにすることを忘れないでください。