JavaScript はプロトタイプベースのオブジェクト指向言語ですが、関数型言語の機能も備えています。JavaScript は関数をオブジェクトとして扱うことができるので、関数を変数に代入したり、引数として渡すことができます。また、値として関数を返すこともできるので、関数を作る関数を定義することも簡単にできます。
関数を引数として受け取る関数を「汎関数 (functional) 」とか「高階関数 (higher order function) 」と呼びます。JavaScript は高階関数を簡単に定義することができます。今回は関数型言語でよく使われている高階関数の中で、マッピング、フィルター、畳み込み (縮約) について説明します。
まず最初に、引数の関数 fn() に配列の要素を渡して呼び出し、その結果を配列に格納して返す関数を作ってみましょう。このような操作を「マッピング (写像)」といいます。なお、関数に引数を与えて呼び出すことを、関数型言語では「適用」といいます。本稿でも関数呼び出しの意味で適用を使うことにします。プログラムは次のようになります。
リスト : マッピング function map(fn, ary) { let a = []; for (x of ary) a.push(fn(x)); return a; }
受け取った関数を呼び出す場合、JavaScript では特別なことを行う必要はありません。引数 fn を fn(ary[i]) のように関数として使うと、JavaScript は fn の値を関数として呼び出します。関数を渡す場合も簡単です。関数が定義されている変数を渡すだけでいいのです。
それでは実行例を示しましょう。
> function square(x) { return x * x; } undefined > map(square, [1, 2, 3, 4, 5]) [1, 4, 9, 16, 25]
引数を 2 乗する関数 square() を定義します。この関数を map() に渡すと、要素を 2 乗した新しい配列を返します。このように、JavaScript は高階関数を簡単に定義することができます。
フィルター (filter) は配列の要素に関数を適用し、関数が真を返す要素を配列に格納して返す関数です。関数型言語では、真または偽を返す関数のことを「述語 (predicate) 」といいます。ここでは簡単な例題として、述語が真を返す要素を削除する関数 remove_if() を作ってみましょう。関数名は Common Lisp から拝借しました。
リスト : 要素の削除 function remove_if(fn, ary) { let a = []; for (let x of ary) if(!fn(x)) a.push(x); return a; }
map() と同様に remove_if() も簡単です。fn(x) が真ならば x を配列 a に加えません。偽ならば x を配列に加えるだけです。簡単な実行例を示します。
> function evenp(x) {return x % 2 == 0;} undefined > function oddp(x) {return x % 2 != 0;} undefined > remove_if(evenp, [1,2,3,4,5,6]) [1, 3, 5] > remove_if(oddp, [1,2,3,4,5,6]) [2, 4, 6]
最初の例では偶数の要素が削除されます。次の例では奇数の要素が削除されます。
もちろん、フィルターも簡単に定義することができます。remove_if() とは逆に、述語が真を返すとき要素を配列に追加し、偽を返すときは配列に加えません。
リスト : フィルター function filter(fn, ary) { let a = []; for (let x of ary) if(fn(x)) a.push(x); return a; }
簡単な実行例を示しましょう。
> filter(evenp, [1,2,3,4,5,6]) [2, 4, 6] > filter(oddp, [1,2,3,4,5,6]) [1, 3, 5]
2 つの引数を取る関数 f() と配列を引数に受け取る関数 fold() を考えます。fold() は配列の各要素に対して関数 f() を下図のように適用します。
(1) [a1, a2, a3, a4, a5] => f( f( f( f( a1, a2 ), a3 ), a4 ), a5 ) (2) [a1, a2, a3, a4, a5] => f( a1, f( a2, f( a3, f( a4, a5 ) ) ) ) 図 : 関数 fold() の動作
関数 f() を適用する順番で 2 通りの方法があります。図 (1) は配列の先頭から f() を適用し、図 (2) は配列の後ろから f() を適用します。たとえば、関数 f() が単純な加算関数とすると、fold() の結果はどちらの場合も配列の要素の和になります。
f(x, y) = x + y の場合 fold() => a1 + a2 + a3 + a4 + a5
このように、fold() は配列のすべての要素を関数 f() を用いて結合します。このような操作を「縮約」とか「畳み込み」といいます。また、fold() の引数に初期値 g を指定することがあります。この場合、fold() は下図に示す動作になります。
(1) [a1, a2, a3, a4, a5] => f( f( f( f( f( g, a1 ), a2 ), a3 ), a4 ), a5 ) (2) [a1, a2, a3, a4, a5] => f( a1, f( a2, f( a3, f( a4, f( a5, g ) ) ) ) ) 図 : fold() の動作 (2)
ここでは簡単な例題として、上図 (1) の動作を行う関数 fold_left() と、上図 (2) の動作を行う関数 fold_right() を作ってみましょう。
プログラムは次のようになります。
リスト : 畳み込み function fold_left(fn, a, ary) { for (let x of ary) a = fn(a, x); return a; } function fold_right(fn, a, ary) { for (let i = ary.length - 1; i >= 0; i--) a = fn(ary[i], a); return a; }
fold_left(), fold_right() の引数 fn が適用する関数、a が初期値、ary が配列です。for ループで ary の要素を先頭から一つずつ取り出し、fn(a, x) を実行します。fold_left() は変数 a の値を fn() の返り値に更新することで、図 (1) の動作を実現しています。
たとえば、配列が [1, 2, 3] で a が 0 とします。最初は fn(0, 1) が実行され、その返り値が a にセットされます。次は fn(a, 2) が実行されますが、これは fn(fn(0, 1), 2) と同じことです。そして、その結果が a にセットされます。最後に fn(a, 3) が実行されますが、これは fn(fn(fn(0, 1), 2), 3) となり、図 (1) と同じ動作になります。
fold_left() の場合、配列の要素が関数 fn() の第 2 引数になり、第 1 引数にはこれまでの処理結果が渡されます。これに対し、fold_right() の場合は逆になり、関数 fn() の第 1 引数に配列の要素が渡されて、これまでの処理結果は第 2 引数に渡されます。これで図 (2) の動作を実現することができます。
簡単な実行例を示します。
> function add(x, y) { return x + y; } undefined > fold_left(add, 0, [1,2,3,4,5]) 15 > fold_right(add, 0, [1,2,3,4,5]) 15
fold_left(), fold_right() に関数 add() を渡すと、配列の要素の合計値を求めることができます。畳み込みは 2 引数の関数と組み合わせると、いろいろな処理を実現することができます。
高階関数を使うようになると、数を 2 乗する square のような小さな関数をいちいち定義するのが面倒になります。とくに、その高階関数でしか使わないのであれば、なおさらそう思うでしょう。JavaScript には Lisp の「ラムダ式 (lambda)」のような「匿名関数 (anonymous function)」という名前のない関数が用意されています。
匿名関数は次のように定義します。
function(仮引数, ...) { 処理A; ...; 処理Z; }
匿名関数は Lisp / Scheme のラムダ式が関数を返すのと同様に、関数オブジェクトを生成して返します。JavaScript は匿名関数をそのまま実行することができますし、関数の引数に匿名関数を渡すこともできます。簡単な例を示しましょう。
> (function(x) { return x * x; })(20) 400 > map(function(x) {return x * x;}, [1, 2, 3, 4, 5]) [1, 4, 9, 16, 25]
JavaScript の場合、匿名関数をそのまま実行するときは、匿名関数をカッコで囲んでください。そのあとに引数を指定します。map 関数を使う場合、わざわざ square() を定義しなくてもいいので簡単です。このように、匿名関数は高階関数と組み合わせて使うと便利です。
ES2015 では、匿名関数を => で表記することができます。これを「アロー関数 (Arrow Functions)」といいます。
(引数, ...) => { 処理; ... }
( ... ) の中に仮引数を指定します。引数が一つしかない場合、丸カッコを省略することができます。関数本体の処理は { ... } の中に記述します。値を返す場合は return 文を使います。また、処理 (式) がひとつしかない場合、波カッコと return 文を省略することができます。
簡単な実行例を示します。
> (x => x * x)(25) 625 > map(function(x) {return x * x;}, [1, 2, 3, 4, 5]) [ 1, 4, 9, 16, 25 ] > map(x => x * x, [1, 2, 3, 4, 5]) [ 1, 4, 9, 16, 25 ] > filter(x => x % 2 == 0, [1,2,3,4,5,6]) [ 2, 4, 6 ] > filter(x => x % 2 != 0, [1,2,3,4,5,6]) [ 1, 3, 5 ] > fold_left((a, x) => a + x, 0, [1,2,3,4,5,6]) 21 > fold_right((x, a) => a + x, 0, [1,2,3,4,5,6]) 21
function による関数定義は、匿名関数やアロー関数を使って次のように書き直すことができます。
リスト : function による関数定義 function square(x) { return x * x; }
リスト : 匿名関数 (アロー関数) による関数定義 const square1 = function(x) { return x * x; } const square2 = x => x * x
変数を const で宣言すると、関数定義の書き換えを防止することができます。簡単な実行例を示します。
> const square1 = function(x) { return x * x; } undefined > square1(100) 10000 > const square2 = x => x * x undefined > square2(1111) 1234321
このように、匿名関数で関数オブジェクトを生成し、その値を変数 square にセットすれば、square(100) のように関数として呼び出すことができます。これは Scheme の関数定義とよく似ています。
ただし、上記のプログラムは関数を定義する動作は同じですが、そのタイミングが異なります。通常の関数定義の場合、プログラムを読み込んだあと、それを実行する前に関数が定義されます。したがって、次のようなプログラムでも動作します。
リスト : 関数 square の実行 (1) var a = square(100); console.log(a); function square(x) { return x * x; }
上のプログラムは正常に動作して、10000 が表示されます。ところが次のリストはエラーになります。
リスト : 関数 square の実行 (2) var a = square(100); console.log(a); var square = function(x) { return x * x; }
この場合、関数呼び出しでエラーが発生します。匿名関数は定義文ではないので、プログラムを実行したときに関数オブジェクトが生成されます。
匿名関数で関数を定義する場合、square(100) を実行するとき、まだ変数 square には関数オブジェクトがセットされていないのでエラーになるわけです。ご注意くださいませ。
ここで、もう少し詳しくローカル変数の規則を見てみましょう。変数 x を表示する関数 foo() を定義します。
> function foo() { console.log(x); } undefined > x = 10 10 > foo() 10 undefined
foo() には変数 x を定義していないので、foo() を実行した場合グローバル変数の値を探しにいきます。それでは foo1() という関数から foo() を呼び出す場合を考えてみましょう。foo1() には引数 (ローカル変数) x を定義します。この場合、foo() はどちらの値を表示するのでしょうか。実際に試してみましょう。
> function foo1(x) { foo(x); } undefined >foo1(100) 10
グローバル変数の値を表示しました。このように、foo1() で定義されているローカル変数 x は、foo() からアクセスすることはできません。下図を見てください。
┌───── JavaScript system ─────┐ │ │ │ グローバル変数 x ←────┐ │ │ │ │ │ ┌→┌─ 関数 foo ──────┐ │ │ │ │ │ ┌─────┼─┘ │ │ │ │ console.log(x) │ │ │ │ └────────────┘ │ │ │ ┌─ 関数 foo1: x ────┐ │ │ │ │ │ │ │ └─┼─ foo() │ │ │ └────────────┘ │ │ │ └────────────────────┘ 図 : レキシカルスコープ
上図では、変数の有効範囲を枠で表しています。foo1() で定義したローカル変数 x は、関数 foo1() の枠の中でのみ有効です。もしも、この枠で変数が見つからない場合は、ひとつ外側の枠を調べます。この場合、関数定義の枠しかないので、ここで変数が見つからない場合はグローバル変数を調べます。
foo() は関数定義の枠しかありません。そこに変数 x が定義されていないので、グローバル変数を調べることになるのです。このように、foo() から foo1() の枠を超えて変数 x にアクセスすることはできないのです。これを「レキシカルスコープ (lexical scope) 」といいます。レキシカルには文脈上いう意味があり、変数が定義されている範囲内 (枠内) でないと、その変数にアクセスすることはできません。
それでは、匿名関数の場合はどうでしょうか。次のリストを見てください。
リスト : リストの要素を n 倍する function times_element(n, ary) { return map(x => x * n, ary); }
匿名関数の仮引数は x だけですから、変数 n はグローバル変数をアクセスすると思われるかもしれません。ところが、変数 n は関数 times_element の引数 n をアクセスするのです。下図を見てください。
┌───── JavaScript system ────┐ │ │ │ ┌─ times_element : n l ─┐ │ │ │ ↑ │ │ │ │ └─┐ │ │ │ │ ┌ function : x ─┐│ │ │ │ │ │ ↑ ││ │ │ │ │ │ ┌──┘ ││ │ │ │ │ │ x * n ││ │ │ │ │ │ └──┼┘ │ │ │ │ └────────┘ │ │ │ └─────────────┘ │ │ │ └───────────────────┘ 図 : 匿名関数の変数
ポイントは、匿名関数が関数 times_element() 内で定義されているところです。変数 n は関数の引数として定義されていて、その有効範囲は関数の終わりまでです。匿名関数はその範囲内に定義されているため、変数 n にアクセスすることができるのです。つまり、関数内で定義された匿名関数は、そのとき有効なローカル変数にもアクセスすることができるわけです。
もうひとつ簡単な例題を示しましょう。指定した文字 c が先頭にある文字列を、リストから削除する関数を作ってみましょう。最初に実行例を示します。
> remove_string("a", ["abc", "def", "agh", "ijk"]) [ "def", "ijk" ]
配列に格納された文字列の中で、a から始まる文字列を削除します。この処理は remove_if() と匿名関数を使うと簡単に定義できます。
リスト : 先頭文字が c の文字列を削除 function remove_string(c, ary) { return remove_if(x => c == x[0], ary); }
匿名関数の中で remove_string() の引数 c をアクセスできるので、このような定義が可能になります。
JavaScript は関数の中で別の関数を定義することができます。つまり、関数のネスト(入れ子)ができるわけです。入れ子の関数は局所的な関数として扱われるので、定義された関数の中でのみ有効です。他の関数から呼び出すことはできません。また、入れ子の関数は、匿名関数のように外側の関数のローカル変数にアクセスすることができます。
簡単な例として、入れ子の関数を使って times_element() を書き直してみましょう。次のリストを見てください。
リスト : リストの要素を n 倍する (2) function times_element(n, ary) { function _times(x) { return x * n; } return map(_times, ary); }
入れ子関数の定義は今までの関数定義と同じで、特別なことはありません。関数定義の中で、別の関数が定義されているだけです。関数 _times() は times_element() 内で定義されているので、_times() から times_element() の引数 n を参照することができます。
ちなみに、この処理は匿名関数を使って次のように書き換えることもできます。
リスト : リストの要素を n 倍する (3) function times_element(n, ary) { const _times = x => x * n; return map(_times, ary); }
JavaScript の場合、関数の中で this を使用することができます。たとえば、object.method() のように関数 (メソッド) を呼び出した場合、this には object がセットされます。関数呼び出しでも this を参照することができますが、その値は大域変数を格納しているオブジェクト (グローバルオブジェクト) になります。
> x = 100 100 > function foo() { return this.x; } undefined > foo() 100 > obj = {x: 123} { x: 123 } > obj.f = foo [Function: foo] > obj.f() 123
foo() と呼び出すと、this はグローバルオブジェクトになるので、x の値は 100 になります。obj を生成して、関数 foo() をプロパティ f にセットします。obj.f() を呼び出すと this の値は obj になるので、x は 123 になります。
call() や apply() を使うと、this の値を変更することができます。
> oops = {x: "oops!"} { x: 'oops!' } > foo.call(oops) 'oops!' > foo.call(obj) 123 > foo.call() 100 > foo.call(null) 100 > foo.call(undefined) 100
foo.call(oops) とすると、this の値は oops になるので oops! と表示されます。obj を渡すと 123 になり、引数無し (または null や undefined) だとグローバルな関数呼び出しになります。
new を使うと新しいオブジェクトが関数の this に渡されます。
> function bar() { this.x = 1.2345; } undefined > new bar() bar { x: 1.2345 } > bar() undefined > x 1.2345
new を付けずに bar() を呼び出すと this の値はグローバル変数になるので、変数 x の値を書き換えることになります。
局所関数の場合、匿名関数とアロー関数では this の値が異なるので注意が必要です。匿名関数の場合、this の値はグローバルオブジェクトになるので、外側の関数の this を参照することはできません。逆に、アロー関数は this をレキシカルスコープで管理するので、外側の関数の this を参照することができます。
> function baz() { ... var f1 = function() { console.log(this.x); }; ... var f2 = () => { console.log(this.x); }; ... f1(); ... f2(); ... } undefined > var x = 100 undefined > var obj = {x: 123} undefined > baz.call(obj) 100 123 undefined
匿名関数 f1 は大域変数 x の値 100 を表示しますが、アロー関数は baz() の this の値 (obj) を参照するので、obj のプロパティ x の値 123 を表示します。
グローバルな環境でアロー関数を定義すると、this の値はグローバルオブジェクトになります。この場合、メソッド形式の呼び出しでも、call() や apply() を使っても this の値はグローバルオブジェクトのままです。
> var func = () => { console.log(this.x); } undefined > func() 100 undefined > obj.f = func [Function: func] > obj.f() 100 undefined > func.call(obj) 100 undefined
詳細な説明はリファレンス this | JavaScript MDN をお読みくださいませ。
今回は「クロージャ (closure) 」について説明します。
クロージャは実行する関数と参照可能なローカル変数をまとめたものです。クロージャは関数のように実行することができますが、クロージャを生成するときに参照可能なローカル変数を保存するところが異なります。参照可能なローカル変数の集合を「環境 (environment)」と呼びます。
JavaScript の場合、関数はオブジェクトですが、それがクロージャになります。トップレベルで関数を定義するとき、ローカル変数は存在しないので、クロージャに保存される環境は空になると考えてください。匿名関数や局所関数もクロージャになります。関数内で匿名関数や局所関数を定義するとき、外側の関数に引数やローカル変数が定義されていれば、それらがクロージャに保存されます。
匿名関数や局所関数は、外側の関数の引数やローカル変数にアクセスすることができましたね。これはクロージャに環境が保存されているからできることなのです。つまり、レキシカルスコープはクロージャによって実現されているわけです。
ちなみに、レキシカルスコープを採用した最初の Lisp 処理系が Scheme です。拙作のページ「Scheme で作る micro Scheme」や「Common Lisp で作る micro Scheme」では、簡単な Scheme インタプリタを作成していますが、関数をクロージャとして扱うことでレキシカルスコープを実現しています。
また、Lisp / Scheme や関数型言語などは、関数内で新しい関数を生成して、それを返すことも簡単にできます。このときクロージャがとても役に立ちます。もちろん、JavaScript でも同様のことが可能です。
JavaScript で関数を返す関数を作るには、関数の中で「匿名関数」または「局所関数」を定義して、それを return で返します。たとえば、「引数を n 倍する関数」を生成する関数は、匿名関数を使うと次のようになります。
> function foo(n) { return x => x * n; } undefined > foo10 = foo(10) [Function (anonymous)] > foo10(1) 10 > foo10(10) 100 > foo5 = foo(5) [Function (anonymous)] > foo5(1) 5 > foo5(10) 50
関数 foo() は引数を n 倍する関数を生成します。変数 foo10 に foo(10) の返り値をセットします。すると、foo10 は引数を 10 倍する関数として使うことができます。同様に、変数 foo5 に foo(5) の返り値をセットすると、foo5 は引数を 5 倍する関数になります。
匿名関数で関数を生成するとき、評価する関数のほかに、そのとき参照可能なローカル変数、つまり「環境」もいっしょに保存されます。この場合、参照可能なローカル変数は foo() の引数 n です。そして、クロージャを実行するときは、保存されているローカル変数を参照することができるのです。
foo(10) を実行して匿名関数を生成するとき、定義されているローカル変数は n で、その値は 10 です。この値がクロージャに保存されているので、foo10 の関数は引数を 10 倍した結果を返します。foo(5) を評価すると n の値は 5 で、それがクロージャに保存されているので、foo5 の関数は引数を 5 倍した結果を返すのです。
局所関数を定義して返す場合は次のようになります。
リスト : マップ関数のカリー化 function map(func) { function _map(ary) { let result = []; for (let x of ary) result.push(func(x)); return result; } return _map; }
関数 map() は引数 func に関数を受け取り、その関数を呼び出すマップ関数を返します。局所関数 _map() はクロージャなので、map() の引数 func にアクセスすることができます。
簡単な実行例を示しましょう。
> map2 = map(function(x) { return x * x; }) [Function: _map] > map2([1, 2, 3, 4, 5]) [1, 4, 9, 16, 25] > map(x => x * x)([1,2,3,4,5]) [1, 4, 9, 16, 25]
最初の例は map() で生成した関数を変数 map2 にセットし、それから map2 を関数呼び出しします。次の例は、map() の返り値を直接関数呼び出ししています。カッコが多くなりますが、2 引数の map() と同じように呼び出すことができます。これでもリストの要素を 2 乗することができます。
2 番目の例は、最初の引数を受け取って新しい関数を生成して返し、その関数に次の引数を適用して値を求めるという動作になります。このように、関数の引数が一つでも、「関数を返す関数」を使うことで、複数の引数を処理することができます。このような関数を「カリー化関数 (curried function)」といいます。
関数型言語には、カリー化関数をサポートしているプログラミング言語、たとえば Haskell や ML (SML/NJ, Ocaml) などがあります。これらのプログラミング言語では、高階関数はカリー化関数として定義されています。また、関数を合成して新しい関数を作ることも簡単にできます。
クロージャの応用例として「ジェネレータ (generator) 」というプログラムを紹介しましょう。ジェネレータは、呼び出されるたびに新しい値を生成していきます。たとえば、JavaScript の関数 Math.random() は実行するたびに乱数を返します。つまり、random() は乱数列を発生する「ジェネレータ」と考えることができます。
簡単な例題として、奇数列 ( 1, 3, 5, ..... ) を発生するジェネレータを作ってみます。関数名は gen_odd_number としましょう。グローバル変数を使うと、次のようになります。
> function gen_odd_number() { return prev_number += 2; } undefined > prev_number = -1 -1 > gen_odd_number() 1 > gen_odd_number() 3 > gen_odd_number() 5
グローバル変数 prev_number は、gen_odd_number() が返した値を記憶します。新しい値は、この prev_number に 2 を足せばいいのです。
このように、グローバル変数を使うと簡単にジェネレータを作ることができますが問題点もあります。それは、複数のジェネレータが必要になる場合です。単純に考えると、必要な数だけグローバル変数と関数を用意すればいいのですが、数が増えるとグローバル変数や関数を定義するだけでも大変な作業になります。
ところがクロージャを使うと、もっとスマートにジェネレータを用意できます。まず、ジェネレータを作る関数を定義します。
リスト : ジェネレータを作る関数 function make_gen_odd_number() { let prev_number = -1; return () => prev_number += 2; }
関数 make_gen_odd_number() はクロージャを返します。そして、このクロージャがジェネレータの役割を果たすのです。それでは、実際に実行してみましょう。
> a = make_gen_odd_number() [Function (anonymous)] > a() 1 > a() 3 > a() 5 > b = make_gen_odd_number() [Function (anonymous)] > b() 1 > b() 3 > b() 5
make_gen_odd_number() で作成したクロージャを変数 a にセットして実行します。実行するたびに 1, 3, 5 と奇数列を生成していますね。次に新しいクロージャを変数 b にセットします。このクロージャを実行すると、新しい奇数列を生成します。確かにジェネレータとして動作しています。
このプログラムのポイントは、ローカル変数 prev_number です。クロージャで保存される環境は変数 prev_number です。この値は make_gen_odd_number が実行されたときに -1 で初期化されています。クロージャにはこの値が保存されます。
次は a にセットしたクロージャを実行します。匿名関数はクロージャに保存されたローカル変数にアクセスするので、prev_number += 2 の値は 1 になり、クロージャに保持されている prev_number の値は 1 に更新されます。
環境はクロージャによって異なります。a のクロージャが評価されると、そのクロージャの環境が更新されるのであって、ほかのクロージャに影響を与えることはありません。
したがって、ジェネレータが発生する奇数列が、ほかのジェネレータに影響を与えることはないのです。あとは必要な数だけジェネレータを make_gen_odd_number() で作り、そのクロージャを変数に格納しておけばいいわけです。
ところで、クロージャが保存するのは「環境」なので、JavaScript のように変数の値を書き換えることができるプログラミング言語でクロージャを使うときには注意が必要です。次のリストを見てください。
リスト : 繰り返しで複数の関数を生成 func = []; function make_func() { for (let i = 0; i < 5; i++) { func.push(() => console.log("foo" + i + "call")); } }
make_func() は匿名関数でクロージャを 5 つ生成し、それを配列 func に格納します。匿名関数の中では局所変数 i を参照します。配列に格納されたクロージャを実行すると、次のようになりました。
> func = [] [] > make_func1() undefined > func[0]() foo0call undefined > func[1]() foo1call undefined > func[2]() foo2call undefined > func[3]() foo3call undefined > func[4]() foo4call undefined
各クロージャの変数 i には異なる値が格納されていることがわかります。ところが、次のように for 文の前で変数 i を宣言すると異なる結果になります。
リスト : 繰り返しで複数の関数を生成 function make_func1() { let i = 0; for (; i < 5; i++) { func.push(() => console.log("foo" + i + "call")); } }
> func = [] [] > make_func1() undefined > func[0](); foo5call undefined > func[1](); foo5call undefined > func[2](); foo5call undefined > func[3](); foo5call undefined > func[4](); foo5call
どのクロージャも foo5call と表示されました。make_func1() の中で局所変数 i は一つしかありません。各クロージャは同じ変数 i を参照することになります。make_func1() で変数 i の値を書き換えると、クロージャで保持している i の値も当然変わります。したがって、どのクロージャでも変数 i の値は make_func1() が書き換えた 5 になるのです。
以前の JavaScript では、for 文の中で変数 i を var i と宣言しても、make_func1() と同じ動作になりました。他のプログラミング言語、たとえば、Python や Common Lisp などでも同じです。for 文でこのような動作をする今の JavaScript は凄いですね。ちょっと驚きました。
ちなみに、Scheme の繰り返し do を使うと make_func() と同じ動作になるのですが、それは do が末尾再帰で実装されているからです。JavaScript でも次のように再帰定義でプログラムすると正常に動作します。
リスト : 再帰呼び出しで複数の関数を生成 function make_func2(i) { if (i < 5) { func.push(() => console.log("foo" + i + "call")); make_func2(i + 1); } }
関数 make_func2() の場合、再帰呼び出しするたびに新しい環境が生成され、そこに引数 i が追加されます。クロージャはこの環境を保持するので、個々のクロージャに保存される環境は別のものになり、参照する変数 i の値は make_func2() を呼び出したときの値になります。
それでは実行してみましょう。
> func = [] [] > make_func2(0) undefined > func[0]() foo0call undefined > func[1]() foo1call undefined > func[2]() foo2call undefined > func[3]() foo3call undefined > func[4]() foo4call undefined
正常に動作していますね。
なお、SML/NJ, OCaml, Haskell などの関数型言語は、関数の引数や変数の値を書き換えることができません。手続き型言語は代入により変数の値を書き換えることができますが、純粋な関数型言語に代入操作はありません。当然ですが、クロージャに保存された変数の値も書き換えることはできません。ご注意くださいませ。
最後に、再帰を使った面白い関数を紹介しましょう。次のリストを見てください。
リスト : たらいまわし関数 function tarai(x, y, z) { if (x <= y) return y; return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y)); } function tak(x, y, z) { if (x <= y) return z; return tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y)); }
tarai や tak は「たらいまわし関数」といって、再帰的に定義されている関数です。これらの関数は Lisp などでベンチマークとして利用されることがあります。tarai は通称「竹内関数」と呼ばれていて、日本の代表的な Lisper である竹内郁雄氏によって考案されたそうです。そして、tak は tarai のバリエーションで、John Macarthy 氏によって作成されたそうです。
たらいまわし関数がベンチマークで使われることは M.Hiroi も知っていましたが、このような由緒ある関数だとは思ってもいませんでした。
それでは、たらいまわし関数を Node.js で実行してみましょう。実行時間を求めるため Date.now() を使います。
リスト : 時間計測 function test(f, x, y, z) { let s = new Date().getTime(); console.log(f(x, y, z)); let e = new Date().getTime(); console.log(e - s); }
このメソッドは現在時刻をミリ秒単位で求めることができます。結果は次のようになりました。
tarai(14, 7, 0) : 2.81 秒 tak(22, 11, 0) : 3.08 秒
他の JavaScript 処理系では異なる結果になるかもしれません。興味のある方は試してみてください。
たらいまわし関数が遅いのは、同じ値を何度も計算しているためです。この場合、表 (table) を使って処理を高速化することができます。同じ値を何度も計算することがないように、計算した値は表に格納しておいて、2 回目以降は表から計算結果を求めるようにします。このような手法を「表計算法」とか「メモ化」といいます。
JavaScript は関数型言語の機能を備えているので、関数を「メモ化」するメモ化関数を簡単に作成することができます。次のリストを見てください。
リスト : メモ化関数 function memoize(func) { let table = new Map(); function _memo_func(...key) { if (!table[key]) table[key] = func(...key) return table[key]; } return _memo_func; }
関数 memoize() は関数 func() を引数に受け取り、それをメモ化した関数を返します。memoize() は局所関数 _memo_func() を返すので、その中で memoize() の引数 func や局所変数 table にアクセスすることができます。
_memo_func() の引数 key がマップ table のキーになります。JavaScript の Map は配列をキーにすることができます。そして、table[key] が偽の場合は func を呼び出して値を求めます。スプレッド演算子 ... を使って配列 key を分解して func() に渡します。あとは table[key] の値を返すだけです。
関数 memoize() の使い方は簡単です。次の例を見てください。
リスト : メモ化関数の使い方 tarai = memoize(tarai); tak = memoize(tak);
とても簡単ですね。それでは実際に実行してみましょう。
tarai(14, 7, 0) : 0.002 秒 tarai(200, 100, 0) : 0.292 秒 tak(22, 11, 0) : 0.005 秒 tak(200, 100, 0) : 0.768 秒
このようにメモ化すると、たらいまわし関数はとても速くなります。
関数 tarai() は「遅延評価」を行う処理系、たとえば関数型言語の Haskell では高速に実行することができます。また、Scheme でも delay と force を使って遅延評価を行うことができます。
tarai のプログラムを見てください。x <= y のときに y を返しますが、このとき引数 z の値は必要ありませんね。引数 z の値は x > y のときに計算するようにすれば、無駄な計算を省略することができます。なお、関数 tak は x <= y のときに z を返しているため、遅延評価で高速化することはできません。ご注意くださいませ。
完全ではありませんが、JavaScript でもクロージャを使って遅延評価を行うことができます。次のリストを見てください。
リスト : クロージャによる遅延評価 function tarai(x, y, z) { if (x <= y) return y; let zz = z(); return tarai(tarai(x - 1, y, () => zz), tarai(y - 1, zz, () => x), () => tarai(zz - 1, x, () => y)); }
遅延評価したい処理をクロージャに包んで引数 z に渡します。そして、x > y のときに引数 z を評価 (関数呼び出し) します。すると、クロージャ内の処理が実行されて z の値を求めることができます。たとえば、() => 0 を z に渡す場合、z() とすると返り値は 0 になります。() => x を渡せば、x に格納されている値が返されます。() => tarai( ... ) を渡せば、関数 tarai が実行されてその値が返されるわけです。
それでは、実際に実行してみましょう。tarai(400, 200, 0) という大きな値を与えて実行しました。
test(tarai, 200, 100, () => 0) : 0.003 秒
このように、たらいまわし関数は遅延評価でも高速化することができます。