概要
ANSI Common Lispの第10章「シンボル」を説明します。シンボルのスロット(セル)
シンボルはこれまでのサンプルでも至る所に登場してきた、Lispの基本的なデータ構造です。ポイントは、シンボルは単に「変数」なのではなく、データ構造であるという点です。シンボルは以下のスロットを持つ構造体のようなデータです。
- シンボルの名前
- シンボルが属するパッケージ
- シンボルの属性リスト
- シンボルが束縛している値
- シンボルが束縛している関数
- Function:
symbol-name
- シンボルを引数に取り、そのシンボルの名前を文字列として返す関数です。
- Function:
symbol-package
- シンボルを引数に取り、そのシンボルが属するパッケージをオブジェクトとして返す関数です。パッケージは文字列ではなく実体として返り値となります。どのパッケージにも属さないシンボルの場合は
nil
が返り値となります。 - Accessor:
symbol-plist
- シンボルを引数に取り、その属性リストを返り値として返す関数です。
(setf symbol-plist)
関数も同時に定義されており、setf
マクロで使うと属性リストの設定に利用することもできます。 - Accessor:
symbol-value
- シンボルを引数に取り、そのシンボルが束縛している値を返す関数です。レキシカル変数の値は参照することができません。また、
(setf symbol-value)
関数も同時に定義されており、setf
マクロで使うとシンボルに値を設定することもできます。ただし、setq
やsetf
を生で使うこともできます。 - Accessor:
symbol-function
- シンボルを引数に取り、そのシンボルが束縛している関数を返す関数です。マクロやスペシャルオペレータのシンボルに対して使用した場合の返り値は処理系依存です。。
(setf symbol-function)
関数も同時に定義されており、setf
マクロで使うと関数の設定に利用することもできます。
試しに、
+
関数のセルを確かめてみましょう。
(symbol-name '+)
; => "+"
(symbol-package '+)
; => #<PACKAGE COMMON-LISP>
(symbol-plist '+)
(CLOS::%METHOD-COMBINATION #<METHOD-COMBINATION + #x000000020011C811>)
(symbol-value '+)
; => (SYMBOL-PLIST '+)
(symbol-function '+)
; => #<SYSTEM-FUNCTION +>
5つのセル全てに値が格納されています。
+
というシンボルは一般には関数のセルが使われますが、実は変数としても定義されていて、REPLにおける直前の評価式がセットされています。この扱いはANSI Common Lispの標準で定められていますので、直前にどのような評価を行ったかによって+
シンボルのsymbol-value
スロットは変化します。
ここでは
common-lisp
パッケージの+
という既存の標準関数を使いましたが、自分で使うシンボルも同じです。
;; スペシャル変数を定義するとシンボルが用意される
(defparameter a 10)
; => A
;; シンボルが名前としても使われる
(symbol-name 'a)
; => "A"
;; ユーザー定義のシンボルは指定しないと現在のパッケージに属する
;; 現在のパッケージは標準では common-lisp-user パッケージ
(symbol-package 'a)
; => #<PACKAGE COMMON-LISP-USER>
;; symbol-value は単にシンボルを評価するのと同じ
(symbol-value 'a)
; => 10
;; シンボルの属性リストセルに属性リストをセットする
(setf (symbol-plist 'a) '(:weight 50 :height 100))
; => (:WEIGHT 50 :HEIGHT 100)
;; 属性リストをまとめて取り出す
(symbol-plist 'a)
; => (:WEIGHT 50 :HEIGHT 100)
;; シンボルの関数スロットに別の関数の実体をセットする
(setf (symbol-function 'a) (symbol-function '+))
; => #<SYSTEM-FUNCTION +>
;; a が + と同じように働く
(a 1 2)
; => 3
シンボルはこのような「構造体」として様々なデータを格納できます。そして、
quote
スペシャルオペレータの存在により、シンボルをそのシンボルそのものとして扱うことができるため、シンボルは「第1級のオブジェクト( first class object )」であると言えます。
多くの言語にとって変数は数学上の「代数」のように、単に値を指し示すもの、値の代わりに使うことができるものでしかありません。他方、Common Lispのように変数それ自体をデータとして扱うことができる、ということは関数の引数や返り値として使うことができる、ということです。
以下の関数
test-variable
はシンボルを引数に取り、その名前と値を共に出力する関数です。プログラムの開発途中で使うことで、簡単なテスト用ツールとして使うことができます。これはとても簡単な関数ですが、変数(シンボル)を第1級のオブジェクトとして使うことができない言語の場合、変数自体を引数としてやり取りすることができないので、結構難しい処理になると思います。
(defun test-variable (symbol)
(format t "~a = ~s~%" (symbol-name symbol) (symbol-value symbol)))
; => TEST-VARIABLE
(defparameter lisp "Fine!")
; => LISP
(test-variable 'lisp)
; LISP = "Fine!"
; => NIL
ただし、このテスト用関数は不十分です。なぜなら、
symbol-value
関数で値セルを見ることができるのはスペシャル変数だけだからです。let
やlet*
で束縛されるレキシカル変数は値セルを見ることができず、unbound-variable
というエラー(コンディション)が発生します。
(let ((b 20))
(test-variable 'b))
; SYMBOL-VALUE: variable B has no value
; [Condition of type SYSTEM::SIMPLE-UNBOUND-VARIABLE]
シンボルから話はそれますが、このような機能を実現したければ、マクロとして定義すれば達成できます。
(defmacro test-variable (symbol)
`(format t "~a = ~s~%" ',symbol ,symbol))
; => TEST-VARIABLE
(let ((b 20))
(test-variable b))
; B = 20
; => NIL
マクロの場合は引数の評価を抑制することができるので、
'b
ではなくb
としている点に留意してください。シンボルと属性
前節でシンボルに属性リストを格納することができる、と述べましたが、ANSI Common Lispではこのようなシンボルの属性を操作しやすくするために2つの関数が用意されています。- Accessor:
get
- 第1引数にシンボルを、第2引数に属性リストの key を渡すと、属性リストの該当する value が返り値となります。
(setf get)
関数も同時に定義されているので、setf
で使うと属性の設定に使うこともできます。 - Accessor:
remprop
- 第1引数にシンボルを、第2引数に属性リストの key を渡すと、その key と対応する value が属性リストから削除されます。
(defparameter web nil)
; => WEB
(setf (get web :address) "lisp.satoshiweb.net")
; => "lisp.satoshiweb.net"
(setf (get web :title) "独学 Common Lisp")
; => "独学 Common Lisp"
(setf (get web :since) 2017)
; => 2017
web
; => NIL
(symbol-plist web)
; => (:TITLE "独学 Common Lisp"
; :SINCE 2017
; :ADDRESS "lisp.satoshiweb.net"
; SYSTEM::INSTRUCTION 0
; SYSTEM::TYPE-SYMBOL #<COMPILED-FUNCTION SYSTEM::TYPE-SYMBOL-NIL>)
(get web :title)
; => "独学 Common Lisp"
(remprop web :title)
; => T
(get web :title)
; => NIL
属性リストは数が多くなるとアクセス時間が遅くなるので、実際にはあまり利用されませんが、シンボルには値・関数・属性をそれぞれ独立して格納(束縛)できることは覚えておいてください。
シンボルに関するオペレータ
その他のシンボル関連の主要なオペレータを以下に示します。- Function:
symbolp
- 引数がシンボルであるかどうかを判定します。
- Function:
keywordp
- 引数が
keyword
パッケージに属するシンボル(つまりキーワード)であるかどうかを判定します。 - Function:
make-symbol
- 文字列を引数に取り、その文字列を名前セルの値とするようなシンボルを作成して返します。作られたシンボルはそのままではどのパッケージにも属していません。また、基となった文字列と、シンボルの名前セルに格納された文字列が
eq
関数で等しくなるかどうかは処理系依存ですので、注意してください。 - Function:
gensym
- どのパッケージにも属さない新しいシンボルを作成します。主にマクロ展開における名前の衝突を防ぐために用いられます。
- Function:
boundp
- シンボルが束縛済みであるかどうかを判定します。
- Function:
makunbound
- 束縛済みのシンボルを未束縛にします。
make-symbol
関数とgensym
関数はシンボル自体が返り値になりますので、通常はそのシンボルを別のシンボルに束縛して使います。例えば、2つの値を入れ替えるswap
マクロは以下のように定義できます。
(defmacro swap (x y)
(let ((temp (gensym)))
`(let ((,temp ,x))
(setq ,x ,y)
(setq ,y ,temp))))
; => SWAP
(let ((a 1)
(b 2))
(format t "(~a, ~a)~%" a b)
(swap a b)
(format t "(~a, ~a)~%" a b))
; (1, 2)
; (2, 1)
; => NIL
swap
マクロの定義の2行目で(gensym)
していますが、その返り値である新しいシンボルをtemp
を使ってレキシカル変数に束縛しています。マクロ展開の中ではこの新しいシンボルを使うために,temp
としています。もし(gensym)
をした新しいシンボルを使わなければ、交換したい変数の名前がたまたまtemp
だった時に誤作動が起こり得ます。
なお、
gensym
はマクロでよく使うため、定番ライブラリのAlexandriaにはwith-gensyms
という便利マクロが定義されています。複数のgensym
が必要になる場合は、こちらの使用も検討してみてください。
0 件のコメント :
コメントを投稿