CL入門 No.7 - シンボルとパッケージ

SYMBOL

プログラムを見てみると、
実はたくさんのシンボルが登場しています。

t
=> T
(+ 4 2)
=> 6
(cons 'a 'b)
=> (A . B)

tはなぜtなのか。+はなぜ加算演算になるのか。
consがなぜ新しいCONSセルを生成するのか。

これら全てはSYMBOLによって解決されるのです。

SYMBOLはそれ自身ではなく
「指し示す別の値」に評価されるオブジェクトです。
S式は「Symbolic-Expression」という名前どおり、
SYMBOLのリストによって構成されます。

t, +, consはもちろんa, bもSYMBOLです。

'a
=> A

SYMBOLを評価すると指し示す値に評価されてしまうため、
SYMBOL自身を扱う場合はQUOTEするわけです。

大文字と小文字

SYMBOLに含まれるアルファベット(A-Z)は、
大文字・小文字の区別がありません。
今までのプログラムで大文字でも小文字でも表現していましたが、
それらは同じSYMBOLです。

大文字と小文字を区別するプログラミング言語を知っている人には
不思議に思われる仕様ですが、大文字と小文字の区別を付けないことで
同じSYMBOLを複数の表現で記述できるメリットがあります。

例えば、
プログラマの書くソースコードのSYMBOLは小文字で統一して、
PRINTが出力するSYMBOLは大文字で統一していれば、
見ただけで出力なのか入力なのか判断がつくようになります。

値スロット

SYMBOLを評価した場合、束縛された値に評価されます。
SYMBOLには値スロットがあり、値を記録しているからです。

SYMBOL-VALUE関数でSYMBOLの値を取得することができます。

(symbol-value 't)
=> T

関数スロット

SYMBOLがリストの先頭に現れたとき、それは関数の名前として扱われます。
SYMBOLには値と別に関数を記録するスロット*1があり、
SYMBOL-FUNCTION関数によってアクセスすることができます。

(symbol-function 'cons)
=> #<FUNCTION CONS>

関数オブジェクトはPRINT-READ可換な表現*2を持たないため、
「CONSという名前の関数」であると表示されます。

関数の詳細な構造は、例えばInspectを利用すると閲覧することができます。

PACKAGE

同じ名前のSYMBOLは同じ値、同じ関数を指します。
そうでなければ、名前を付けて繰り返し参照することができないからです。

しかし、SYMBOLのテーブルが世界にたった一つしか無いと
すぐに名前が枯渇してしまいます。
この問題を解決するために存在する機能がPACKAGEです。

PACKAGEとはSYMBOLの表です。
複数の名前が違うSYMBOLが表になって登録されています。
そして、PACKAGE自体もPACKAGEの表に登録され、
PACKAGE固有の名前を持っています。

PACKAGEも含めた完全なSYMBOLの記法は::を用います。

PACKAGE-NAME::SYMBOL-NAME

またはEXTERN*3されたSYMBOLに関しては:で表記することができます。

PACKAGE-NAME:EXTERNED-SYMBOL-NAME

ソースコード中のほとんどのSYMBOLがPACKAGEを省略しています。
PACKAGEが省略されたSYMBOLはREAD時点での、COMMON-LISP:*PACKAGE*に束縛されたPACKAGEにINTERN*4されます。

標準Common Lispの全てのSYMBOLは、COMMON-LISPパッケージに属します。
そしてデフォルトでは、Common Lisp処理系開始時の*PACKAGE*には、COMMON-LISP-USERというパッケージが束縛されています。

デフォルトのPACKAGEを切り替える

「*PACKAGE*」を切り替えたいときは、IN-PACKAGE関数を使います。

(in-package "COMMON-LISP")
=> #<PACKAGE "COMMON-LISP">
*package*
=> #<PACKAGE "COMMON-LISP">

全てのPACKAGEを確認する

現在の処理系上に存在する全てのPACKAGEは、
LIST-ALL-PACKAGES関数で確認できます。

(list-all-packages)
=> ( ...省略... )

新しいSYMBOL

ソースコードとして記述している以上、
通常表記ではSYMBOLは何らかのPACKAGEに属してしまいます。
しかしCommon Lispでは、
いずれのPACKAGEにもINTERNされていないSYMBOLを生成することができます。

  • MAKE-SYMBOLは、INTERNされていない指定した名前のSYMBOLを生成します。
  • COPY-SYMBOLは、INTERNされていない同じ名前のSYMBOLを複製します。
  • GENSYMはINTERNされていない識別番号付のSYMBOLを生成します。
  • #:というリーダーマクロを使えば、INTERNされていないSYMBOLにREADされます。

INTERNされていないSYMBOLはPACKAGEに所属していないため、
他のSYMBOLと衝突することがないという性質を持ちます。
つまり局所的に利用するSYMBOLを作り出すことができるというわけです。

このSYMBOLは、マクロ機能*5を利用する上で大いに役立ちます。
マクロに関してはまた後ほど記事にまとめていきます。

キーワード

キーワードはそれ自身に評価される特別なSYMBOLです。
キーワードは:を使って次のように表現します。

:KEYWORD-NAME

キーワードはKEYWORDパッケージにINTERNされています。
キーワードはもちろん文字列の代わりに一意な名前として使用しますが、
他にもラムダリストのキーワード機能*6を使えば、
関数の引数を強力な表現付きでディスパッチすることができます。

まとめ

SYMBOLは他の値や関数を指し示すオブジェクトです。
PACKAGEはSYMBOLの表で、SYMBOLはPACKAGEに所属しています。

SYMBOLはS式の根幹を成すオブジェクトなので、
その性質を熟知しておきましょう。*7

次回は、Common Lispにおける「変数」についてまとめていきます。

*1:このように変数値と関数が別の名前空間に分かれている言語をLisp-2と言います

*2:PRINTしたものをREADして元と同じ構造に復元できるもの

*3:PACKAGEのSYMBOLを外部のPACKAGEから参照しやすくする機能。

*4:SYMBOLをPACKAGEに登録する機能。

*5:Lispの目玉である「プログラムを生成するプログラム」のこと。

*6:Common Lispでは引数を関数に対してキーワードで名前指定して渡すことができます。

*7:他のプログラミング言語では、必ずしも第一級オブジェクトとしてSYMBOLが存在しない。