Schemeプログラマにとってlambdaは空気のような存在です。Schemeプログラマはいわば呼吸するようにlambdaを書くのです。
lambda式を書いて「手続き型の値」を返す
lambda式は一般的に次の構文をとります。
(lambda 引数仕様 式本体)
たとえば、2乗を計算するlambda式を書いてみましょう。
(lambda (x) (* x x))
lambda式が返す値は手続き(procedure)型の値です。Gaucheの対話型インタプリタでこれを確かめてみましょう。
gosh> (lambda (x) (* x x))
#<closure #f>
closureは手続き型を表します。
手続きとは「手続き型の値」に名前を付けたもの
lambda式を実引数に作用させると2乗が計算されます。
gosh> ((lambda (x) (* x x)) 4)
16
これはsquare手続きを数値4に作用させたのとまったく同じです。
gosh> (define (square x) (* x x))
square
gosh> (square 4)
16
square手続きの定義式をもう一度見てみます。
(define (square x) (* x x))
これは次の式と等価です。
(define square (lambda (x) (* x x)))
実はこれが本来の式で、(define (square x) ...で始まる定義式は省略形なのです。
手続き型の値に名前を付けたものがいわゆる(名前付きの)手続きです。
ブロックとしてのlambda
Schemeプログラマはあらゆるコードでlambdaを多用します。その一つがブロックとしてのlambdaです。
Gaucheのユーザーリファレンスに書かれている、UNIX互換OSでのcatコマンドに似たスクリプトの例を見てみましょう。
#!/usr/bin/env gosh
(define (main args) ;entry point
(if (null? (cdr args))
(copy-port (current-input-port) (current-output-port))
(for-each (lambda (file)
(call-with-input-file file
(lambda (in)
(copy-port in (current-output-port)))))
(cdr args)))
0)
スクリプトが実行されると、まず最初にmain手続きが適用されます。argsはコマンドライン引数のリストです。
コマンドライン引数の2つめ以降を示す(cdr args)が空リストかどうかをnull?手続きで判定しています。
引数が空リストのときは標準入力ポートの内容を標準出力ポートにコピーします。copy-portは入出力ポート間で入出力されるデータをコピーします。Gaucheでは入出力ポートを使って入出力を行います。current-input-portは現在の入力ポートを示します。
引数が空リストでないとき、引数リストの各要素をファイル名だとしてそのファイルの内容を表示します。引数リストすべてを処理するためにfor-each手続きを使います。
一般にfor-each手続きは次の構文をとります。
(for-each 式 リスト)
ここでは引数リストの2番目以降のリスト(cdr args)を処理しています。
(for-each 式 (cdr args))
for-eachに与えられたリストの各要素について式が適用されます。このとき、式には引数としてリストの各要素が渡されます。
(for-each (lambda (file)
(call-with-input-file file
(lambda (in)
(copy-port in (current-output-port)))))
(cdr args)))
ここではfileを引数にとるlambda式を書いてfor-eachに与えられたリストの各要素を処理しています。fileが各要素の値、つまりファイル名に束縛されるので、call-with-input-file手続きの引数にfileを与えてひとつのファイルの入出力を行います。
一般的にcall-with-input-file手続きは次の構文をとります。
(call-with-input-file ファイル名 式)
ファイル名で指定したファイルがオープンされ、そのファイルの入出力ポートが引数として式に渡されます。
(lambda (in)
(copy-port in (current-output-port)))))
そのファイルの入出力ポートのデータを、copy-port手続きで現在の出力ポートにコピーしています。
catプログラムの例で見てきたとおり、lambdaは(他の多くのプログラミング言語で言う)ブロックとしての役割りも持っています。と言うより、lambda以外の方法でブロックを実現する方法はない、と言った方が正しいかも知れません。これが「Schemeプログラマは呼吸するようにlambdaを書く」という所以です。
letはlambdaのシンタックスシュガーである
letによる局所的な値への名前付けは、lambdaによる引数への名前付けとまったく等価です。
(define (weekday-name index)
(let ((day-names (list "日" "月" "火" "水" "木" "金" "土")))
(if (or (< index 0)
(> index 6))
#f ;; 0〜6の範囲外なら偽
(list-ref day-names index)))) ;; 曜日名を返す
weekday-name手続きはletでなくlambdaを使ってまったく等価に書き換えることができます。
(define (weekday-name index)
((lambda (day-names)
(if (or (< index 0)
(> index 6))
#f ;; 0〜6の範囲外なら偽
(list-ref day-names index))) ;; 曜日名を返す
(list "日" "月" "火" "水" "木" "金" "土")))
引数day-namesを伴うlambda式を、値(list "日" "月" "火" "水" "木" "金" "土")に適用することによってlet式と同じ効果を得ています。いえ、let式の方がlambdaのシンタックスシュガーなのです。
(シンタックスシュガーとは分かりやすい別記法のことです)
無限エクステントと値のカプセル化
Schemeの計算過程で生成された値は常に存在し続けることが処理系によって保障されます。この性質を無限エクステントと呼びます。ただし実装においては、以後どこからも絶対に参照されないことが証明されている値に限って廃棄することが可能になっています。
値が無限に存在すると何がうれしいのでしょうか?
- 値の廃棄について心配する必要がなくなる
- 値が廃棄されているかどうかを気にせずに、いつでも値を使うことができる
Schemeプログラマは記憶領域が足りなくなることを心配せずに、いくらでも値を使うことができます。値を廃棄する必要がないので、いつでもどの値でも使うことができます。
これはlambdaの内部に保存されている値も同様です。それを確かめるために、「すべてオブジェクトである」で例にあげた<2d-point>クラスと同様の計算をlambdaで実現してみましょう。
計算を実現するには構築子(constructor)と選択子(selector)を書く必要があります。
<2d-point>クラス型に相当するオブジェクトの構築子は次の通りです。
(define (make-2d-point x y)
(lambda (operation)
(cond ((equal? operation "x") x)
((equal? operation "y") y)
(else #f))))
この構築子が返す値は関数型の値です。
返ってきた関数に引数を適用すると内部のx座標やy座標の値を返します。
equal?手続きは値が等価かどうかを判定します。ここでは引数の値が文字列"x"に等しいときx座標の値を返し、文字列"y"に等しいときy座標の値を返しています。
2次元座標(10, 5)をmake-2d-pointを使って定義してみましょう。
gosh> (define p0 (make-2d-poit 10 5))
p0
返ってきた値p0は関数です。引数"x"、"y"に関数p0を適用するとx座標の値とy座標の値が得られます。
gosh> (p0 "x")
10
gosh> (p0 "y")
5
これを利用してx座標とy座標の値を得る選択子get-xとget-yが書けます。
gosh> (define (get-x 2d-point)
(2d-point "x"))
get-x
gosh> (define (get-y 2d-point)
(2d-point "y"))
get-y
gosh> (get-x p0)
10
gosh> (get-y p0)
5
無限エクステントという性質によって、lambdaは値をカプセル化することができます。これはオブジェクト指向で言う「オブジェクト」の性質とまったく同じです。
lambdaは〜である
これまで出てきたlambdaの性質をまとめてみましょう。
- lambdaは関数である
- lambdaはブロックである
- lambdaは値をカプセル化する
lambdaの持つ性質は、「〜に使える」と言うよりも、これらすべての性質を併せ持つと言うべきものです。
Schemeのすべての計算はlambda式に還元できます。無意識にlambda式を書いているのだとしたら、呼吸するどころかSchemeプログラマの思考のすべてがlambdaだと言っても過言ではありません。