2017年07月21日

lambda式からlet式へ - いわゆるローカル変数

これら一連の記事は、numb-lambda インタープリタから始まる連載企画になっています。

今回はタイトルの通りlet式について説明しようと思います。let式を使うと、ふつうの手続き型言語でいうところのローカル変数を利用できるようになります。

余談になりますが、この連載では手続き型言語に慣れてはいるけれどLISP(Scheme)は実用性がないし理解しにくいのではないかという疑問を持って入って来られた方にもLISPを理解できるよう配慮して執筆しています。ですから、LISPのLISPたる所以であるリストというデータ構造と、それを式として表現するという題目には、まだ、触れていません。まずは、LISPが(括弧の多さを除けば)ふつうのプログラミング言語だという点から説明しています。リストについては数回後に説明しますから安心してください。

復習しましょう。LISPの式は次のような構造をしているのでした。

(<式1> <式2> <式3>...)

この括弧で囲まれた一連の式はリストを形成しています。この中で<式1>はオペレータになっています。オペレータには関数スペシャルフォームがありますが、今回は関数にフォーカスします。
この式を表すリストが関数呼び出しを表すとき、<式1>は、lambda式です。続く<式2>、<式3>... は関数の引数となり、<式1>が適用される前に評価されるのでした。コードを見てみましょう。


((lambda (r) (* 3.141592654 (* r r))) (* 5 2.54))
506.70747916365997
>

この式が関数呼び出しに見えないときは、lambda式を何かシンボルにバインドして、式を書き換えてみると解り易いかも知れません。以下のようにします。


> (define area-of-circle
    (lambda (r) (* 3.141592654 (* r r))))
> (area-of-circle (* 5 2.54))
506.70747916365997
>

lambda式はデータ(今風に言えばオブジェクト)ですのでシンボルにバインド(変数に代入)できます。

さて、話を元の式に戻しましょう。((lambda (r) (* 3.141592654 (* r r)))lambda式(関数)で上の形式で言えば<式1>になっています。このlambda式は仮引数 r をひとつ取ります。さて、この関数呼び出しにおいて仮引数 r対応する実引数は、上の形式では<式2>の部分を指します。つまり、(* 5 2.54)ですね。これは、lambda式が適用される前に評価されるのでした。つまり、仮引数 r にバインドされる実引数は、12.7です。

REPLで r を評価してみましょう。


> r
net.prizo.scmlib2.sc_exception: unbound variable: r
at net.prizo.scmlib2.symbol.eval(symbol.java:53)
at net.prizo.scmlib2.environ.eval(environ.java:73)
at net.prizo.scmlib2.scheme.run(scheme.java:79)
at java.lang.Thread.run(Thread.java:745)

; unbound variable: r
>

r は、関数の仮引数なので、関数の外側ではバインドされていません。関数の中でだけ評価すると値12.7が返って来ます。これは、手続き型言語と何ら変わりはありませんね。

複数の引数を持つ関数は次のように書きます。


> ((lambda (a b c) (- (* b b) (* 4. a c))) 1. 4. 4.)
0.0
>

この式も関数呼び出しに見えなかったら、lambda式をシンボルにバインドしてみましょう。


> (define discriminant
    (lambda (a b c) (- (* b b) (* 4. a c))))
> (discriminant 1. 4. 4.)
0.0
>

lambda式の仮引数は、シンボルlambdaの直後にある括弧内に書かれています。この場合は、a, b, c です。実引数はlambda式の、すぐ後の式の列です。この場合は、1., 4., 4. です。

ところで、この程度の式ならば、lambda式をわざわざ書かなくてもいいと思われるかも知れません。その要求を満たしてくれるのがlet式なのです。let式を使って、上の式を書き換えると次のようになります。


> (let ((a 1.)(b 4.)(c 4.))
    (- (* b b)(* 4. a c)))
0.0

とても簡潔になりました。let式の中では、それぞれ、a には 1.b には 4.c には 4.、のようにシンボルに値がバインドされます。これらのシンボルが有効なスコープはlet式の中だけです。let式を抜けた後、各シンボルを評価すると(let式の)外側でバインドされた値になります。abc は手続き型言語のローカル変数に相当します。

let式の一般形は次のような形をしています。


(let ((<シンボル> <>)...) <> <> <>...)

letの直後の括弧内は、ローカル変数の定義と初期化部のようになります。その括弧の後の式は、 beginlambda と同様に逐次実行され最後の式の値がlet式の値となります。

ところで、let式が構文糖(シンタックスシュガー)であることに気付かれましたか。便宜的にローカル変数と呼んでいるものは、じつのところlambda式の仮引数に相当します。つまり、let式は、その場でlambda式を生成し、即座に実引数を与えて、そのlambda式を呼び出しているのです。(理解できないようでしたら、今は深く考えなくて結構です)

ここで、不思議な関数をひとつ紹介します。


> (define gen-seqno
    (let ((n 0))
      (lambda ()
        (set! n (+ n 1))
        n)))
> (gen-seqno)
1
> (gen-seqno)
2
> (gen-seqno)
3


いかかですか。JavaScriptやPythonを知っている方には、お馴染みかも知れません。ヒントは、gen-seqno にバインドされているのは、let式内にあるlambda式であるということです。let式は最後の式を評価して値として返すのでした。繰り返しますが lambdaもオブジェクトなのです。理解できなければ今は読み飛ばして構いません。lambdaを使って書き換えてみましょう。


> (define gen-seqno-another
    ((lambda (n)
      (lambda ()
        (set! n (+ n 1))
        n)) 0))
> (gen-seqno-another)
1
> (gen-seqno-another)
2
> (gen-seqno-another)
3


C++などの言語は、関数から戻るとスタックフレームは破棄されてしまいます。しかし、Schemeや、Common Lispなどでは、lambda(関数)の仮引数を誰か(この場合は内側のlambda式)が使っている間は、それを破棄しないということです。

letについては以上です。次回は、ループについて説明する予定です。