M.Hiroi's Home Page

Common Lisp Programming

お気楽 Common Lisp プログラミング入門

[ PrevPage | Common Lisp | NextPage ]

属性リスト

●属性リストの構造

「属性リスト (property list : plist)」は、キーとなるシンボル(属性名)とそれに対応するデータ(属性値)が交互に配置されたリストです。次の図を見てください。

このままでは要素の並び方を決めただけで、普通のリストと同じではないかと思われることでしょう。確かに、属性リストの構造は属性名と属性値を交互に並べただけで、普通のリストとの違いはまったくありません。では、どこが違うのでしょうか。それは、属性リストがシンボルに格納されるところです。

●シンボルの構造

下図にシンボルの構造 (概略) を示します。

このように、シンボルは 4 つの要素を持ったデータ構造と考えることができます。

まず、自分自身の名前ですね。この名前があるおかげで、シンボルを特定することができます。次は関数定義を格納する場所です。defun で関数を定義すると、その関数はここに格納されます。その次が変数の値を格納する場所です。関数定義と変数値を格納する場所が異なっているので、関数が定義されているシンボルでも変数として使うことができます。最後が属性リストです。属性リストも変数値とは異なる場所に格納されるので、属性リストをセットしたシンボルでも、それを変数として使うことができます。

●属性リストの操作関数

それでは、属性リストを操作する関数を説明します。

属性リストからデータを取り出すには関数 get を使います。

get symbol key &optional default

関数 get は symbol の属性リストから属性名 key の属性値を返します。見つからない場合は default を返します。default が省略された場合は NIL を返します。

Common Lisp の場合、属性リストにデータを書き込むときは setf と get を使います。

setf (get symbol key) value

関数 get は汎変数として使用することができます。

属性リストからデータを削除するには関数 remprop を使います。

remprop symbol key

関数 remprop は symbol の属性リストから属性名 key を削除します。属性名 key が見つからない場合は NIL を返します。key を見つけて削除したら NIL 以外の値を返します。SBCL の場合、属性名とその値を格納したリストを返します。

このほかに、Common Lisp には関数 symbol-plist があります。

symbol-plist symbol

関数 symbol-plist は symbol にセットされた属性リストを返します。

●簡単な使用例

それでは、簡単な使用例を示しましょう。

* (setf (get 'tarou 'height) 180)

180
* (setf (get 'tarou 'weight) 80)

80
* (symbol-plist 'tarou)

(WEIGHT 80 HEIGHT 180)
* (get 'tarou 'height)

180
* (get 'tarou 'weight)

80
* (get 'tarou 'bust)

NIL
* (remprop 'tarou 'height)

(HEIGHT 180)
* (get 'tarou 'height)

NIL
* (symbol-plist 'tarou)

(WEIGHT 80)

最初に setf で TAROU の身長 (height) と体重 (weight) を属性リストに設定します。symbol-plist でシンボル TAROU の属性リストを求めると、WEIGHT と HEIGHT という 2 つの属性が設定されていることがわかります。それから、get と remprop は関数なので引数は評価されます。TAROU, HEIGHT, WEIGHT のシンボルにはクォートを忘れないで下さいね。

次に get でデータを取り出します。HEIGHT という属性名はありますが、属性名 BUST は設定されていないので NIL を返します。remprop でデータを削除したあとは、get でデータを取り出すことはできません。実際に symbol-plist で属性リストを表示すると、HEIGHT という属性が削除されていることがわかります。


マクロ

今までにいろいろな関数を作ってきましたが、Common Lisp は関数だけではなく、「マクロ (macro)」を定義することができます。Common Lisp でプログラミングする場合、ほとんどの処理は defun で定義する関数で作成することができます。ところが、場合によっては引数を評価しない関数が必要になることもあります。ここで役に立つのがマクロなのです。また、マクロを使って新しい制御構造を定義することもできます。

●マクロの定義

C/C++ユーザーであればマクロはお馴染みの機能なので、よく使っていることと思います。ところが Lisp のマクロはちょっと毛色が変わっています。C言語のマクロは、記号を定義した文字列に置き換えるという機能です。これに対して Lisp のマクロは、まさに Lisp らしいといえる機能を持っています。C言語との対比でいえば、「S 式を置き換える」と表現することができます。

それでは、Lisp におけるマクロの使い方を説明しましょう。Lisp ではマクロを関数のように定義します。マクロを定義するには defmacro を使います。

defmacro マクロ名 (仮引数 ...) S式 ...

defmacro の構文は defun と同じです。defmacro で定義されたマクロは、次のような特徴を持ちます。

  1. 引数は評価されない。
  2. S 式を順番に評価し、いちばん最後の評価結果を再度評価して、その結果を返す。

この 2 番目の機能が Lisp におけるマクロの特徴です。これを図に示すと、次のようになります。

 [S式] --  評価 --> [新しいS式] -- 評価 --> [マクロの返り値] 

        (マクロ展開)


                    図 : マクロの動作

S 式を評価することで新しい S 式を組み立てます。この部分がマクロ展開に相当します。そして、その S 式を評価した値がマクロの返り値となります。S 式を組み立てるということはプログラムを作ることと同じです。これは、リストにプログラムとデータの 2 つの役割を持たせている Lisp だからこそ可能なことなのです。

●マクロと関数の違い

まず、マクロと関数の違いを理解するために、数を 2 乗する処理をマクロと関数で作ってみましょう。関数は簡単ですね。

リスト : 数を 2 乗する関数

(defun square (x) (* x x))

マクロは次のように定義します。

リスト : 数を 2 乗するマクロ

(defmacro m-square (x) (list '* x x))

マクロ名は m-square としました。それでは、引数に (+ 1 2) を与えて m-square を評価してみます。

(m-square (+ 1 2))

仮引数 x に (+ 1 2) がセット (評価されないことに注意)

マクロの本体 (list '* x x) を評価する

=> (* (+ 1 2) (+ 1 2))  (S式が組み立てられる)

=> 9                    (S式を評価した結果)


        図 : マクロの実行

関数であれば引数 (+ 1 2) が評価されて、その返り値である 3 が square に渡されますね。マクロの場合、引数は評価されないので、仮引数 X には S 式である (+ 1 2) がそのままセットされます。

次に、マクロ本体を評価します。マクロを使いこなすポイントですが、まず評価したい S 式を組み立てることを考えます。最初の評価で S 式を組み立て、それを評価することで目的の処理を実現するのがマクロなのです。

この場合、引数の 2 乗する (* x x) という S 式を作ればいいわけです。list は引数を要素とする新しいリストを返す関数でしたね。この場合、シンボル * と X の値である (+ 1 2) が要素となったリストが返されます。

これでマクロ展開が終了しました。マクロの仮引数は、マクロ展開されるときだけ有効です。マクロ展開されたS式を評価するときは、それらの値は破棄されます。あとは、この S 式を評価して 9 という値が結果となります。

次に示す関数 foo をを引数に与えて square と m-square を評価すると、関数とマクロの違いがよくわかると思います。

* (defun foo (x) (format t "~D " x) x)

FOO
* (square (foo 2))
2
4
* (m-square (foo 2))
2 2
4

関数 square は、引数が評価されるので 2 が 1 回だけ出力されます。ところが m-square では、引数は評価されずに渡されて S 式 (* (foo 2) (foo 2)) が組み立てられます。その後、この S 式が評価されるので 2 が 2 回出力されるのです。

●マクロとコンパイラの関係

ところで、昔の Lisp 処理系では、引数を評価するタイプを EXPR 型や SUBR 型、引数を評価しないタイプを NEXPR 型や FSUBR 型と呼び、ユーザーが NEXPR 型の関数を定義することができました。Common Lisp や Scheme の場合、ユーザーが定義できるのは関数とマクロだけです。特殊形式の関数を定義する場合はマクロを使うことになります。

マクロを実行する場合、必ずマクロ展開が行われるため、通常の関数よりも実行時間は遅くなります。だったら、NEXPR 型の関数を定義できるようにした方が実行速度の点で有利なはずです。ところが、Common Lisp や Scheme では必要最低限の特殊形式を定義し、よく使われる制御構造はマクロで定義されています。これではインタプリタでの動作が遅くなります。

では、なぜ実行速度が遅くなるのにマクロを使っているのでしょう。それは、Lisp / Scheme 処理系がコンパイラの使用を前提としているからです。たとえば、CLISP や Gauche (Scheme) はプログラムをバイトコードにコンパイルすることができます。今の実用的な Lisp / Scheme 処理系のほとんどは、プログラムをバイトコードまたはネイティブコードにコンパイルすることができます。

プログラムでマクロを呼び出している場所は、コンパイル時にマクロ展開されるため、コンパイル済みのコードにはマクロ呼び出しがなくなってしまうのです。つまり、コンパイル済みのコードは、マクロを呼び出す処理とマクロ展開の処理がなくなることにより、確実にインタプリタよりも高速に実行することができるのです。逆にいえば、コンパイラを使わないとマクロを効果的に使うことはできません。ご注意くださいませ。

●スタックの操作

今度は、もう少し複雑な例を見てみましょう。スタックを操作する関数をマクロで定義してみます。 逐次実行 で作成した関数 push-stack と pop-stack は、スペシャル変数 *STACK* にスタックを保持しましたが、これから作成するマクロは、スタック用の変数を引数として渡すことにします。

Common Lisp には push と pop が用意されているので、マクロ名は my-push と my-pop にします。my-push は次のようになります。

リスト : my-push

(defmacro my-push (item place)
  (list 'setq
        place
        (list 'cons item place)))

それでは、実際に試してみましょう。

* (defvar a nil)

A
* (my-push 10 a)

(10)
* (my-push 20 a)

(20 10)
* a

(20 10)

最初に変数 A を NIL に初期化しておきます。my-push は list を使って S 式を組み立てます。PLACE には A が、ITEM には 10 がセットされているので、(setq a (cons 10 a)) という S 式が組み立てられます。この S 式が再度評価されて、変数 A に (10) がセットされます。マクロですから引数は評価されません。

もし、引数 ITEM の評価結果をスタックに積みたい場合は、ITEM を eval で評価することになりますが、Common Lisp には eval を使うよりも簡単な構文が用意されています。これはあとで説明します。

my-pop も同様に実現できます。

リスト : my-pop

(defmacro my-pop (place)
  (list 'prog1
        (list 'car place)
        (list 'setq
              place
              (list 'cdr place))))

それでは、実際に試してみましょう。

* (setq a '(30 20 10))

(30 20 10)
* (my-pop a)

30
* a

(20 10)
* (my-pop a)

20
* a

(10)

(my-pop a) は (prog1 (car a) (setq a (cdr a))) という S 式に展開され、この S 式が評価されます。実際には prog1 がマクロなので、prog1 をマクロ展開したあとの S 式を評価することになります。

ちなみに、マクロ展開の結果は関数 macroexpand と macroexpand-1 を使って確認することができます。

macroexpand   form &optional env
macroexpand-1 form &optional env

Common Lisp の場合、局所的なマクロを定義することができます。そのようなマクロを考慮する場合は引数 env に設定します。普通のマクロであれば env を指定する必要はありません。macroexpand-1 はマクロを 1 回だけ展開します。つまり、(my-pop a) であればマクロ my-pop を展開するだけで、その中の prog1 は展開されません。macroexpand はマクロを最後まで展開するので、prog1 も展開されます。

それでは (my-pop a) を macroexpand-1 で展開してみましょう。

* (macroexpand-1 '(my-pop a))

(PROG1 (CAR A) (SETQ A (CDR A)))
T

このように、マクロ展開の結果を確認することができます。

●バッククォート

ところで、マクロを定義するとき、S 式を組み立てるため list や cons をたくさん使うことになり少々面倒です。実は、「バッククォート ( ` )」という機能を使うと、S 式を簡単に組み立てることができます。

バッククォートはクォート ( ' ) と同様に引数の評価を行いません。ですが、バッククォートの中でコンマ ( , ) で始まる S 式があると、その S 式を評価した値で置き換えられます。簡単な例を示しましょう。

* (setq v 'pen)

PEN
* `(this is a ,v)

(THIS IS A PEN)

変数 V にはシンボル PEN がセットされています。次の S 式の中で ,v は V を評価した値、つまり PEN に置き換わるのです。また、S 式の評価結果がリストの場合は、コンマアットマーク (,@) を使うことができます。,@ を使うと、リストをはずした値と置き換わります。,@ を使う場合、値がリストでなければエラーになります。次の例を見てください。

* (setq v '(pen))

(PEN)
* `(this is a ,v)

(THIS IS A (PEN))
* `(this is a ,@v)

(THIS IS A PEN)

今度は変数 V にリスト (PEN) がセットされました。次の S 式の中で ,v は (PEN) に置き換わります。そして、その次の S 式の中では、,@v は PEN に置き換わるのです。それから、コンマやコンマアットマークはバッククォートの中でしか使うことができません。ほかの S 式の中で評価した場合はエラーとなります。ご注意くださいませ。

それではバッククォートを使って my-push と my-pop を書き直してみましょう。

リスト : my-push と my-pop の改良

(defmacro my-push (item place)
  `(setq ,place (cons ,item ,place)))

(defmacro my-pop (place)
  `(prog1 (car ,place)
          (setq ,place (cdr ,place))))

my-push は item にコンマ ( , ) がついているので、item を評価した結果がスタックに積まれることに注意してください。list を使うよりもバッククォートの方が、どんな S 式が組み立てられて評価されるのかよくわかると思います。ですが、関数に比べるとマクロは理解するのが難しいですね。そのマクロがどんなことをするのか、きちんとコメントを書くことをお勧めします。

●マクロ展開後の実行環境

ところで、(my-push x a) をマクロ展開すると (setq a (cons x a)) になります。展開後の S 式に X が含まれていますね。この X は、どのように評価されるのでしょうか。次の例を見てください。

 (dotimes (x 10) (my-push x a))
                 ~~~~~~~~~~~~~ マクロ呼び出し

 (dotimes (x 10) (setq a (cons x a))) 
                 ^^^^^^^^^^^^^^^^^^^ マクロ展開されたS式


        図 : my-push の実行環境

マクロ展開された S 式は、マクロ呼び出しと置き換えた状態で評価されます。上の例では、dotimes の中でマクロ my-push が評価されますが、この部分をマクロ展開後の S 式に置き換えて評価するのです。したがって、変数 X は dotimes で定義された局所変数として扱われます。

関数呼び出しでは、関数の仮引数やその中で定義された変数をレキシカル変数として扱いますが、それ以外の変数はスペシャル変数として扱われます。ところがマクロの場合、マクロ展開時には関数呼び出しと同じ規則が適用されますが、展開後の S 式を評価するときは、マクロ呼び出し時に定義されている局所変数が有効になるのです。ご注意くださいませ。

●マクロとラムダリストキーワード

defmacro は defun と同じように、&optional や &rest などのラムダリストキーワードを使うことができます。このほかにも、defmacro で使用できるラムダリストキーワードがあります。

&body は &rest と同じ働きをしますが、プリティプリント機能による整形出力を行う関数 (pprint など) に対して、そのフォームの残りを本体として扱い、字下げすることを指示します。あとの 2 つは使用する機会があれば、そのときに説明することにします。

簡単な例題として、Scheme の define をマクロで定義してみましょう。Scheme は Lisp の方言のひとつです。言語仕様は Lisp よりシンプルで美しいといわれ、熱烈なファンが多いプログラミング言語です。define は次の形式で関数を定義します。

define (関数名 引数1 ... 引数n) 本体 ...

define を defun に変換するマクロを定義します。動作例を示しましょう。

(define (foo x y) (+ x y)) => (defun foo (x y) (+ x y))

この場合、本体は (+ x y) のひとつしかありませんが、複数の S 式が定義できることに注意してください。

プログラムは次のようになります。

リスト : define マクロ

(defmacro define (args-list &body body)
  `(defun ,(car args-list) ,(cdr args-list) ,@body))

本体は &body を使って受け取ります。本体はリストに格納されて変数 body にセットされます。あとはバッククオートを使って S 式を組み立てます。関数名は args-list の先頭要素なので、,(car args-list) で取り出すことができます。残りは引数なので ,(cdr args-list) でいいですね。,@ を使うとリストをはずしてしまうので、期待通りにマクロ展開されません。逆に、body はリストに本体が格納されているので、,@ を使ってリストをはずす必要があります。

それでは実行してみしょう。

* (define (foo x y) (+ x y))

FOO
* (foo 10 20)

30

●分配

define マクロは defmacro の「分配 (destructuring)」という機能を使うと、もっと簡単に定義することができます。この機能は、defmacro のラムダリストを入れ子にすることができる、というものです。ただし、マクロ呼び出しするときは、そのラムダリストと同じリスト構造を与えなければいけません。簡単な例を示しましょう。

(defmacro foo ((a b c) ((d e) (f g))) .....)

(foo ((1 (2 a) (3 b)) (((4 c) 5) ((6 d) 7))))

a => 1
b => (2 a)
c => (3 b)
d => (4 c)
e => 5
f => (6 d)
g => 7

なかなか複雑な構造ですが、defmacro のラムダリストと foo に与える引数が、同じリスト構造になっていることに注意してください。たとえば、最初の引数は (a b c) とリストになっているので、リスト以外のデータを与えるとエラーになります。また、入れ子のリストにラムダリストキーワードを設定することもできます。

それでは define マクロを書き換えてみましょう。

リスト : define マクロ (改良版)

(defmacro define ((name &rest args) &body body)
  `(defun ,name ,args ,@body))
* (define (bar x y) (* x y))

BAR
* (bar 10 20)

200

とても簡単になりましたね。分配はとても強力な機能で、複雑なマクロを書くための必須機能といえます。


多値

一般に、関数の返り値は一つしかありません。複数の値を返す場合、Lisp ではリストに格納して返すのが普通です。この場合、返す側は必要なデータをリストに格納し、受け取る側はリストからデータを取り出す処理が必要になります。ところが、Common Lisp の多値 (multiple values) という機能を使うと、複数の値を簡単にやり取りすることができます。

●複数の値を受け取る

複数の値を受け取るには、マクロ multiple-value-bind を使うと簡単です。

multiple-value-bind (&rest vars) values-form &rest form

multiple-value-bind は、多値を返す関数 values-form を評価し、その結果を vars で定義した変数にセットします。変数はレキシカル変数として設定されるので、multiple-value-bind を実行している間だけ有効です。

簡単な例を示しましょう。Common Lisp には、整数でない値を整数に変換する関数 floor, ceiling, truncate, round が定義されています。これらの関数は 2 つの値 (多値) を返します。

* (truncate 10 3)

3
1
* (let ((x (truncate 10 3))) x)

3
* (multiple-value-bind
    (q r)
    (truncate 10 3)
    (format nil "商 ~D, 余り ~D" q r))

"商 3, 余り 1"

関数 truncate は割り算を行って商と余りを返します。truncate の返り値を変数に代入すると変数の値は商になります。multiple-value-bind を使うと、商のほかに余りも受け取ることができます。Q と R は truncate が返す値を受け取る変数です。次に、truncate を評価して結果を変数にセットします。あとは、残りの form を順番に評価していきます。multiple-value-bind は最後に評価した form の値を返します。

もしも、返される値よりも変数の個数が多い場合、残りの変数には NIL がセットされます。逆に、返される値が変数よりも多い場合、余分な値は捨てられます。次の例を見てください。

* (multiple-value-bind (q)
    (truncate 10 3)
    (list q))

(3)
* (multiple-value-bind (q r s)
    (truncate 10 3)
    (list q r s))

(3 1 NIL)

最初の例では、変数 Q しか定義されていないので、Q には商がセットされますが余りは捨てられます。次の例では、変数 S が定義されていますが、truncate は 2 つの値しか返さないので、S には NIL がセットされます。

●複数の値を返す

次は、複数の値を返す方法を説明します。これはとても簡単で、関数 values を使うだけです。

values &rest args
* (values 1 2 3)

1
2
3

引数 args を値として順番に返します。簡単な例を示しましょう。

* (defun foo (x y z) (values x y))

FOO
* (multiple-value-bind (a b c)
    (foo 1 2 3)
    (list a b c))

(1 2 NIL)

foo は 3 つ受け取る引数から最初の 2 つを返します。これは (values x y) とすればいいですね。実際に試してみると、multiple-value-bind で 2 つの値を受け取ることができます。

●その他

このように、多値の操作は multiple-value-bind と values を使って簡単に行うことができます。このほかにも、便利な関数やマクロがあるので紹介しましょう。

values-list list
multiple-value-list form
* (values-list '(1 2 3))

1
2
3
* (multiple-value-list (truncate 10 3))

(3 1)

関数 values-list は list の要素を多値として返します。逆に、マクロ multiple-value-list は form を評価して、多値をリストに格納して返します。

multiple-value-call function &rest form

特殊形式 multiple-value-call は、form を評価した結果をすべて集めて、それらを引数として function に渡して評価します。次の例を見てください。

(multiple-value-call #'+ (truncate 10 3) (truncate 14 3))
=> (+ 3 1 4 2)
=> 10

最初の truncate は 3 と 1 を返し、次は 4 と 2 を返します。したがって、関数 + には 3, 1, 4, 2 が引数として与えられ、その結果が 10 となるのです。function に #'list を与えれば、multiple-value-list と同様の働きをします。

multiple-value-prog1 form &rest forms

prog1 の多値バージョンです。複数の form を順番に評価しますが、最初に評価した form の結果(多値)を返します。

multiple-value-setq var-list form

multiple-value-bind の setq バージョンです。ようするに、var-list で指定した変数に値を代入するだけで、これらの変数はレキシカル変数として設定されるわけではありません。let などで定義したレキシカル変数や、スペシャル変数へ値を代入するときに使うと便利でしょう。

●簡単な例題

それでは簡単な例題として、ソートとマージ で作成した関数 quick-sort を、多値を使って書き直してみましょう。次のリストを見てください。

リスト : クイックソート (多値版)

;;; リストの分割
(defun partition (p xs pred)
  (let (ys zs)
    (dolist (x xs (values ys zs))
      (if (funcall pred x p) (push x ys) (push x zs)))))

;;; クイックソート
(defun quick-sort (xs pred)
  (if (null xs)
      nil
    (multiple-value-bind
     (ys zs)
     (partition (car xs) (cdr xs) pred)
     (append (quick-sort ys pred)
             (list (car xs))
             (quick-sort zs pred)))))
* (quick-sort '(5 6 4 7 3 8 2 9 1 0) #'<)

(0 1 2 3 4 5 6 7 8 9)
* (quick-sort '(5 6 4 7 3 8 2 9 1 0) #'>)

(9 8 7 6 5 4 3 2 1 0)

関数 partition は values を使って 2 つのリストを多値で返します。関数 quick-sort は parititon の返り値を multiple-value-bind で受け取り、それを変数 YS と ZS にセットします。多値を使うとリストを取り出す操作が不要になるので、こちらのほうがすっきりとしたプログラムになると思います。

もう一つ簡単な例を示します。2 つのリスト xs, ys を受け取り、同じ位置にある要素をリストにまとめ、それをリストに格納して返す関数 zip xs ys と、それを元に戻す関数 unzip xs を作ってみましょう。unzip は多値で返すものとします。プログラムは次のようになります。

リスト : zip と unzip

(defun zip (xs ys) (mapcar #'list xs ys))

(defun unzip (xs)
  (if (null xs)
      (values nil nil)
    (multiple-value-bind
     (ys zs)
     (unzip (cdr xs))
     (values (cons (caar xs) ys) (cons (cadar xs) zs)))))
* (zip '(a b c d e) '(1 2 3 4 5))

((A 1) (B 2) (C 3) (D 4) (E 5))
* (unzip (zip '(a b c d e) '(1 2 3 4 5)))

(A B C D E)
(1 2 3 4 5)

関数 zip は mapcar を使えば簡単ですね。関数 unzip は引数 XS が空リストであれば values で NIL を 2 つ返します。そうでなければ、unzip を再帰呼び出しして返り値を multiple-value-bind で受け取ります。2 つのリストは変数 YS と ZS にセットされます。あとは、(car xs) の先頭要素 (caar xs) を YS に、二番目の要素 (cadar xs) を ZS に追加して、それらを values で返すだけです。


Copyright (C) 2020 Makoto Hiroi
All rights reserved.

[ PrevPage | Common Lisp | NextPage ]