lambdaことはじめ

私の職場では一部のプロジェクトでSchemeを使っている。私はハッピーなのだが、 C言語に慣れている他の同僚は、戸惑うことも多いようだ。 今日も同僚の一人(仮にChrisとしとこう)がオフィスにやってきて尋ねた。

「あるリストの各要素に対して関数 foo を適用したいんだ。 for-each が使えそうなんだけど、問題があって、 foo は2つ引数を取るんだ。一つ目にはリストの各要素を、 二つ目にはこのコードのローカルで定義されている変数を渡したい。どうすれば良いんだろう?」

わかりやすくするために、CとSchemeでコードを書いてみる。Chrisがやりたいのは、 Cで書けばこんな感じのコードなのだろう。(リストを表す List 型は どっかで適当に定義されているとする)。

/* 例1 */ void bar(List list, Context ctx) { List listp; for (listp = list; listp; listp = listp->next) { foo(listp->element, ctx); } }

Schemeには、与えられたリストの各要素に対して関数を呼び出す for-each という組み込み関数がある。これが使えそうだ。

(for-each 関数 リスト ...)

例えば、次のコードはリストの各要素 "a" , "b" , "c" に対して関数 display を適用して、出力に ` abc ' と表示する。

;; 例2 (for-each display '("a" "b" "c"))

複数の引数を取る関数を渡すこともできる。その場合、それぞれの引数に対応する リストを for-each に渡す。例えば：

;; 例3 (for-each cons '(1 2 3) '(4 5 6))

この例では、 (cons 1 4) , (cons 2 5) , (cons 3 6) が順に実行される。( for-each は結果を捨ててしまうので、このコード例は 実質的に何もしない。同じ構文で使える map では、各要素への実行結果が リストになって返される。)

さて、Chrisの問題に戻ろう。 foo が一つの引数しか取らないのであれば、 例2のようにしてやれば良いわけだが、 foo は 二つの引数を要求している。では例3のパターンを使ってみようか。

;; 例4 (define (bar list context) ;; DOESN'T WORK (for-each foo list context))

残念ながらこれはうまく動かない。 for-each は第2引数以降にリストを要求するのに、 Chrisは foo に context そのままを渡したいのだ。 ここでSchemerがやるのは、 lambda を使ってローカルな関数を作ってやることだ。

;; 例5 (define (bar list context) (for-each (lambda (listp) (foo listp context)) list))

lambda で始まる部分は、一つの引数を受け取る無名の関数を生成する。 それによって、例2のパターンが使えるようになる。 lambda の内部からはそれを囲むローカル変数が参照できるので、その中で context を foo に渡してやれば、問題解決というわけだ。

でもさ、いちいち関数を作るの、重くない?

しかし、Chrisは納得できないようだ。

「うーん、lambdaを使うやり方は思い付いたんだよ。でもさ、 Cで書いたバージョン(例1)では foo をループから直接呼べたのに、 Schemeバージョン(例5)では一回関数呼び出しが増えてるでしょ。 それに、この lambda は「関数を生成する」と言うけど、 bar が呼ばれる度に毎回毎回関数を生成してるのかな。 なんだかよく分からないけど、重そうじゃない? もっとストレートにやる方法はないのかなあ。」

いや、例5のバージョンはこれ以上無い程ストレートなのだが… なるほど、 C言語においては、関数とその呼び出しコードは、明示的にインライン宣言でもしない限り レジスタ退避・スタックフレームの作成や破棄などの 定型処理を含めたコードとしてコンパイルされる。そのオーバヘッドは、 関数本体の処理が小さい程、そして呼ばれる回数が多い程、負担になる。 例5のように、何も処理をせずに別の関数を呼ぶためだけの関数なんて 無駄以外の何者でもない、と思ってしまっても無理はない。

実はSchemeでは、関数は抽象的に処理単位を記述するために使われ、 実際に実行されるコード上でのスタックフレームの操作とは必ずしも 一対一に対応しないのだ。 特に、 lambda で作られた関数や ローカルに定義された関数は処理系がその使われ方を解析できることが多いため、 様々な方法で最適化が可能だ。

例5のような場合、少なくとも2通りの最適化が考えられる。 lambda フォーム内の foo の呼び出しは、 lambda で定義された関数の一番最後にあるから、これは末尾呼び出しだ (*1)。 この場合、処理系が賢ければ、fooへの制御の受渡しは関数呼び出しではなく 単なるジャンプ処理に変換されるので、 関数呼び出しのオーバヘッドはfooを直接呼ぶ場合とほどんど変わらない。 これが一つの方法。

また、 for-each は組み込み関数だから、 ユーザによって置き換えられていない限り、 これをインライン展開することも可能だ。 すると、 lambda で作られる関数はここでしか使われていないから、 それ自身もインライン展開が可能で、 結局、実行されるコードはC言語版の例1とほとんど同じになり、 関数呼び出しのオーバヘッドは foo を呼び出すだけとなる。 このステップを疑似コードを使って書いてみると、下の例6のようになる。

;; 例6 : 例5の関数の最適化 ;; 元の関数 (define (bar list context) (for-each (lambda (listp) (foo listp context)) list)) ;; for-each をインライン展開 (define (bar list context) for each element listp in list : (lambda (listp) (foo listp context)) ) ;; lambdaをインライン展開 (define (bar list context) for each element listp in list : (foo listp context) )

処理ブロックとしての lambda

Cにも例えば sort など関数ポインタを受け取る標準関数というのはあるが、 Schemeではずっと多くの標準関数が関数の形の引数を取る。 上に挙げた for-each もそうだし、例えばファイルの入出力に便利な call-with-output-file なんかもそのパターンだ。

(call-with-output-file filename func)

この関数は、filenameで与えられるファイルをオープンして 得られるポートを関数funcに渡す。 funcから戻るか、func内でエラーが生じて制御が外に出る際に、 ファイルは閉じられる(*2)。 ファイルのオープン／クローズを気にせずに簡単にファイル入出力が行えるので なかなか便利だ。よくfuncのところにlambda式を入れた形で使われる。

;; 例7 (call-with-output-file "tempout.txt" (lambda (port) ;; port は "tempout.txt" への出力ポート (display "Hello, world" port) ;; "Hello, world" をファイルへ出力 (newline port) ;; 改行をファイルへ出力 ))

このように使われるlambdaは関数を生成しているというより、 C言語で {} で囲まれたブロックを使うという感覚だ。 そして実際、処理系が call-with-output-file の動作を知っていれば、 for-each の時と同じようにインライン展開が可能で、 例7は下の例8のCコードと同じように実行され得る。

/* 例8 */ { FILE port = fopen("tempout.txt", "w"); /* エラー処理は省略 */ fprintf(port, "Hello, world"); fprintf(port, "

"); fclose(port); }

一般に、 lambda で生成された関数は、 処理系がその関数の使われる場所を全て追うことができれば、 すなわち、どこかに保存されたり未知の関数の引数として渡されたりしなければ、 あたかもローカルのコードブロックのように扱うことが可能だ。

Schemeプログラマは、よく関数の中でローカルに関数を定義して使う。 ローカルな関数も、要は lambda で作られる無名の関数に あとから参照しやすいように名前をつけているだけだから、 処理系がその関数の使われ方を解析できれば、 関数本体をコードブロックのように展開することができる。 次に示すのは例7と等価なコードだ。等価というのは、 単に同じ動作をするというだけでなく全く同じ効率で動作し得るということだ。

;; 例9 (letrec ((proc (lambda (port) ; 処理ブロックに名前をつけておく (display "Hello, world" port) (newline port)) )) (call-with-output-file "tempout.txt" proc)) ;; 上の式が関数定義のボディだったり、let文の中に置かれた場合は、次のような ;; "internal define" という構文も使える。ローカル関数を定義しているという ;; ことがはっきりわかるので、このスタイルを好む人もいる。実行効率は上のコードと ;; 同じなので、どちらを使うかは単なるスタイルの問題。 (let () (define (proc port) ; ローカル関数procを定義 (display "Hello, world" port) (newline port)) (call-with-output-file "tempout.txt" proc))

私は、複雑な処理をこなさなければならない時に部分部分をどんどん ローカル関数にしてしまう流儀だ。 例えば、次のようなCの関数を考えてみる。

/* 例10 */ void big_function( Obj v, Obj w, Obj x, Obj y, Obj z) { Obj r, Obj s, Obj t, Obj u; /* 処理のローカルなコンテキスト */ int error = 0; /* エラーが起きたらセットされる */ /* 第1ステップ */ ... (r, s, t, v, w, x, y, z を使う長ーい処理、500行) ... if (error) return; /* 第2ステップ */ ... (またr, s, t, v, w, x, y, z を使う長ーい処理、500行) ... if (error) return; /* 第3ステップ */ ... (またまたr, s, t, v, w, x, y, z を使う長ーい処理、500行) ... if (error) return; }

あまり良いコーディングではないけれど、 こういうコードをいじらなければならないことは現場では往々にしてあるものだ。 この関数を各ステップ毎に分割しようとすると、 それぞれの関数に処理のコンテキストである変数 r 〜 z を引数経由で渡してやらねばならない。実行効率が気になる時や、 まだ書きかけの状態で引数の仕様などがはっきりしていない時には あまりそういうことをしたくないものだ。 Schemeでは、ローカル関数を定義して見通しを良くすることができる。 big-function は依然として大きな関数だが、 内部の define を使って処理を関数の形で分割したものが次のコードだ。

;; 例11 (define (big-function v w x y z) (let ((r #f) (s #f) (t #f) (u #f)) (define (step-1) ;; 第1ステップ。エラー時に#f, 成功時に#tを返す ... r, s, t, u, v, w, x, y, z を使う長ーい処理... ) (define (step-2) ;; 第2ステップ。エラー時に#f, 成功時に#tを返す ... r, s, t, u, v, w, x, y, z を使う長ーい処理... ) (define (step-3) ;; 第3ステップ。エラー時に#f, 成功時に#tを返す ... r, s, t, u, v, w, x, y, z を使う長ーい処理... ) ;; 関数本体。処理の流れが一目瞭然。 (and (step-1) (step-2) (step-3)) ))

ここでも、処理系がしっかりしていれば 各ローカル関数 step-1 , step-2 , step-3 はインライン展開され、関数呼び出しのオーバヘッドは発生しない。 まあ、関数の定義の開始部と本体とが離れてしまうので 依然として読みにくいことは読みにくいが…

ローカル変数の束縛と引数の束縛

例7と例8のコードを見比べてみると、ローカルに定義された lambda の 引数( port )が、オープンに展開された時にローカル変数に置き換えられて いるのに気付く。実際、 lambda の名前の由来でもある ラムダ算法においては、ローカルな変数 (ほんとは変数という名称は適切でないが…束縛かな) はラムダ式を使って導入される。次の二つの例を比べてみよう。

;; 例11 (let ((a 1) (b 2)) (+ a b) ) ;; このコードは次のコードと等価 ( (lambda (a b) (+ a b)) 1 2 )

先に述べたようなコードのオープン化を行えば、この両者は全く同等に実行される。 次の例も同じだが、 lambda がローカルに束縛されている。

;; 例12 (define (foo x y) (letrec ((bar (lambda (a b) (+ a b)))) (bar x y))) ;; このコードは次のコードと等価 (define (foo x y) (let ((a x) (b y)) (+ a b)))

この例でわかるように、ローカル変数の束縛と引数の束縛とは実質的に同じものだ。 Schemeプログラマが lambda を多用する時は、 多かれ少なかれこのような最適化を処理系に期待しており、 暗黙のうちに両者を読み替えている場合が少なくないと思う。

Cプログラマがよく混乱するSchemeの構文に、named-let構文がある。 ローカル変数を束縛する通常の let とよく似ているが、 let と変数束縛の記述との間にシンボルが入っているものだ。 次のように定義される*3:。

;; named let構文 (let name ((var expr) ...) body ...) ;; これは次のコードと等価 (letrec ((name (lambda (var ...) body ...) )) (name expr ...))

私が最初にこの説明を見たとき、何がなんだかさっぱりわからなかったのを覚えている。 let はローカル変数を定義する構文なのに、何故 lambda が出て来る必要があるのか。 例12の関係が理解できるまで、どうしても納得がいかなかった。 (named let構文の定義は例12の書き換えを逆にして、 let の後に名前を入れただけだ)。

何でわざわざ特別な構文まで用意するのかというと、 この構文が実に便利だからだ。 私は特に、ループを書く時に多用する。 次の例はtrivialだが、1から100までを加算するコードだ。 (Schemeにおいて "loop" という識別子には特別の意味はなく、 単なるローカル変数として使われている)。

;; 例13 (let loop ((num 1) (sum 0)) (if (> num 100) sum (loop (+ num 1) (+ sum num))) ) ;; これは次のコードと等価。つまり再帰で計算をしている。 ;; 末尾再帰なので実行効率はループと同じ。 (let () (define (loop num sum) (if (> num 100) sum (loop (+ num 1) (+ sum num)))) (loop 1 0))

遅延処理構文としての lambda

lambda はまた、 特定のコードブロックの処理の実行を遅らせる構文と考えることも出来る。 通常の関数呼び出しでは、関数が呼ばれる前にその引数が全て評価されて、 関数本体にはその結果だけが渡されるわけだが、 上に例をあげた for-each や call-with-output-file では、関数が欲しいのは「評価すべき内容」であって「評価の結果」ではない。

どんなプログラミング言語にも、「評価結果ではなく評価すべき内容」 を必要とする場合がある。 例えば条件分岐では、条件の真偽によってその後に実行すべき内容が変わるから、 if 文に渡される引数を最初に全部評価してしまうわけにはいかない。 Schemeでも if は関数ではなく構文(syntax)として定義されている。

しかし、関数呼び出しを基本とするSchemeにおいて構文とは特殊な存在であり、 あまり無節操に構文を増やしたくはない。でないと、 コードを読む度にこの引数は評価されるのかどうかと悩むことになってしまう。 そのため、かどうかは実は知らないのだが、 他のプログラミング言語では構文にしてしまっているような機能が、 Schemeでは組み込み関数として定義されているものがいくつもある。 もちろんその場合、呼び出す側はコードブロックを lambda で括って渡すわけだ。上の例にあげた call-with-output-file の例も、 lambda フォームはコードブロックを評価せずに渡す役割をしている と見ることができる。

例外事態が起きてルーチンを抜ける前に、ファイルを閉じるとか一時ファイルを 消すとか、何らかの後始末を行っておかねばならない場合がしばしばある。 C++ だったら、try-catch構文を用いることで実現できる。

/* 例14 */ void foo() throws Exception { FILE *fp = fopen("input", "r"); try { /* fpを使った処理。例外時にはExceptionがthrowされる。 */ } catch (Exception e) { fclose(fp); /* closeを忘れずに */ throw(e); } fclose(fp); return OK; }

Schemeには、このような場合のために dynamic-wind という組み込み関数が用意されている *4:。 これは3つの引数無しの関数を受け取って、それを順繰りに実行する。 とりあえず、3つの関数はそれぞれ前処理、処理本体、後処理と考えて良いだろう。 そして、もし処理本体でエラーが発生して処理が中断された場合でも、 後処理ブロックは必ず実行される。 (厳密な説明をするには「継続」の説明をしなくてはならないので、 この文書の範囲を超える。今のところはこのように理解しておいて欲しい。) 上のC++コードと似たような処理は、こんなふうに書ける。

;; 例15 (define (foo) (let ((fp #f)) (dynamic-wind (lambda () ;; 1番目のブロック。前処理。 (set! fp (open-input-file "input))) (lambda () ;; 2番目のブロック。処理本体 ;; inputを使った処理 ) (lambda () ;; 3番目のブロック。後処理 (close-input-port fp)) ) ))

C++の例と違って、後処理ブロックは本体が正常終了しても必ず実行される。 (逆に、特定の例外だけを捕まえるための構文というのはScheme規格にはない。 継続を使えば出来るが、その説明はまたの機会に。)

もちろん、他で3つの関数を定義しておいて dynamic-wind に渡してやっても構わない。 for-each の時と同様に、 アグレッシブな処理系は dynamic-wind に渡された関数がローカルなものであればインライン展開するだろう。

dynamic-wind は実質的には言語組み込みの機能だから、 構文にしてしまっても良かったのでは、という気もするかもしれない。 実際、CommonLispでの対応する機能 unwind-protect は構文として定義されている。 dynamic-wind だって、次のように3つのフォームを受け取るように定義しておけば、 lambdaが省けてすっきりする。

;; 例16 構文版のdynamic wind (dynamic-wind-syntax (begin ...) ;; 前処理フォーム (begin ...) ;; 本体フォーム (begin ...)) ;; 後処理フォーム ;; これを使って例15を書くとこうなる。lambdaは不要。 (define (foo) (let ((fp #f)) (dynamic-wind-syntax (set! fp (open-input-file "input))) (begin ;; inputを使った処理 ) (close-input-port fp)) ))

どちらが良いかは、まあ、究極、好みの問題になるのかもしれない。 きっと、慣れたSchemeプログラマはlambdaをほとんど無意識に使用するので、 例15のようなlambdaの洪水を見ても別に気にならないのだ。 だから、コーディング上すっきりする dynamic-wind-syntax よりも、 関数という一般的な概念で全てが記述できる現在の dynamic-wind の方を好むのだろう。 (ちなみに、慣れたLispプログラマはカッコの存在を意識しないなんて話もある)。

「関数を生成する」って具体的には何をしているの?

ここまでは、lambdaで生成された関数やローカルで定義された関数が 最適化によってインライン展開される、すなわちC言語での関数みたいに重くはならないんだ、 という議論だった。しかし、最適化出来ない場合というのもたくさんある。 具体的にはこんなケースだ。

生成された関数が、他の未知の関数に引数として渡される場合

生成された関数がグローバル変数に束縛される場合

生成された関数が戻り値として返されて、そのあとどのように使われるかわからない場合

こういう場合は、ちゃんとスタックフレームの処理などを伴うC言語的な「関数」 が生成される必要がある (ここでは実行モデルに、C言語との親和性が高い スタックアーキテクチャを使っていると考える。 継続渡しスタイル等にすれば事情はちょっと変わって来るが、それは別の話。)

これも、C言語に親しんで来たプログラマにはぴんと来ない話だと思う。 Cでは全ての関数はコンパイル時に決定されていて、 その関数アドレスを受け渡したりすることはできるが、 実行時に関数を「作る」とは? まさかlambdaフォームが評価される度にコンパイラが走ったりしているのだろうか。

次は、lambdaフォームによる実行時の関数の作成を示すのに良く使われる例だ。

;; 例17 ;; 「与えられた数にXを加える」という関数を生成する関数 (define (addx x) (lambda (y) (+ x y)) ;; 「与えられた数に4を加える」という関数add4を作成 (define add4 (addx 4)) ;; add4の適用 (add4 5) => 9 (add4 8) => 12 ;; 「与えられた数に-1を加える」関数sub1を作成 (define sub1 (addx -1)) (sub1 5) => 4 (sub1 8) => 7 ;; addxで作られる関数を直接呼び出すこともできる ((addx 13) 17) => 30

関数addxは引数xを取り、新たな関数を作成して返す。 作られる関数はひとつの引数yを取り、 それに作成時に与えられるxの値を足したものを返す。 xを変えてaddxを何度も呼べば、その都度異なる動作をする関数が返って来る。 ここで知りたいのは、addxの中で具体的に何が起きているか、だ。

addxがいくつ関数を作ろうが、 それぞれの関数が内部で行っている「処理のステップ」は全く同じである、 ということに注目しよう。 add4 も sub1 も、 内部でやっていることは引数 y を受け取って x と加算する という手順だ。 違う動作をするように見えるのは、そこで参照されている x が add4 の場合は4、 sub1 の場合は-1である、 という違いがあるからだ。

例17のコードは、頑張ればC++で書き直すことができる。 (Cでもできるけどもう少し複雑になる。)

/* 例18 */ class Closure { public: int x; int apply(int); }; /* これがlambdaの中身 */ int Closure::apply(int y) { return (this->x + y); } /* addxの本体 */ Closure *addx(int x) { Closure *c = new Closure; c->x = x; return c; } /* add4やsub1を作ってみる */ Closure *add4 = addx(4); add4->apply(5); /* => 9 */ add4->apply(8); /* => 12 */ Closure *sub1 = addx(-1); sub1->apply(5); /* => 4 */ sub1->apply(8); /* => 7 */

addxの中のlambdaで処理されるべきステップ、すなわちxとyを足す、 というのはコンパイル時に確定しているから、 それは一つの関数としてコンパイルしておける ( Closure::apply )。しかし、 addxが呼ばれた時点でのxの値を記憶しておくために、 クラス Closure を定義している。 addxは関数ポインタではなく Closure のインスタンスを返す。

実は、この Closure こそがSchemeにおける「関数」の正体なのだ。 Schemeに限らず、ローカルな関数の定義を許す言語では、 関数は「処理手順」と「環境」のペアで表現される。 上の例では "xとyを足す" というのが処理手順で、xの値がその環境だ。 処理手順はコンパイル時に確定している (Cの関数は「処理手順」のみによって構成されていると考えられる)。 実行時に関数が「作られる」というのは、 既にコンパイルされている処理手順へのポインタと、 関数が作られる時点での環境とを合わせたオブジェクトが作られる、ということだ。

Schemeの場合、環境とは関数が定義される場所から見えている変数束縛の全てだ。 関数を作った時点で、それらの変数束縛を関数の中に「閉じ込む」ことから、 このようにして作られる関数をclosureと呼ぶ。

Closureは実用的なプログラム中でも非常に便利なものだ。 例えば、GUI関係のコードをCで書いたことのある人なら、 特定のイベントが起きた時の動作をコールバックで記述したことがあるだろう。 Cのインタフェースでは、 コールバックを登録する時に付加的なデータへのポインタを一緒に登録するようになっていることが 多い。次に示すのは仮想的なGUIライブラリを使って、 ボタンひとつのトップレベルウィンドウを作り、 ボタンがクリックされたらトップレベルウィンドウを消す、というコードだ。

/* 例19 仮想的なGUIコード */ void quit_button_callback(void *client_data) { Widget main_window = (Widget)client_data; destroy_window(main_window); } Widget make_main_window() { Widget main_window = make_toplevel_window(); Widget button = make_button(main_window); /* add_callback(Widget w, void (*callback)(void *), void *data) */ add_callback(button, quit_button_callback, (void*)main_window); }

GUIツールキットによって詳細は違うが (それに普通はもっとめんどくさいが) コードのだいたいの骨格は似たようになるだろう。ここでは、 add_callback の3番目の引数が付加的な情報として、コールバック関数に渡されている。

Schemeの場合、渡したい付加的な情報はクロージャの中に閉じ込むことが出来るから、 余分な引数は必要ない。次の例では、 コールバック関数をlambdaで直接作成している。

;; 例20 (define (make-main-window) (let* ((main-window (make-toplevel-window)) (button (make-button main-window))) (add-callback button (lambda () (destroy-window main-window))) ))

まとめ

以上、長々と説明してきたが、簡単にまとめると：

Schemeにおける「関数」はより抽象的な処理ブロックを記述するのに使われ、 C言語における「関数」とは一対一に対応しない。

ローカルに定義されてすぐ適用される関数は、ほとんどの場合、インライン化可能。 だからSchemeプログラマは気軽にlambdaを使いまくる。

実際に関数が生成されるという場合でも、既に固定された「処理手順」に その時の「環境」を対にして返しているだけ。

ということになる。

C言語を使い込んだ人であればある程、 C言語の関数が機械語レベルでどのように実行されるか知っているがゆえに、 Schemeの関数の概念に戸惑うことがあると思う。 この文章が理解の助けになれば幸いだ。

脚注