前回は高階関数について説明しました。今回は「クロージャ (closure)」を取り上げます。簡単に説明すると、クロージャは評価する関数と参照可能な局所変数を結びつけたものです。クロージャは関数と同じく評価することができますが、クロージャを生成するときに参照可能な局所変数を保持しているところが異なります。参照可能な局所変数の集合を「環境 (environment)」と呼ぶことがあります。
Clojure の場合、関数の実体は無名関数であり、無名関数を評価するとその値はクロージャになります。そして、クロージャを変数に代入するだけで関数を定義することができます。一般に、defn で大域的な関数を定義する場合、その時点で定義されている局所変数はないので、クロージャを意識することはありません。
関数の中で局所的な関数を定義する、または関数の中で無名関数を使うとき、クロージャを意識して使うとプログラムが簡単になる場合があります。とくに、高階関数と組み合わせて使うとき、クロージャはとても大きな効果を発揮します。また、クロージャを使うと関数を生成する関数を簡単に作ることができます。
クロージャは初心者の方にとってちょっと難しい概念かもしれません。クロージャを正しく理解するポイントのひとつに「局所変数の有効範囲」があります。まず最初に、局所変数のアクセスについて、詳しく説明することにしましょう。
次のように、変数 x を表示する関数 foo を定義します。
user=> (def x 10) #'user/x user=> (defn foo [] (println x)) #'user/foo user=> (foo) 10 nil
関数 foo には変数 x の定義はありません。したがって、foo を実行した場合は x を大域変数として扱うので 10 と表示されます。それでは foo1 という関数から foo を呼び出す場合を考えてみましょう。foo1 には局所変数 x が定義されています。この場合 foo はどちらの値を表示するのでしょうか。
user=> (defn foo1 [] (let [x 100] (foo))) #'user/foo1 user=> (foo1) 10 nil
大域変数の値を表示しました。つまり、foo1 で定義した変数 x は foo からアクセスすることはできないのです。簡単なことのようですが、実はとても大事なことなのです。これを図に示すと、下図のようになります。
変数の有効範囲を表すのに「スコープ (scope)」という用語 [*1] を使います。そして、このようなスコープ規則を「レキシカルスコープ (lexical scope)」といいます。レキシカルとは文脈上という意味があり、変数が定義されている位置によって、その変数にアクセスできるかどうかが決まります。
┌────── Clojure system ──────┐ │ │ │ 大域変数 x ←────┐ │ │ │ │ │ ┌→┌─ 関数 foo ──────┐ │ │ │ │ │ ┌──────┼─┘ │ │ │ │ (println x) │ │ │ │ │ │ │ │ │ └────────────┘ │ │ │ ┌─ 関数 foo1 ─────┐ │ │ │ │ │ │ │ │ │ ┌─let : x ───┐ │ │ │ │ │ │ │ │ │ │ └─┼─┼─ (foo) │ │ │ │ │ └────────┘ │ │ │ └────────────┘ │ │ │ └────────────────────┘ 図 : レキシカルスコープ
近代的な関数型言語では、関数の仮引数や let などで定義した局所変数は、レキシカルスコープで扱われます。次の例を見てください。
(defn foo2 [a] ────────── (let [b 20] ────── ↑ ↑ │a:10 ・・1・・ │b:20 │ │ │ (let [c 30] ── │ │ ↑c:30 │ │ ・・2・・ ↓ ↓ │ )) ────── │ │ ・・3・・ ↓ ) ───────── 図 : 局所変数の有効範囲 (その1)
たとえば、(foo2 10) を評価したとしましょう。仮引数 a には 10 がセットされますね。変数 a の値は関数 foo2 の全体で有効です。つまり、foo2 の中で変数 a にアクセスすると、その値は 10 となります。次に、let で局所変数 b を定義しています。変数 b は、それを定義した let の中だけが有効です。同様に局所変数 c を let で定義します。
1 の位置では、局所変数は a と b の 2 つが定義されていますが、c は定義されていません。この位置で a と b にアクセスすると、値はそれぞれ 10 と 20 になりますが、c は大域変数として扱われます。2 の位置では 3 つの局所変数 a, b, c が定義されているので、その値は 10, 20, 30 となります。3 の位置では、局所変数 b と c を定義した let の範囲外ですね。この場合、アクセスできる局所変数は a だけであり、変数 b と c は大域変数として扱われるのです。
つまり、局所変数はそれを定義した S 式の範囲内でのみ有効なのです。関数の仮引数であれば、それを定義した defn の S 式が範囲内であり、let の場合も同じです。定義されていない変数は、すべて大域変数として扱われます。このように、S 式という文脈から局所変数のアクセスを決定できるので、レキシカルスコープ [*2] と呼ばれます。
ところが、伝統的な Lisp はレキシカルスコープではありません。たとえば最初の例で、foo1 で定義した変数 x は、呼び出された関数 foo からもアクセスすることができます。これを「ダイナミックスコープ (dynamic scope)」といいます。foo1 で定義された変数 x は、foo1 の実行が終了するまで存在し、foo1 から呼ばれた関数ならば、どこからでもアクセスすることができるのです。これを図に示すと次のようになります。
┌────────────┐ ┌─定義された局所変数─┐ │(defun foo () (print x))│ │ () │ └────────────┘ └───────────┘ 定義された変数はなし (1) foo を評価する ┌────────────┐ ┌─定義された局所変数─┐ │(defun foo1 () │─→│ ((x 100)) │ │ (let ((x 100)) (foo))) │ └───────────┘ └────────────┘ ↓ 呼び出し ┌────────────┐ ┌─定義された局所変数─┐ │(defun foo () (print x))│←─│ ((x 100)) │ └────────────┘ └───────────┘ x は 100 となる foo で定義した変数 x (2) foo1 から foo を呼び出す 図 : ダイナミックスコープ
defun は Clojure の defn に相当する Lisp の関数です。ダイナミックスコープの場合、局所変数はひとつの連想リストで管理されていると考えてください。この場合、キーが局所変数で、データがその値に対応します。局所変数にアクセスするときは、この連想リストから該当する変数を探します。見つからない場合は、大域変数として扱われることになります。
関数呼び出しや let などで局所変数が定義されたとき、変数とその値が連想リストの先頭へ追加されます。そして、関数や let の評価が終了したときに、連想リストから変数が削除されます。関数が呼び出されるたびに、新しい変数が連想リストに追加されますが、呼び出した側で定義した局所変数もこの連想リストの中に残っていることに注意してください。
たとえば、(1) のように、関数 foo を呼び出した場合は、関数の引数がなくて局所変数の定義もないので、連想リストは空です。ところが、(2) のように関数 foo1 を呼び出した場合は、局所変数 x が定義されているので、連想リストに (x 100) がセットされます。この状態で関数 foo を呼び出すと、連想リストには foo1 で定義した変数 (x 100) が残っているので、foo では x の値が 100 となるのです。
参照可能な局所変数の集合を「環境 (environment)」といいますが、この局所変数を定義した連想リストを環境と考えるとわかりやすいでしょう。レキシカルスコープの場合は、関数呼び出しが行われるたびに、新しい連想リストが用意され、そこに仮引数や let などで定義された局所変数がセットされます。
ところが、ダイナミックスコープの場合は、ひとつの連想リストで局所変数を管理するので、変数のアクセスは関数を評価する順番に左右されます。したがって、関数の中で定義されていない変数があっても、それが大域変数として扱われるとは限らないのです。
なお、連想リストは説明のために用いたもので、今の実用的な Lisp / Scheme 処理系は、連想リストよりも効率的な方法で局所変数を管理していると思います。ただし、簡単な Lisp 処理系を自作する場合、局所変数の管理に連想リストを使う方法は有効です。興味のある方は拙作のページ「Scheme で作る micro Scheme」をお読みください。
今度は無名関数の場合を考えてみます。次の例を見てください。
リスト : リストの要素を n 倍する (defn times-element [n ls] (map (fn [x] (* x n)) ls))
無名関数は関数呼び出しと同じ働きをします。無名関数の仮引数は x だけですから、無名関数内の変数 n は大域変数をアクセスするように思われるかもしれません。ここで、無名関数が定義されている位置に注目してください。無名関数は、関数を定義している S 式の中で定義されています。この場合、変数 n は関数 times-element の仮引数 n をアクセスするのです。これを図に示すと、次のようになります。
┌────── Clojure system ─────┐ │ │ │ ┌─ times-element : n ls ─┐ │ │ │ ↑ │ │ │ │ └─┐ │ │ │ │ ┌─── fn : x ─┐│ │ │ │ │ │ ↑ ││ │ │ │ │ │ ┌──┘ ││ │ │ │ │ │ (* x n) ││ │ │ │ │ │ └───┼┘ │ │ │ │ └────────┘ │ │ │ └─────────────┘ │ │ │ └───────────────────┘ 図 : 無名関数内の変数
関数 times-element の中に無名関数が包み込まれていると考えてください。このように関数内で定義されている無名関数は、その時点で定義されている局所変数にアクセスすることができるのです。次の図を見てください。
(defn foo [a] ───────── (let [b ...] ────── ↑ ↑ │a ・・1・・ │b │ │ │ (map (fn [c] ── │ │ ↑c │ │ ・・2・・ ↓ ↓ │ ) b)) ────── │ │ ・・3・・ ↓ ) ───────── 図 : 局所変数の有効範囲 (その2)
1 の位置では、関数 foo の仮引数 a と let で定義された変数 b が、局所変数として扱われます。次に、無名関数内である 2 の位置では、変数 a と b に加えて、無名関数の仮引数である c が局所変数として扱われます。a, b, c 以外の変数は大域変数として扱われます。
もうひとつ簡単な例題をあげましょう。指定した文字 code が先頭にある文字列を、リストから削除する関数を作ってみましょう。これは、次に示すような処理を行います。
user=> (remove-string \a '("abc" "def" "agh" "ijk")) ("def" "ijk")
Clojure の場合、'\ + 文字' を「文字型データ (character)」として扱います。リストに格納された文字列の中で、a から始まる文字列を削除します。まず、この処理を再帰定義で作ってみましょう。処理手順は次のようになります。
│\a ("abc" "def" "agh" "ijk") │ = ("def" "ijk") 再 ↓ ↑ 帰 │\a ("def" "agh" "ijk") │ 呼 │ X ─────→ ("def" "ijk") び ↓ ↑ 出 │\a ("agh" "ijk") │ し │ = ("ijk") ↓ ↑ │\a ("ijk") │ │ X ─────→ ("ijk") ↓ ↑ \a () () ───→ () ─┘ 図 : remove-string の処理手順
引数の文字と文字列の先頭文字が異なっていれば、それをリストに格納して返せばいいわけです。これをプログラムすると、次のようになります。
リスト : 指定した先頭文字の文字列を削除 (defn remove-string [c ls] (cond (not (seq ls)) '() (= (first (first ls)) c) (remove-string c (rest ls)) :else (cons (first ls) (remove-string c (rest ls)))))
文字列の先頭文字は関数 first や nth で取得することができます。簡単な例を示しましょう。
user=> (first "abc") \a user=> (nth "abc" 0) \a user=> (remove-string \a '("abc" "def" "agh" "ijk")) ("def" "ijk")
この処理は remove と無名関数を使うと簡単に定義できます。
リスト : 指定した先頭文字の文字列を削除 (defn remove-string [c ls] (remove #(= (first %) c) ls))
無名関数内で remove-string の仮引数 c をアクセスできるので、このようなプログラムが可能になるのです。無名関数と高階関数をうまく組み合わせると、複雑な処理でも簡単にプログラムすることができます。
Clojure では、関数の中で別の関数を定義することができます。これを「局所関数 (local function)」といいます。局所関数の中では、外側の関数の局所変数にアクセスすることができます。たとえば、関数 foo の中で関数 bar を定義すると、bar の中から foo の局所変数にアクセスすることが可能になります。Pascal をご存知の方にはお馴染みの機能だと思います。
Clojure で局所関数を定義するには特殊形式 letfn を使います。
(letfn [(func1 (args ...) body1) (func2 (args ...) body2) ... ] body)
letfn は最初に局所関数を定義して、letfn の本体 body を評価します。局所関数は複数定義することができますが、呼び出すことができるのは body の中だけであることに注意してください。なお、body1 と body2 から関数名 func1 と func2 を参照することはできるので、再帰定義や相互再帰を使っても大丈夫です。
簡単な実行例を示しましょう。
user=> (defn foo [a] (letfn [(bar [] (println a))] (bar))) #'user/foo user=> (foo 10) 10 nil user=> (foo 100) 100 nil
プログラムに意味はありませんが、関数 foo の引数 a に注目してください。a の有効範囲は foo の中ですが、この中で letfn により関数 bar が定義されていますね。この場合、関数 bar は局所変数 a にアクセスできるのです。したがって、(foo 100) を評価すると局所変数 a の値 100 が表示されます。
拙作のページ「繰り返しと末尾再帰」では、階乗を求める関数 fact やフィボナッチ数を求める関数 fibo を末尾再帰に変換するとき loop / recur を使いました。これを使わずに letfn で定義することもできます。プログラムと実行例を示します。
リスト : letfn の使用例 ;; 階乗 (defn fact [n] (letfn [(facti [n a] (if (zero? n) a (facti (dec n) (* a n))))] (facti n 1))) ;; フィボナッチ数 (defn fibo [n] (letfn [(fiboi [n a b] (if (zero? n) a (fiboi (dec n) b (+ a b))))] (fiboi n 0 1)))
user=> (dotimes [x 10] (println (fact x))) 1 1 2 6 24 120 720 5040 40320 362880 nil user=> (fact 20) 2432902008176640000 user=> (dotimes [x 10] (println (fibo x))) 0 1 1 2 3 5 8 13 21 34 nil user=> (fibo 40) 102334155
それでは、クロージャの話に入ります。クロージャは関数だけではなく、それを評価する「環境」も取り出して保持しています。次の例を見てください。
(defn foo [a] ─────── (let [b 20] ───── ↑ ↑ │a ・・1・・ │b │ │ │ (foo 10) を評価した場合 (map (fn [c] ── │ │ => #<closure ...)> ↑c │ │ S式 (fn [c] ..... ) ・・2・・ ↓ ↓ │ 環境 ((b 20) (a 10)) ) b)) ───── │ │ ・・3・・ ↓ ) ─────── 図 : 無名関数の評価とクロージャ
この例では評価する関数は無名関数ですね。そして、それを評価する環境は、関数 foo の仮引数 a と let で定義された変数 b です。これが環境 (上図では連想リストで表現) としてクロージャに格納されます。クロージャはこの環境で関数を評価するのです。上図では評価する関数本体を S 式 [*3] で表しています。
ここがクロージャを理解するポイントです。無名関数が評価される場合、まず仮引数の値がセットされますね。たとえば、c に 30 がセットされたとしましょう。すると、クロージャに保持されている環境に (c 30) が追加されます。
環境 : ((c 30) (b 20) (a 10))
無名関数の本体はこの環境で評価されるため、無名関数の仮引数 c 以外の局所変数、つまり foo の仮引数や let で定義された局所変数にアクセスできるのです。関数の内部で無名関数が評価されてクロージャが生成されるので、その時点で有効な局所変数にアクセスすることができるのです。
今度は、クロージャ独特の使い方を見ていきましょう。クロージャは無名関数の評価値でしたね。つまり Clojure で扱うことができるデータのひとつです。ということは、クロージャを返す関数を作成することができるはずです。次の例を見てください。
リスト : この関数は何をする? (defn foo [x] (fn [y] (* x y)))
関数 foo はクロージャを返します。このクロージャを変数にセットしておいて、あとから呼び出すことができます。次の例を見てください。
user=> (defn foo [x] (fn [y] (* x y))) #'user/foo user=> (def t2 (foo 2)) #'user/t2 user=> (t2 4) 8 user=> (t2 5) 10 user=> (def t10 (foo 10)) #'user/t10 user=> (t10 1) 10 user=> (t10 2) 20
この例からもわかるように、変数 t2 に格納されたクロージャを評価すると、引数を 2 倍した値を返します。変数 t10 のクロージャは、引数を 10 倍した値を返します。つまり関数 foo は、引数を定数倍する関数を生み出していたのです。
(foo 2) => #<closure : .....> S式 (fn [y] (* x y)) 環境 ((x 2)) (foo 10) => #<closure : .....> S式 (fn [y] (* x y)) 環境 ((x 10)) 図 : 関数 foo の評価
(foo 2) を評価すると、そのクロージャには評価する関数本体と環境がセットされます。このとき、定義されている局所変数は foo の引数 x だけです。したがって、環境は ((x 2)) となります。このクロージャを評価するときは、この環境で行われます。したがって、(t2 4) を評価すると y = 4, x = 2 なり、関数の評価結果は 8 となるのです。
(foo 10) の場合も同様ですね。これは x の値が 10 になるだけです。環境の保存はクロージャを生成するときに行われます。仮引数 x は同じだからといって、環境も共通で使われるわけではありません。(foo 2) と (foo 10) で生成したクロージャの環境は異なることに注意してください。
それから、局所的な関数を定義して、その関数を返してもクロージャを生成することができます。次のプログラムを見てください。
リスト : カリー化関数 (defn my-map [func] (letfn [(map1 [ls] (if-not (seq ls) '() (cons (func (first ls)) (map1 (rest ls)))))] map1))
上のプログラムは関数 mapcar を 1 引数の関数に直したものです。my-map は関数 func を受け取り、その func を呼び出してリストを操作する関数を返します。これでもマッピングの動作ができるのです。簡単な例を示しましょう。
user=> (def foo (my-map (fn [x] (* x x)))) #'user/foo user=> (foo '(1 2 3 4 5)) (1 4 9 16 25) user=> ((my-map #(* % %)) '(1 2 3 4 5)) (1 4 9 16 25)
最初の例は my-map で生成した関数を変数 foo にセットし、それから foo を関数呼び出しします。次の例は、my-map の返り値を直接関数呼び出ししています。カッコが多くなりますが、 2 引数の my-map と同じように呼び出すことができます。これでもリストの要素を 2 乗することができます。
2 番目の例は、最初の引数を受け取って新しい関数を生成して返し、その関数に次の引数を適用して値を求めるという動作になります。このように、関数の引数が一つでも、「関数を返す関数」を使うことで、複数の引数を処理することができるのです。これを「カリー化関数 (curried function)」といいます。
関数型言語には、カリー化関数をサポートしているプログラミング言語、たとえば Haskell や ML (SML/NJ, Ocaml) などがあります。これらのプログラミング言語では、高階関数はカリー化関数として定義されています。また、関数を合成して新しい関数を作ることも簡単にできます。
Clojure の場合、カリー化関数はサポートされていませんが、「部分適用」という方法が用意されています。関数 partial を使います。簡単な例を示しましょう。
user=> (def foo (partial map #(* % %))) #'user/foo user=> (foo '(1 2 3 4 5)) (1 4 9 16 25)
部分適用に興味のある方は Clojure のマニュアルをお読みください。
今回はここまでです。簡単に復習しておきましょう。
ところで、Lisp / Scheme のように、変数の値を書き換えることができる処理系では、クロージャに保存された変数の値を変更することが可能です。興味のある方は Common Lisp 入門:「レキシカルスコープとクロージャ」をお読みください。
次に示す関数を定義してください。
関数 flatmap は次の動作と同じになります。
user=> (map #(list % %) '(1 2 3 4 5)) ((1 1) (2 2) (3 3) (4 4) (5 5)) user=> (apply concat (map #(list % %) '(1 2 3 4 5))) (1 1 2 2 3 3 4 4 5 5)
map で生成したリストの要素を concat で連結するだけです。
リスト : マッピングしたあと平坦化する (defn flatmap [func xs] (apply concat (map func xs)))
user=> (flatmap #(list % %) '(1 2 3 4 5)) (1 1 2 2 3 3 4 4 5 5) user=> (flatmap #(repeat % %) '(1 2 3 4 5)) (1 2 2 3 3 3 4 4 4 4 5 5 5 5 5)
repeat n x は x を n 個格納したシーケンスを生成する関数です。簡単な使用例を示しましょう。
user=> (repeat 10 'a) (a a a a a a a a a a)
repeat x は無限シーケンスを生成するので注意してください。
リスト : pred が真の要素を取り出す (defn my-take-while [pred xs] (if (and (seq xs) (pred (first xs))) (cons (first xs) (my-take-while pred (rest xs))) '()))
my-take-while は xs が空リストまたは述語 pred が偽を返すとき空リストを返します。そうでなければ、my-take-while を再帰呼び出しして、その返り値にリストの要素を追加します。
user=> (my-take-while #(< % 5) '(1 2 3 4 5 6 7 8)) (1 2 3 4) user=> (my-take-while pos? '(1 2 3 4 5 6 7 8)) (1 2 3 4 5 6 7 8) user=> (my-take-while neg? '(1 2 3 4 5 6 7 8)) ()
リスト : pred が真の要素を取り除く (defn my-drop-while [pred xs] (if (and (seq xs) (pred (first xs))) (recur pred (rest xs)) xs))
my-drop-while は簡単です。リスト xs が空リストまたは述語 pred が偽を返すとき、リスト xs を返します。そうでなければ、my-drop-while を再帰呼び出しするだけです。
user=> (my-drop-while #(< % 5) '(1 2 3 4 5 6 7 8)) (5 6 7 8) user=> (my-drop-while pos? '(1 2 3 4 5 6 7 8)) () user=> (my-drop-while neg? '(1 2 3 4 5 6 7 8)) (1 2 3 4 5 6 7 8)
リスト : 連続した同じ記号を部分リストにまとめる (defn pack [xs] (if-not (seq xs) '() (cons (my-take-while (fn [x] (= (first xs) x)) xs) (pack (my-drop-while (fn [x] (= (first xs) x)) xs)))))
user=> (pack '(a a a b c c c c d d e e e e e)) ((a a a) (b) (c c c c) (d d) (e e e e e))
関数 pack は my-take-while と my-drop-while を使うと簡単です。(car xs) と等しい要素を my-take-while で取り出します。pack を再帰呼び出しするとき、(first xs) と等しい要素を drop-while で取り除きます。
データ内で同じ値が並んでいる場合はその値と個数で符号化する方法のことを、「ランレングス圧縮」または「ランレングス符号化」といいます。
リスト : ランレングス符号化 (defn encode [xs] (map #(list (first %) (count %)) (pack xs))) (define (encode xs) (map (lambda (ys) (cons (car ys) (length ys))) (pack xs)))
関数 encode は pack を使うと簡単です。pack の返り値を map で (code n) に変換するだけです。
user=> (encode '(a a a b c c c c d d e e e e e)) ((a 3) (b 1) (c 4) (d 2) (e 5))
リスト : ランレングス復号 (defn decode [xs] (flatmap #(repeat (second %) (first %)) xs)) (define (decode xs) (flatmap (lambda (ys) (make-list (cdr ys) (car ys))) xs))
ランレングスの復号は関数 flatmap と make-list を使うと簡単です。make-list で (code n) をリストに変換し、flatmap でそれを平坦化するだけです。
user=> (decode '((A 3) (B 1) (C 4) (D 2) (E 5))) (A A A B C C C C D D E E E E E)