2009-04-20
Lesson3 関数を知れ! - Eloquent JavaScript
なんとなく復活
もうすぐ子供が生まれるって事で、どうも最近は教育関係にばっか目がいってな。MITのScratchだの、ContextFreeだのProcessingだの、、何かイロイロと気になるものが多くて正直なところ家にいる間はJavascriptどころじゃなかったんだが、、、実は最近仕事でちとつまづいてな。やっぱJavascriptについては本格的に理解しとかないと駄目なんじゃねーのか?オイ、オレ。みたいな感じになりつつあるので、Eloquent JavaScriptについてもちょっとじっくりと読んでみようという事と相成った。んで、読むならまぁ、超訳もついでにやっとくか?って事で今回の復活となった次第。逆に言えばGoogle App Engineでアプリを作る上で特に苦労が無いPythonの方は「Dive Into Python で勉強せねば!」というモチベーションが全く働かずに放置されている有様で関係各位には大変申し訳ない、、とか思ってたらこのサイトに結構な分量の和訳が置いてありますぜ兄貴!という事でオレはやっぱりJavascript中心に攻めるコトにする。
Pythonよ、そいぎんた(佐賀弁で「さようなら」という意味らしい)。
今日も元ネタ提供者の Marijn Haverbeke さんに感謝しつつレッスンを進めるぞ。あくまで超訳ってコトを頭に入れてもらった上でついてきな!
で、早速本題
プログラムを書いてると、違う場所で同じ処理をするって場面に結構出くわすよな。だが、ここで全ての文(statement)を毎回毎回繰り返して使うってのは、ウザいしエラーの温床になりやすいんじゃね?と思うワケだ。同じ処理なら一箇所に置いちまって、必要になったらプログラムを回り道させてそこを通らせるようにした方がイロイロ良さそうだよな?コレが関数が発明された理由ってワケだ。関数ってのは、必要なときにはいつでも中身が調べられるコードの缶詰みたいなもんだな。スクリーンに文字を描画しようと思ったら結構な数の文が必要になるところなんだが、print関数があれば単に「print("うほっ")」と書くだけでOK。有り難いもんだな。
単に関数をコードの缶詰として見るだけってのは、正しい理解とはいえねーぞ。必要に応じ、純粋な関数として、はたまたアルゴリズムだのモジュールだのデータ構造だのと、より多くの役割を演ずることができるようになってるからな。関数を巧く使うってコトは、あらゆるプログラミングにおいて必要なことになるぜ(お手軽なもんは除くケドな)。今回のレッスンでは主にさわりの部分について学ぶだけだ。第6回に更に関数の奥深い話題について触れる予定になってるぜ。
・・・・
まずは手始めに純粋な関数の話からな。コイツらは、オマエが人生の中で何度かつまづいたんじゃねーかと思われる数学の授業で出てきた関数ってヤツだ。コサインの計算だの、数の絶対値の算出だのってのは純粋な関数だ。ついでに言えば、足し算も2つの引数を持つ関数だ。
純粋な関数ってヤツらの性質は、同じ引数が与えられた場合に常に同じ値を返し、副作用は決して発生しないってところにある。ヤツらはいくつかの引数をとって、その引数をベースにした値を返すってコトをするダケで、他には何もしやしない。
Javascriptにおいて足し算ってのは演算子になってるが、こんな風に関数にする事だって出来るぞ(見た感じ意味ねー感じがするだろうが、役に立つ場合があるんだな、コレが。)。
function add(a, b) { return a + b; } show(add(2, 2));
add ってのは関数の名前。aとbってのは2つの引数それぞれの名前って事になってる。「return a + b」ってのは関数の本体だ。
キーワード「function」ってのは新しく関数を作る場合には常に使うもんだ。で、その後に引き続いて変数名が来る場合、作られた関数はその名前で保存されることになる。名前の次に来るのは引数のリストだ。で、最後に関数の本体がくるってワケ。「while」ループや「if文」と違って関数本体を囲む括弧は「{}」になるぜ。
キーワードretrun(この後に式が続く)は、関数が返却する値を指定するのに使われるようになってる。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を解いてたんなら、このやり方には見覚えがないと困るぜ。
ところで、結果を示す変数を作ったり、それを更新したりってのは副作用だ。オレ、さっき純粋な関数には副作用がないとかって言ってなかったっけか?
関数内につくられる変数は、関数内だけに存在してるもんだ。これはラッキーだぜ。そうじゃなきゃ、プログラマはプログラム中で使われるあらゆる変数の名前が被らないようにケアしなきゃなんねーんだからな。とにかく、関数powerの中にだけresultってのは存在するようになってるワケだから、関数が返り値を戻すまで、コイツはずっと変化し続けることになる。こんな仕組みになってるから、この関数を呼び出してるコードの観点からすると、副作用は無いって事になるワケだ。
・・・・
練習問題 3.1
関数 absolute を書いてみろ。コイツは、引数として与えられた数字の絶対値を返す関数だ。負の数の絶対値は、同じ数の正の値ってコトになるな。で、正の数(あるいはゼロ)の絶対値は、その数自身ってワケだ。
【例によって回答は元ネタ側を参照のコト】
・・・・
純粋な関数には、2つのかなりイケてる特性がある。まず第1に、設計を考えるのが簡単だ。そして第2に、再利用するのが簡単だ。
関数が純粋なものなら、その呼び出しはモノと同じ扱いとして見なせる。関数の動作に疑いがあるなら、コンソールから直接それを呼び出すことでテストができるぞ。このテストは、文脈への依存がないから単純だ。だから、特定の関数のテストプログラムを自動化して記述するなんてのは簡単だ。逆に、非純粋な関数の場合、色々な要因に応じて返却される値が変わる可能性があるし、副作用だってあるかも知れない。だから、テストも設計も大変さぁ。
純粋な関数ってヤツは独り立ちしてるからな、非純粋な関数よりも適用範囲が広そうだよな。たとえば、関数showを見てみるぞ。この関数の有用性は、結果を出力するスクリーンに依存してる。出力先となる場所がないなら、この関数は役立たずだ。同様な関数を考えてみるぞ。ソイツを関数formatと呼ぶとしようか。コイツは引数として値を取り、その値を意味する文字列を返す。この関数は、showより多くのシチュエーションで使えるハズだ。
もちろん、関数formatはshowと同じ問題を解決するためのもんじゃあない。それに、純粋な関数は副作用を必要とするような問題の解決には使えないもんだしな。多くの場合、非純粋な関数ってヤツが必要になる。そうじゃない場合もあるにはあるが、非純粋な関数の方がずっと便利か、あるいは効率的だ。
っつーワケで、何かが純粋関数として簡単に表現出来るなら、そうやって書いてくれ。だが、言葉に惑わされて非純粋な関数を書く事を汚いとか感じるんじゃねーぞ。あくまで適材適所だ。
・・・・
ちなみに、副作用を持つ関数は別に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ではそれは"No"だ。関数は、新しいスコープをつくる唯一のものだ。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"は、まだそっちの値を使う。こういう現象をクロージャって呼ぶんだぜ。
・・・・
プログラムテキストの形を見ることで手早く変数がプログラムのどの箇所で利用できるか見ることを凄く簡単にしてくれるって話とは別に、レキシカルスコーピングも、関数を統合するのに役立ってるぞ。囲んでいる関数内の変数を使うことで、異なる処理をするような内部関数が書けるからな。例えば、中身は違うが似たような関数をいくつか必要とするケースを考えてみるとイイ。(引数に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));
・・・・
異なる関数達が不整合なく同じ名前の変数を持つことができるってコトで、これらのスコーピング規則は、関数が自身を問題なく呼び出せるってコトも許容してくれてる。ちなみに関数が関数自身を呼ぶような形を、再帰的とか言うらしいぞ。再帰を使うと面白い定義が出来る。この関数"power"の実装を見てみろ。
function power(base, exponent) { if (exponent == 0) return 1; else return base * power(base, exponent - 1); }
これは数学者が累乗を定める方法にむしろ近いもんだ。そして、オレにとっては以前のヤツよりもっとイケてる感じがする。ループの一種なんだが、while文もなければfor文もない。そして目に見えたローカル副作用さえない。自身を呼び出すことで関数は同じ処理をするってワケだ。
ただ、重要な問題が1つある。大部分のブラウザでは、この第2のバージョンは、最初のヤツよりおよそ10倍処理が遅い。JavaScriptでは、単純なループを使う方が、再帰で複数回関数を呼び出すよりも安上がりってコトだな。
・・・・
処理速度をとるか、コードのエレガントさをとるか?ってジレンマは興味深い議論だな。これは別に for文にするか再帰を使うかって2択を決める時だけの話じゃねーぞ。多くの場合、エレガントで、直観的で、しばしばコードとしても短くなる手法は、コードとしては複雑だが、より処理速度の速い手法と取り替えられ得る。
上記関数"power"の場合、非エレガントなバージョンでも十分に単純で、読むのが簡単だな。再帰的なバージョンと入れ替えだけ意味は殆どないだろう。だが、多少の効率を犠牲にしたとしてもプログラムを扱いやすいようにしておく事が魅力的な選択肢だと思えるような複雑性に直面することが結構あるもんだ。
基本的なルールとしては、明らかにプログラムが遅すぎるって話じゃない限り、速度(処理効率)の話は気にしないでイイと思うゼ。で、速度が遅すぎるって場合は、どの部品が問題になってるのかを突き止めろ。で、その部品を効率優先なものと交換するようにするとイイぞ。
もちろん、今言ったルールは、必ずしもパフォーマンスを無視しなきゃならんなんてことを意味してるワケじゃねーぞ。多くの場合、関数"power"と同様『エレガントな』アプローチで単純さが得られるってワケじゃねーしな。ベテランプログラマなら、単純なアプローチが十分な速度を得られないって事にすぐ気が付く、なんて場合もある。
オレがこんな話をワザワザしてるワケは、驚くほど多くのプログラマが熱狂的なまでに効率に集中してる(すっげー細かい話の時でさえ)って現状があるからだ。その結果として出てくるものは、より大きくて、より複雑で、しばしばより不正確なプログラムだ。そして、等価のコードよりちょっとばかり早く動くだけなのに、無駄に時間をかけて書いてたりする。
・・・・
でもまぁ、今の本題は再帰だったな。話を戻そう。
再帰と密接に関連した概念として、スタックって呼ばれてるものがあるぞ。関数が呼び出される際、コントロールは呼び出された関数本体に与えられるコトになる。で、関数本体が返される時、その関数を呼び出したコードが再開される。関数本体が動いている間、その処理の後にどこから続けて処理をするのか?を知っておく為に、コンピュータは関数が呼び出されたコンテクストを記憶しておかなきゃならん。このコンテクストが保存される場所は、スタックと呼ばれてるんだ。
『スタック』と呼ばれているという事実は、見ての通り、関数本体が関数を再び呼び出すことができるってコトに関係がある。関数が呼ばれるたびに、別のコンテクストが保存されなけりゃならん。これを、コンテクストのスタックとして想像できるだろ?関数が呼び出される度に、現在のコンテクストがスタックの上に投げられる。関数が値を返す際、コンテクストのスタックのトップにあるものが取り除かれて、処理が再開されるってワケだ。
このスタックは、コンピュータメモリ上のスペースに保存されることを要求してる。そんなワケで、スタックがあまりに大きくなり過ぎた場合、コンピュータは「スタックスペース不足」或いは「再帰が多過ぎ」のようなメッセージを出して処理を放棄するようになってるぞ。これは、再帰的な関数を記述するとき、覚えておくべきだ。
function chicken() { return egg(); } function egg() { return chicken(); } print(chicken() + " came first.");
コイツは単に動かないプログラムの例として見せてるダケじゃねーぞ。関数それ自身が直接自分を呼び出すコトを再帰っていうワケじゃねーぞ、ってコトも示してる。最初の関数を呼び出すような関数を呼んでいる場合(直接だろうと間接的だろうと)、それもまた再帰だ。
・・・・
こんなパズルを考えてみてくれ。1から始まって、5も足すか、3を掛けるかの繰り返しを行う事で、新しい数を無限に生み出していくような処理だ。この足し算と掛け算の組み合わせで、任意の値に達する為の計算順序を導き出す為の関数はドウやって書けばイイだろうな?
たとえば、最初に1に3を掛け算し、その結果に5を足す処理を2回すると13になる。これだと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));
別に最も短い計算順序を見つけなきゃならんって話じゃない点に注意してくれよ。どんな計算順序だろうと発見出来りゃOKだ。
内部関数"find"は、2つの異なる方法で自身を呼び出すことによって、現在の数に5を加えるか、3を掛け算するべきか、両方の可能性を探るような形になってる。で、目的の数にたどり着いた時に、それまでの計算の履歴として記録された全ての演算子を返すようになってる。コイツは同時に現在の数字がgoalより大きいかどうかも調べるぞ。もしそうなら、答えにはたどり着かないワケだからな。
例の中では ||演算子を使ってるワケだが、コイツは 『まず始めに5を足し算するところから作り出した解法を返してみる。それでダメなら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."; }
コレはドウいう事かって言うと、残りのプログラムを実行し始める前に、起コンピュータがすべての関数定義を調べて、付随する関数を格納してるってことだ。同じことが、他の関数内に定義される関数に対しても起こる。外の関数が呼ばれる際、最初に起こる現象は、内部関数が新しい環境に加えられるってコトだ。
・・・・
関数定義にはもう1つやり方がある。そして、その方法ってのは、他の値が生成される時の方法に似てるんだ。キーワード"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; }; }
第1バージョンの関数"makeAddFunction"において"add"と名づけられた関数だが、コイツは1回しか参照されてない。名前を付けたところで何かを解決してるコトもないし、関数を直接返したほうが良さそうだ。
練習問題 3.2
1つの引数(数字)を取り、テスト関数を返す関数"greaterThan"を書け。この関数"greaterThan"から返された関数が、1つの引数と供に呼び出された際、ブーリアンを返す(テスト関数の生成に使われた数よりも得られた値が大きい場合には"true"を、そうでない場合には"false"を返す。)
【例によって回答は元ネタ側を参照のコト】
・・・・
ほんじゃ最後な。
コレをちょっと試してみな。
alert("Hello", "Good Evening", "How do you do?", "Goodbye");
関数"alert"は公式には1つの引数だけを取ることになってる。まぁ、こんな風に呼び出したところでコンピュータは何も文句を言わず、単に他の引数を無視するダケなんだけどな。
show();
もちろん、引数が不足な状態での関数呼び出しだって出来る。その場合には"Undefined"が返されるようになってるぞ。
次回は、関数が正確な引数のリストを受け取れるようにする方法について確認していくぞ。関数が受け取れる引数の数に制限がなくなると便利だよな。関数"print"はそれが出来るようになってるぜ。
print("R", 2, "D", 2);
勿論、逆に言えば関数"alert"の時みたく、取れる引数の数が決まっているような関数に対して、誤って異なる数の引数を渡してしまい、それが分からないまま処理が進んじまう、ってコトもあり得るワケだがな。
・・・・
ああ、それから忘れてるかも知れないんで最後に。以前も言ったが、このテキストの文章で何か間違いとか気になるところがあった場合、それは多分訳者の問題だろう。だから、くれぐれも先走って元ネタの作者であるMarijn Haverbeke氏に「(゚Д゚)ゴルァ!」すんじゃねーぞ。そんなオマエにはオレが「(゚Д゚)ゴルァ!」だ。
それじゃこれからも気合入れて行くぞ!!
さぁ、声を出せ!
Go! ニコガク、Go!!
次回につづく
gocho
2012/01/11 18:26
powerは「整数の階乗」ではなくて「整数の冪乗」ですね
