レキシカルとダイナミック:2種類の入れ子

変数のように、あるLisp Objectを名前で参照するときに、2つの入れ子構造を使い分けます。これらをレキシカル・スコープとダイナミック・スコープと呼びます。

スコープとは、ある名前(シンボル)とLisp Objectの結びつき(バインディング、binding)が見える(有効である)ソースコードの範囲です。

入れ子の内側にいて、ある名前の値を参照しようとすると、その名前の一番内側のバインディングが見えるようになっています。同じ名前の外側のバインディングは一時的に見えなく(shadowed)なります。

レキシカル・スコープ
レキシカルな、つまり、プログラムのソース・コードの字面上でのオペレータの入れ子構造によって範囲(スコープ)が表されます。

KISS>(defun f (a)                              ;; ----------+
       ;; a is an argument given when called   ;;           |
       (let ((result (list a)))                ;;           |
         (let ((a 0))                          ;;           |
           ;; a is 0                           ;; -------+  |
           (setq result (cons a result))       ;;        |  |
           (let ((a 10))                       ;;        |  |
             ;; a is 10                        ;; ----+  |  |
             (setq result (cons a result))     ;;     |  |  |
             (let ((a 100))                    ;;     |  |  |
               ;; a is 100                     ;; --+ |  |  |
               (setq result (cons a result)))  ;; --+ |  |  |
             ;; a is back to 10                ;;     |  |  |
             (setq result (cons a result)))    ;; ----+  |  |
           ;; a is back to 0                   ;;        |  |
           (setq result (cons a result)))      ;; -------+  |
         ;; a is back to the original argument ;;           |
         (setq result (cons a result))         ;;           |
         result))                              ;; ----------+
f

KISS>(f -1)
(-1 0 10 100 10 0 -1)

KISS>
ダイナミック・スコープ
ダイナミックな、つまり、実行時のオペレータ(関数やスペシャル・オペレータ)の動的な呼び出し関係の入れ子構造によってバインディングが見える範囲(スコープ)が決定します。

KISS>(defun foo (x)
       (dynamic-let ((y x))
         (bar 1)))
foo ;; 関数foo定義完了
                
KISS>(defun bar (x)
       (+ x (dynamic y)))
bar ;; 関数bar定義完了

                
KISS>(foo 2)
;;(foo 2)により関数fooが呼び出される
;;  (foo 2)
;;    |  関数fooの引数であるレキシカル変数xは2
;;    |  関数foo内の(dynamic-let ((y x))で、xはいま2なので、
;;    |  ダイナミック変数yの値は2に設定される
;;    |  (dynamic-let ((y 2))
;;    |
;;    |    関数foo内の (bar 1) により関数barが呼び出される
;;    |    (bar 1)
;;  |      |関数barの引数であるレキシカル変数xは1
;;    |      |
;;    |      |関数bar内の (dynamic y) は
;;    |      |実行中の関数foo内の(dynamic-let ((y 2))による
;;    |      |ダイナミック変数yを参照
;;    |      |つまり(dynamic y)は2となる
;;    |      |
;;    |    (+ x (dynamic y)) は、(+ 1 2)になる
;;    |    3を返す
;;    |
;;    3を返す

3

KISS>




Lispのリスト図解

Lispが扱うリストの構造を図解します。これは理解のためのイメージですので、必ずしも実装がこの図解のままの構造を使っているとは限りません。

リスト(正式にはproper list)のテキスト表現は要素をスペースで区切って並べてそれをカッコ()でくくります。

例えばリスト

(a 1 2.0)

は、最初の要素はシンボルaです。2番目の要素は整数1、3番目の要素は小数2.0です。

このリストの処理系内での構造を図にすると次のようになります。

 (a            1           2.0)

+---+---+    +---+---+    +---+---+    +---+
|CAR|CDR---->|CAR|CDR---->|CAR|CDR---> |nil|
+-|-+---+    +-|-+---+    +-|-----+    +---+
  |            |            |
  V            V            V
+---+        +---+        +---+
| a |        | 1 |        |2.0|
+---+        +---+        +---+

リストは、Consセルと呼ばれる部品から作られています。
Consセルは、CAR、CDRという名前のポインタを持ちます。
リストの要素は必ずCARが指し、CDRはリスト構造のために使います。

+---+---+   +---------+
|CAR|CDR--> | Object2 |
+-|-+---+   +---------+
  V
+---------+
| Object1 |
+---------+
(a 1) なら、

+---+---+    +---+---+    +---+
|CAR|CDR---->|CAR|CDR---->|nil|
+-|-+---+    +-|-+---+    +---+
  |            |            
  V            V
+---+        +---+        
| a |        | 1 |        
+---+        +---+       
(a) なら、

+---+---+    +---+
|CAR|CDR---->|nil|
+-|-+---+    +---+    
  |
  V          
+---+        
| a |        
+---+        
() なら、

+---+
|nil|
+---+

シンボルnilは、リスト構造の終わりを表す印として使われますが、要素が何もない空リストは、この印nilのみで表すのが自然です。そこで、シンボルnilはシンボルでありながら同時にリストでもあるという面白い性質を帯びることになります。

KISS>(symbolp 'nil) ;; nilはシンボルか?
t                   ;; yes

KISS>(listp 'nil)   ;; nilはリストか?
t                   ;; yes

KISS>(eq 'nil '())   ;; シンボルnil は 空リスト()と同一物か?
t                    ;; yes

KISS>(length '())    ;; ()の要素数は?
0                    ;; 空リストは要素を持たない

KISS>




Lispのオペレータは関数とマクロに大別できる

REPループについての説明で言ったように、Lispにコンピュータ的な処理をさせるには、要素をカッコで囲んだリストを入力してやります。

(OPERATOR arg1 arg2 ...)

そのリストの最初の要素には普通、オペレータ(を表すシンボル)がきます。

処理系が、このオペレータに渡す引数の扱いのちがいで、オペレータは関数とマクロに大別できます。

関数
すべての引数を一度、評価(eval)してから、関数本体に渡す。
KISS>(+ 10 *pi*)
;; シンボル+が表す関数本体には、
;; 引数として 10を評価した結果 10 と
;; 定数*pi*を評価した結果 0.3141592653589793e1 が渡される

0.13141592653589793e2 ;; 10 + 3.14 = 13.14

KISS>
KISS>(* (+ 10 *pi*) 3)
;; 最初に、掛け算をする関数*への引数がすべて評価される
;;
;; 一つ目の引数 (+ 10 *pi*) を評価すると上記のように値
;; 0.13141592653589793e2 が得られる。
;;
;; 二つ目の引数 3 は評価しても 3 のまま
;;
;; したがって、関数*の本体は、引数として
;; 0.13141592653589793e2 と 3 を受け取り、
;; 掛け算をして結果を返す

0.39424777960769379e2
;; 3.14 + 10 で 13.14 、13.14 * 3 = 39.42
KISS>

定義用オペレータ(defining operator)であるdefunを使ってユーザは関数を定義できます。

マクロ
すべての引数を、そのまま、マクロ本体に渡す。

なぜ、マクロがあるかと言うと、if, whileなどのような構文をユーザが自由に定義できるようにするためです。
たとえば、if構文は以下のような見た目をしています。

(if test-form then-form [else-form])

あるLisp Objectを評価(eval)されるものとして見るとき、それをフォーム(form)と呼びます。

KISS>(if (> 10 0) 'plus 'minus-or-zero)

plus

;; test-formである(> 10 0)を評価すると真(t)なので
;; then-form である'plus が評価されplusが返された
;; else-form である'minus-or-zero は一度も評価されないまま

KISS>

私が実装中のISLisp処理系KISSでは、スペシャルオペレータ if は次のように実装されています。
スペシャルオペレータは、あらかじめ用意されたオペレータで、引数の扱いから見るとマクロのように見えます。

/* special operator: (if test-form then-form [else-form]) -> <object> */
kiss_obj* kiss_if(kiss_obj* test_form, kiss_obj* then_form, kiss_obj* rest) 
{
  //test_form を評価(kiss_eval)した結果が KISS_NIL か
    //どうかで、then_form か rest の
    //どちらか一方だけが、
    //評価(kiss_eval)される
    if (kiss_eval(test_form) != KISS_NIL) {
	return kiss_eval(then_form);
    } else {
	return kiss_eval_body(rest);
    }
}

定義用オペレータ(defining operator)であるdefmacroを使ってユーザはマクロを定義できますが、どの引数が評価されるかは、自由に決められます。