第10章「シンボル」

概要

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マクロで使うとシンボルに値を設定することもできます。ただし、setqsetfを生で使うこともできます。
Accessor: symbol-function
シンボルを引数に取り、そのシンボルが束縛している関数を返す関数です。マクロやスペシャルオペレータのシンボルに対して使用した場合の返り値は処理系依存です。。(setf symbol-function)関数も同時に定義されており、setfマクロで使うと関数の設定に利用することもできます。
これら5つのスロットは基本的には構造体やクラスの「スロット」と同じですが、歴史的な理由からシンボルについてのみ「セル」と呼ばれます。そのため、このページでは「セル」を使います。

試しに、+関数のセルを確かめてみましょう。
(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関数で値セルを見ることができるのはスペシャル変数だけだからです。letlet*で束縛されるレキシカル変数は値セルを見ることができず、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 #&ltCOMPILED-FUNCTION SYSTEM::TYPE-SYMBOL-NIL&gt)

(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 件のコメント :

コメントを投稿