【Eloquent JavaScript和訳】 Chapter 3: 関数

プログラム・テキストの外形をみてすぐにプログラムのどの部分で変数を入手できるかを判別し易くする以外に、レキシカル・スコーピングは関数の '合成' を可能にします。内包する関数の変数をいくつか使って、内部関数の別のことをさせることができます。1 つは引数に 2 を加え、1 つは引数に 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));

異なる関数が混乱を生じさせずに同じ名前の変数を持つことができるという事実に加え、これらのスコーピング・ルールはさらに関数が問題を生じさせずに 自身 を呼び出すことを可能にします。自身を呼び出す関数を再帰関数を呼びます。再帰 によって興味深い定義が可能になります。下の power の実装を見てください:

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

これは数学者が累乗法を定義するやり方に似ています。そして私には前の例よりもずっと洗練されているように見えます。多少のループはありますが、while も for もなく、ローカル副作用も見られません。自身を呼び出すことによって、関数は同じ効果を発揮しています。

しかし重大な問題が 1 つあります: 多くのブラウザーではこの例は前の例に比べて 10倍の時間がかかります。JavaScript では 1 つのループを実行することは関数を複数回呼び出すよりもずっと割安なのです。

スピードか洗練さかのジレンマは興味深い問題です。再帰を採用するか否かの場合だけに留まりません。多くの場合、簡潔な解決法よりも複雑でもより速い解決法が好まれます。

power 関数の場合、上記のより洗練さに欠ける例でも十分に読みやすくシンプルです。再帰の例に代える意味はほとんどありません。しかし、プログラムが現在係わっている概念は非常に複雑で、効率を諦めてプログラムを簡潔にすることがより魅力的な選択となるなる場合が多くあります。

基本ルールは、多くのプログラマーが繰り返し口にし私も全面的に賛成していることですが、プログラムの速度が極端に遅くなるまで効率のことは気にするなということです。遅くなって初めてどの部分が悪いのかを突き止め、その部分を効率的で洗練されたものに交換すればよいのです。

勿論、このルールはパフォーマンスを無視してよいという意味ではありません。power 関数のように、'洗練された' アプローチでもシンプルにはならない場合が多いのです。そうでなくても、経験を積んだプログラマーならシンプルなアプローチが満足する速さに通じないということはすぐに分かるはずです。

私がこのことを重大視する理由は、驚くほど多くのプログラマーが熱狂的に、それもささいな細部に至るまで 効率 に執着しているからです。結果は巨大化し、複雑さを増し、時には正確さを失くしたプログラムになってしまいます。さらに、分かりやすいほうの解決法よりも書くのに時間がかかり、速度は辛うじて速くなるだけです。

しかし私は再帰について話していました。再帰に深く関係する概念はスタックと呼ばれるものです。関数が呼び出されると、制御はその関数の本体に渡されます。その本体が制御を返すと、関数を呼び出したコードが再開します。本体の実行中、コンピューターは関数が呼び出されたコンテキストを記憶し、後で処理を続ける場所を知っておかなければなりません。このコンテキストを保存する場所をスタックと呼びます。

'スタック' と呼ばれるからには、既に見てきたように、関数本体が関数を再び呼び出すことができなければなりません。関数が呼び出される度にコンテキストが保存されます。コンテキストが積み重ねられていくのを思い浮かべることができます。関数が呼び出される度にコンテキストがスタックの上に積まれ、関数が復帰するとスタックの一番上のコンテキストが取り出され、再開します。

このスタックはコンピューターのメモリー・スペースに保存する必要があります。スタックが大きくなり過ぎると、コンピュータは "out of stack space" または "too much recursion" というメッセージを出して保存を諦めます。再帰関数を書く時はこのことを念頭に置いておかなければなりません。

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度しか参照されないため、この名前は無意味で、関数値を直接返したほうがましです。