【Eloquent JavaScript和訳】 Chapter 3: 関数

EloquentJavaScriptJa

公開:

0 なるほど
5,644 VIEW
0 コメント

プログラムは異なる場所で同じことをしなければならないことがよくあります。全てのステートメントを毎回繰り返すのは面倒で間違いの元でもあります。それらを一箇所に置き、必要な時にプログラムが迂回できたら便利です。関数が考案された目的はこれです: 関数は缶詰にしたコードで、プログラムは好きな時に迂回してそこに行くことができます。文字列を画面に表示するには相当数のステートメントが必要ですが、print 関数を使うと print("Aleph") と書くだけで済みます。

関数を缶詰にしたコードと見るのは正当な評価ではありません。必要とあれば関数は純関数にもアルゴリズムにもなれ、間接参照、抽象化、意思決定、モジュール、継続、データ構造その他の役割を担うこともできます。関数を効果的に使えることは本格的なプログラミングに必須のスキルです。本章は関数への導入で、6 章 で関数の巧妙さについてさらに詳しく説明します。

まず、純関数とは数学の授業で関数と呼ばれていたもので、誰でも学生時代に経験したことがあると思います。コサインや絶対値を求めるのは引数が 1 つの純関数です。加算は引数が 2 つの純関数です。

純関数の最大の特徴は、同じ引数を与えると必ず同じものを返し、副作用は全くありません。引数に応じて値を返し、他のものをいじくり回すことはありません。

JavaScript では加算は演算子ですが、次のように関数でラップすることができます (本ドキュメントの何処とは言えませんが、実際に役立つ場面を見掛けることでしょう):

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

show(add(2, 2));

add は関数の名前です。a と b は引数の名前で、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));

exercise 2.2 を解いたら、累乗を計算するテクニックはよくご存知だと思います。

変数 (result) の作成とその更新は副作用です。純関数には副作用が無いと言いませんでしたっけ?

関数内で作られた変数は関数内でのみ存在します。これはプログラマーにとっては好都合で、そうでなければプログラマーは変数が必要になる度に異なる名前を考えなければならなくなります。result は power 内でのみ存在し、それへの変更は関数が返すまでしか続きません。それを呼び出したコードから観ると、副作用は全く存在しません。

Ex. 3.1

引数として与えられる数の絶対値を返す関数 absolute を書いてください。負数の絶対値は与えられた数の正数版で、正数 (またはゼロ) の絶対値がその数自身です。

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

show(absolute(-144));

純関数には 2 つの素晴らしい特徴があります。どんな純関数かを思い浮かべることが容易なことと、その再利用が簡単なことです。

純関数の場合、その呼び出しはそれ自身の中で行われます。正しく機能しているか不安な時は、コンソールから直接それを呼び出してテストすることができます。コンテキスト 2 に依存しないため、シンプルです。関数をテストするプログラムを書いて、それを自動化することは簡単です。非純関数は色々な要因によって異なる値を返し、考えるのもテストするのも難しい副作用があります。

純関数は自足的で、非純関数よりも広い範囲で役に立ちそうです。例えば show を例にとってみましょう。この関数の有益性はスクリーン上に表示できる場所が存在するかどうかで決まります。表示できる場所が無ければ、この関数は役に立ちません。関連する関数、format とでも呼んでおきましょう。これは引数を 1 つ取り、引数の値を表す文字列を返します。この関数は show よりも多くの場合に役立ちます。

勿論 format は show の問題の解決にはならず、いかなる純関数もその問題を解決できません。何故なら、副作用が必要だからです。多くの場合、我々が必要とするのは非純関数です。その他の場合は純関数でも問題は解決しますが、非純変数がずっと便利で効果的です。

このように、純関数で簡単に表現できる場合はその方法で書くべきです。しかし、非純関数を書くことが悪いことだとは考えないでください。

副作用を持つ関数に return ステートメントを含める必要はありません。return ステートメントがなければ、その関数は undefined を返します。

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

yell("Yow");

関数の引数名は関数内の変数として使うことができます。引数名は呼び出された関数の引数の値を参照し、通常の変数と同様、関数内で作成され、関数外では存在しません。最上位レベルの環境とは別に、小さなローカル環境が関数呼び出しによって作られます。関数内の変数を調べる際、まずローカル環境がチェックされ、そこに変数が無い場合に最上位レベルの環境がチェックされます。これにより、関数内の変数によって同じ名前を持つ最上位変数を '隠す (shadow)' ことが可能になります。

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 4 ではブロック内にある変数の定義について何らかの方法が追加されることになっています。

驚くような例があります:

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

var child = parentFunction();
child();

parentFunction はその内部関数を 返し、最後のコードでこの関数が呼び出されます。parentFunction がこの時点で実行を終了しても、variable が値 "local" を持つローカル環境は依然存在し、childFunction はまだそれを使っているのです。この現象を クロージャ (閉包)と呼びます。

プログラム・テキストの外形をみてすぐにプログラムのどの部分で変数を入手できるかを判別し易くする以外に、レキシカル・スコーピングは関数の '合成' を可能にします。内包する関数の変数をいくつか使って、内部関数の別のことをさせることができます。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度しか参照されないため、この名前は無意味で、関数値を直接返したほうがましです。

Ex. 3.2

数値を引数として取り、テストを表す関数を返す greaterThan 関数を書いてください。この返された関数が 1 つの数値を引数として呼び出されると、ブーリアンが返されます。与えられた数値がこのテスト関数を作るのに使われた数値よりも大きい場合は 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 の場合と同様、これについては言及しません。

翻訳元

Chapter 3: Functions
 Chapter 3: Functions
eloquentjavascript.net  

最終更新:

コメント(0

あなたもコメントしてみませんか?

すでにアカウントをお持ちの方はログイン