第3章: 関数

プログラムは、多くの場合、異なる場所で同じ処理を行う必要があります。必要なステートメントを毎回繰り返すのは面倒で、エラーが発生しやすいです。それらを一箇所にまとめておき、必要に応じてプログラムがそこを経由するようにするのが良いでしょう。これが関数が発明された理由です。関数は、プログラムがいつでも実行できる、缶詰になったコードです。文字列を画面に表示するにはかなりの数のステートメントが必要ですが、print関数があれば、print("Aleph")と書くだけで済みます。

しかし、関数を単なる缶詰になったコードのかたまりと見るだけでは、その本質を理解したことになりません。必要に応じて、純粋関数、アルゴリズム、間接参照、抽象化、決定、モジュール、継続、データ構造など、さまざまな役割を果たすことができます。関数を効果的に使用できることは、あらゆる種類の本格的なプログラミングに必要なスキルです。この章では、このトピックの概要を説明し、第6章では、関数の微妙な点をより深く掘り下げて説明します。


まず、純粋関数は、あなたが人生のどこかで受講したであろう数学の授業で関数と呼ばれていたものです。数の余弦または絶対値を求めることは、1つの引数の純粋関数です。加算は2つの引数の純粋関数です。

純粋関数の定義特性は、同じ引数が与えられると常に同じ値を返し、副作用がないことです。引数をいくつか受け取り、これらの引数に基づいて値を返し、それ以外のものに変更を加えることはありません。

JavaScriptでは、加算は演算子ですが、次のように関数でラップできます(これは無意味に見えるかもしれませんが、実際には役立つ状況に遭遇します)。

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

show(add(2, 2));

addは関数の名前です。abは2つの引数の名前です。return a + b;は関数の本体です。

キーワードfunctionは、新しい関数を作成する際に常に使用されます。変数名に続く場合、結果として得られる関数は、この名前で保存されます。名前の後に引数の名前のリストが続き、最後に関数の本体が続きます。whileループまたはif文の本体の周りのものとは異なり、関数の本体の周りの中括弧は必須です1

キーワードreturnは、式の後に続くことで、関数が返す値を決定します。制御がreturn文に遭遇すると、現在の関数からすぐにジャンプし、返された値を関数を呼び出したコードに渡します。式の後にreturn文がない場合は、関数はundefinedを返します。

本体には、もちろん、複数のステートメントを含めることができます。累乗を計算する関数(正の整数指数の場合)を次に示します。

function power(base, exponent) {
  var result = 1;
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
}

show(power(2, 10));

練習問題2.2を解いたことがあるなら、累乗を計算するこの手法は馴染みがあるはずです。

変数(result)の作成と更新は副作用です。純粋関数には副作用がないと言ったばかりではありませんか?

関数内で作成された変数は、関数内でのみ存在します。これは幸運なことです。そうでなければ、プログラマーはプログラム全体で必要なすべての変数に対して異なる名前を考え出す必要がありました。resultpower内でのみ存在するため、その変更は関数が返るまでしか続きません。そして、それを呼び出すコードの観点からは、副作用はありません。


練習問題 3.1

引数として与えられた数の絶対値を返すabsoluteという関数を作成してください。負の数の絶対値は、その数の正のバージョンであり、正の数(またはゼロ)の絶対値は、その数自身です。

function absolute(number) {
  if (number < 0)
    return -number;
  else
    return number;
}

show(absolute(-144));

純粋関数には、非常に優れた2つの特性があります。考えやすく、再利用しやすいということです。

関数が純粋な場合、その呼び出しはそれ自体として見ることができます。正しく動作しているかどうかが確信できない場合は、コンソールから直接呼び出してテストできます。これは、コンテキストに依存しないため簡単です2。これらのテストを自動化すること、つまり特定の関数をテストするプログラムを作成することは簡単です。純粋ではない関数は、あらゆる種類の要因に基づいて異なる値を返し、テストや検討が困難な副作用を持つ可能性があります。

純粋関数は自己完結型であるため、純粋ではない関数よりも幅広い状況で役立ち、関連性が高い可能性があります。たとえば、showを考えてみましょう。この関数の有用性は、出力の印刷のための特別な場所が画面にあることに依存します。その場所がない場合、関数は役に立ちません。関連する関数、formatと呼びましょう。これは、値を引数として受け取り、この値を表す文字列を返す関数です。この関数は、showよりも多くの状況で役立ちます。

もちろん、formatshowと同じ問題を解決するわけではなく、副作用が必要なため、純粋関数ではその問題を解決することはできません。多くの場合、純粋ではない関数がまさに必要なものです。他のケースでは、問題は純粋関数で解決できますが、純粋ではない方がはるかに便利であるか、効率的です。

したがって、何かが純粋関数として簡単に表現できる場合は、そのように記述してください。しかし、純粋ではない関数を記述しても決して罪悪感を感じないでください。


副作用のある関数には、return文を含める必要はありません。return文が検出されない場合、関数はundefinedを返します。

function yell(message) {
  alert(message + "!!");
}

yell("Yow");

関数の引数の名前は、関数内で変数として使用できます。これらは、関数が呼び出されている引数の値を参照し、関数内で作成された通常の変数と同様に、関数外には存在しません。最上位環境とは別に、関数呼び出しによって作成された、より小さなローカル環境があります。関数内の変数を検索する際、最初にローカル環境がチェックされ、変数がそこに存在しない場合にのみ、最上位環境で検索されます。これにより、関数内の変数が、同じ名前の最上位変数を「シャドウイング」することが可能になります。

function alertIsPrint(value) {
  var alert = print;
  alert(value);
}

alertIsPrint("Troglodites");

このローカル環境内の変数は、関数内のコードからのみ参照できます。この関数が別の関数を呼び出す場合、新しく呼び出された関数は最初の関数内の変数を参照できません。

var variable = "top-level";

function printVariable() {
  print("inside printVariable, the variable holds '" +
        variable + "'.");
}

function test() {
  var variable = "local";
  print("inside test, the variable holds '" + variable + "'.");
  printVariable();
}

test();

しかし、これは微妙でありながら非常に有用な現象ですが、関数が別の関数の内部で定義されている場合、そのローカル環境は最上位環境ではなく、それを囲むローカル環境に基づきます。

var variable = "top-level";
function parentFunction() {
  var variable = "local";
  function childFunction() {
    print(variable);
  }
  childFunction();
}
parentFunction();

これは、関数内でどの変数が参照できるかは、プログラムテキスト内のその関数の位置によって決まるということです。関数の定義の「上」で定義されたすべての変数を参照できます。つまり、関数を囲む関数の本体内にあるものと、プログラムの最上位レベルにあるものの両方です。この変数の参照可能性へのアプローチは、レキシカルスコープと呼ばれます。


他のプログラミング言語の経験がある人は、コードブロック(中括弧の間)も新しいローカル環境を作成すると予想するかもしれません。JavaScriptではそうではありません。関数は、新しいスコープを作成する唯一のものです。次のようなフリースタンディングブロックを使用できます…

var something = 1;
{
  var something = 2;
  print("Inside: " + something);
}
print("Outside: " + something);

…しかし、ブロック内のsomethingは、ブロック外の変数と同じ変数を指します。実際、このようなブロックは許可されていますが、全く無意味です。ほとんどの人が、これはJavaScriptの設計者による設計上のちょっとしたミスであり、ECMAScript Harmonyではブロック内に残る変数を定義する方法(letキーワード)が追加されると同意しています。


これは驚くかもしれないケースです。

var variable = "top-level";
function parentFunction() {
  var variable = "local";
  function childFunction() {
    print(variable);
  }
  return childFunction;
}

var child = parentFunction();
child();

parentFunctionは内部関数を返すため、底部のコードはこの関数を呼び出します。この時点でparentFunctionの実行は終了していますが、variableが値"local"を持つローカル環境はまだ存在し、childFunctionはまだそれを使用しています。この現象はクロージャと呼ばれます。


プログラムのどの部分で変数が使用可能かをプログラムテキストの形状を見ることで簡単に確認できることを可能にすることに加えて、レキシカルスコープにより、関数を「合成」することもできます。囲んでいる関数の変数のいくつかを使用することで、内部関数が異なる動作をするようにすることができます。引数に2を加える関数、5を加える関数など、いくつかの類似した関数が必要だと想像してください。

function makeAddFunction(amount) {
  function add(number) {
    return number + amount;
  }
  return add;
}

var addTwo = makeAddFunction(2);
var addFive = makeAddFunction(5);
show(addTwo(1) + addFive(1));

これを理解するには、関数を計算をパッケージ化するだけでなく、環境もパッケージ化するものと考える必要があります。最上位の関数は、最上位の環境で単に実行されます。これは明らかです。しかし、別の関数の内部で定義された関数は、それが定義された時点でその関数に存在していた環境へのアクセスを保持します。

このように、上記の例で`makeAddFunction`が呼び出されたときに作成される`add`関数は、`amount`が特定の値を持つ環境をキャプチャします。この環境と計算`return number + amount`を値としてパッケージ化し、外部関数から返します。

この返された関数(`addTwo`または`addFive`)が呼び出されると、変数`number`が値を持つ新しい環境が、キャプチャされた環境(`amount`が値を持つ環境)のサブ環境として作成されます。これら2つの値が加算され、結果が返されます。


異なる関数が同じ名前の変数を混同することなく含むことができるという事実以外にも、これらのスコープルールにより、関数が自分自身を呼び出す際に問題が発生することを回避できます。自分自身を呼び出す関数を再帰的といいます。再帰により、いくつかの興味深い定義が可能になります。`power`のこの実装を見てください。

function power(base, exponent) {
  if (exponent == 0)
    return 1;
  else
    return base * power(base, exponent - 1);
}

これは、数学者が指数表記を定義する方法にかなり近く、私にとっては以前のバージョンよりもずっと見栄えが良いように見えます。ある種のループですが、`while`、`for`、またはローカルな副作用さえ見られません。自分自身を呼び出すことで、関数は同じ効果を生み出します。

しかし、重要な問題が1つあります。ほとんどのブラウザでは、この2番目のバージョンは最初のバージョンよりも約10倍遅くなります。JavaScriptでは、単純なループを実行する方が、関数を複数回呼び出すよりもはるかに安価です。


速度対エレガンスのジレンマは興味深いものです。これは、再帰の賛否を決定する場合だけでなく、多くの状況で発生します。エレガントで直感的で、多くの場合短い解決策は、より複雑だが高速な解決策に置き換えることができます。

上記の`power`関数の場合は、非エレガントなバージョンはまだ十分にシンプルで読みやすいです。再帰的なバージョンに置き換えるのはあまり意味がありません。しかし、多くの場合、プログラムが扱っている概念は非常に複雑になるため、プログラムをより簡単にするために効率を犠牲にすることが魅力的な選択肢になります。

多くのプログラマーによって繰り返し述べられ、私も心から同意する基本的なルールは、プログラムが明らかに遅すぎるまで、効率について心配しないことです。遅すぎる場合は、どの部分が遅すぎるかを調べ、それらの部分でエレガンスを効率と交換し始めます。

もちろん、上記のルールは、パフォーマンスを完全に無視すべきという意味ではありません。`power`関数のような多くの場合、「エレガントな」アプローチではシンプルさがほとんど得られません。他の場合では、経験豊富なプログラマーは、単純なアプローチでは決して十分な速度にならないことをすぐに認識できます。

私がこれについて大騒ぎしている理由は、驚くほど多くのプログラマーが、些細な詳細においてさえ、熱狂的に効率に焦点を当てているためです。その結果、より大きく、より複雑で、多くの場合、より正確ではないプログラムになり、より簡単なプログラムよりも記述に時間がかかり、わずかにしか高速に実行されません。


しかし、私は再帰について話していました。再帰と密接に関連する概念は、スタックと呼ばれるものです。関数が呼び出されると、その関数の本体に制御が渡されます。その本体が返ると、関数を呼び出したコードが再開されます。本体が実行されている間、コンピューターは関数を呼び出したコンテキストを覚えておく必要があります。その後どこで続行するかを知るためです。このコンテキストが格納されている場所をスタックと呼びます。

「スタック」と呼ばれる理由は、前述のように、関数の本体が再び関数を呼び出すことができるという事実と関係があります。関数が呼び出されるたびに、別のコンテキストを格納する必要があります。これは、コンテキストのスタックとして視覚化できます。関数が呼び出されるたびに、現在のコンテキストがスタックの一番上に追加されます。関数が返ると、スタックの一番上のコンテキストが取り除かれ、再開されます。

このスタックには、コンピューターのメモリに格納するためのスペースが必要です。スタックが大きくなりすぎると、コンピューターは「スタック空間が不足しています」や「再帰が多すぎます」のようなメッセージを付けて処理を停止します。これは、再帰関数を記述する際に考慮する必要があることです。

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
print(chicken() + " came first.");

この例は、壊れたプログラムを作成する非常に興味深い方法を示すことに加えて、関数が直接自分自身を呼び出す必要がないことを示しています。関数が別の関数を呼び出し(直接的または間接的に)その関数が最初の関数を再び呼び出す場合、それでも再帰的です。


再帰は、常にループに対する効率の低い代替手段というわけではありません。ループよりも再帰の方が簡単に解決できる問題もあります。多くの場合、これらは、それぞれがさらに多くのブランチに枝分かれする可能性のある複数の「ブランチ」を探索または処理する必要がある問題です。

このパズルを考えてみてください。数字の1から始めて、5を加算するか3を乗算するかを繰り返し行うことで、無限の数の新しい数字を作成できます。与えられた数字に対して、その数字を作成する加算と乗算のシーケンスを見つけようとする関数をどのように記述しますか?

たとえば、数字の13は、最初に1に3を掛け、次に5を2回加えることで到達できます。数字の15はまったく到達できません。

これがその解決策です。

function findSequence(goal) {
  function find(start, history) {
    if (start == goal)
      return history;
    else if (start > goal)
      return null;
    else
      return find(start + 5, "(" + history + " + 5)") ||
             find(start * 3, "(" + history + " * 3)");
  }
  return find(1, "1");
}

print(findSequence(24));

必ずしも操作の最短シーケンスが見つかるわけではなく、任意のシーケンスが見つかった時点で満足します。

内部の`find`関数は、2つの異なる方法で自分自身を呼び出すことで、現在の数字に5を加算することと3を乗算することの両方の可能性を探ります。数字が見つかった場合、この数字を取得するために実行されたすべての演算子を記録するために使用される`history`文字列を返します。また、現在の数字が`goal`より大きいかどうかをチェックします。大きい場合、このブランチの探索を停止する必要があります。数字は得られません。

例における`||`演算子は、「`start`に5を加算することで見つかったソリューションを返し、それが失敗した場合は、`start`に3を乗算することで見つかったソリューションを返す」と解釈できます。これは、より冗長な方法で次のように記述することもできました。

else {
  var found = find(start + 5, "(" + history + " + 5)");
  if (found == null)
    found = find(start * 3, "(" + history + " * 3)");
  return found;
}

関数定義はプログラムの残りの部分の間のステートメントとして発生しますが、同じタイムラインの一部ではありません。

print("The future says: ", future());

function future() {
  return "We STILL have no flying cars.";
}

起こっていることは、コンピューターがすべての関数定義を調べ、関連付けられた関数をプログラムの実行を開始する前に格納することです。他の関数の中に定義されている関数でも同じことが起こります。外部関数が呼び出されると、最初に起こることは、すべての内部関数が新しい環境に追加されることです。


関数値を定義する別の方法があり、これは他の値が作成される方法により似ています。式が期待される場所に`function`キーワードを使用すると、関数値を生成する式として扱われます。このようにして作成された関数には名前を付ける必要はありません(ただし、名前を付けることは許可されています)。

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

`add`の定義の後のセミコロンに注意してください。通常の関数定義にはこれらは必要ありませんが、このステートメントは`var add = 22;`と同じ一般的な構造を持ち、そのためセミコロンが必要です。

この種の関数値は、無名関数と呼ばれます。これは名前がないためです。以前見た`makeAddFunction`の例のように、関数に名前を付けるのが無意味な場合があります。

function makeAddFunction(amount) {
  return function (number) {
    return number + amount;
  };
}

`makeAddFunction`の最初のバージョンで`add`という名前の関数は1回しか参照されなかったため、名前は目的を果たしておらず、関数値を直接返すこともできます。


例 3.2

1つの引数(数値)を取り、テストを表す関数を返す`greaterThan`関数を作成します。この返された関数が引数として単一の数値で呼び出されると、ブール値を返します。与えられた数値がテスト関数の作成に使用された数値より大きい場合は`true`、そうでない場合は`false`。

function greaterThan(x) {
  return function(y) {
    return y > x;
  };
}

var greaterThanTen = greaterThan(10);
show(greaterThanTen(9));

次のことを試してみてください。

alert("Hello", "Good Evening", "How do you do?", "Goodbye");

`alert`関数は公式には1つの引数しか受け付けません。しかし、このように呼び出すと、コンピューターはまったく文句を言わずに、他の引数を無視するだけです。

show();

明らかに、引数を少なすぎても済むようです。引数が渡されない場合、関数内のその値は`undefined`になります。

次の章では、関数の本体が、関数に渡された引数の正確なリストを取得する方法を示します。これは、任意の数の引数を受け入れる関数を可能にするため、有用です。printはこの機能を利用しています。

print("R", 2, "D", 2);

もちろん、この欠点は、alertのように固定数の引数を期待する関数に誤って引数の数を間違えて渡してしまう可能性があり、それが検知されないことです。

  1. 技術的には、これは必要ありませんでしたが、JavaScriptの設計者は、関数の本体に常に中括弧がある方が明確になると考えたのでしょう。
  2. 技術的には、純粋関数は外部変数の値を使用できません。これらの値は変更される可能性があり、これにより、同じ引数に対して関数が異なる値を返す可能性があります。実際には、プログラマは一部の変数を「定数」と見なす場合があります(変更されることは期待されていません)。そして、定数変数のみを使用する関数を純粋関数と見なします。関数値を含む変数は、多くの場合、定数変数の良い例です。