近況
KuinというPascalライクな言語をさわってます。海外ではLisp系言語よりもPascalのほうが人気があるという噂もあるみたいです。KuinはWindowsのみの対応ですがとても軽装な処理系なので割と気に入っています。
本記事の目的
リーダーマクロについては実は結構日本語の記事が充実しています。本記事は、それらの記事とは一線を画すためにわざとリーダーマクロ自体の解説はせず、周辺ドキュメントの構成や概要を説明することでLispにおけるリーダーマクロのポジションが明確になるようにしました。リーダーマクロで詰まったときにドキュメントのどの部分を参照すればいいのかを、あらかじめドキュメントを読むことでカバーしていこうという試みです。
Common Lisp HyperSpec
Common Lispを触っている人間なら誰でも知っている超有名なドキュメントです。実はこのドキュメントはLispWorksという有償のCommon Lisp処理系のマニュアルの一部なのですが、このドキュメントが秀逸であるためHyperSpecを愛用しているLisperは多い、と私は思っています。
さて、このトップページを見ると、古めかしいアイコンが8個並んでいます。このトップページを見て面食らった人も多いのではないでしょうか。
- Starting points: ノータッチ
- Highlights: ノータッチ
- Contents: いわゆる目次です。
- Master Index: ノータッチ
- Symbol Index: これは使えます。しかしこれの代わりに[Contents] - [1. Introduction] - [1.9 Symbols in the COMMON-LISP Package]を使用することをおすすめします。
- Glossary: 用語集。これは用語の品詞と発音が書いてあるガチなやつです。
- x3j13 issues: 読んでない。理解してない。
- Help: ノータッチ。しかし改めて読み返してみると一番大事なことが書いてある気が…
このHyperSpecというドキュメントはCommon Lispの言語仕様についてのものなので、その拡張であるASDFやMOPについての解説はありません。
それでは以下にHyperSpecについての読書感想文を綴っていきますので、HyperSpecへの関心を深めていただければと思います。
1.9 Symbols in the COMMON-LISP Package
Common Lispで用意されているシンボル(スペシャル・フォーム、マクロ、関数、変数等)の一覧が記述されています。このページをブラウザでテキスト検索するとシンボル名で検索できますので、ここからCommon Lispの仕様に切り込んでいくやり方は割といいやり方だと思います。
2. Syntax
Lispには文法がないですって!当然あります。
2.1 Character Syntax
Lispリーダーはストリームから文字を読み込んでオブジェクトを返します。Cなどの言語ではパーサと呼ばれる部分であり、パーサは抽象構文木を返します。それらの言語では抽象構文木自体にユーザーがアクセスすることはできません。しかしLispならそれができるのです。Lispはリフレクション機能よりももっと強力な自由を提供します。それはLispリーダーの出力にユーザーがアクセス可能だからです。
2.1.1 Readtables
Lispリーダーはリードテーブルというオブジェクトによって定義されます。Common Lispの仕様では、Common Lispの仕様で定義されるstandard readtableからマクロ文字を増やすことは可能ですが、減らすことは残念ながらできません。従ってLispリーダーによってC言語のパーサーを作成することはできません。
ただし、Lispリーダーからすべてのマクロを取り去ってしまう慣用的なやり方があるみたいです(検索してください)。別に私はそこまでしてLispリーダーでLisp以外の言語パーサーを作ろうとは思いません。
さて、リードマクロはこの節にある関数群を用います。これらの関数群の使い方自体については他の人が詳しく解説していますのでそれらを併用してください。
2.1.3 Standard Characters
Common Lispで最低限扱わなければならない文字の一覧です。手っ取り早く言えばASCII文字のサブセットです。
2.1.4 Character Syntax Types
後の2.2 Reader Algorithmと絡んできます。この中で、syntax type(以下文法型と記す)というものが登場してきます。文法型について簡単に解説します。
- constituent: 構成文字。この文字が読み込まれるとトークンが開始される。
- whitespace[2]: 空白文字。この文字が読み込まれるとトークンが終了する。
- terminating macro character: 終端マクロ文字。リードマクロが実行される。トークンの途中であればトークンを終了する。
- non-terminating macro character: 非終端マクロ文字。リードマクロが実行される。トークンの途中であればリードマクロが実行されず、この文字自体がトークンにとりこまれる。
- single escape: 単一エスケープ文字。空白文字や終端マクロ文字をトークンの構成文字として取り込んだり、アルファベットの大文字・小文字変換を回避したりする。
- multiple escape: 複数エスケープ文字。空白文字や終端マクロ文字をトークンの構成文字として取り込んだり、アルファベットの大文字・小文字変換を回避したりする。
2.2 Reader Algorithm
Lispリーダーのアルゴリズムを記述しています。ただしこのアルゴリズムがLispWorks以外の処理系に適用される保証はありません。CLtL2とANSI Common Lispにはこのアルゴリズムが記述されていなかったように思います。
このアルゴリズムを見て分かることは、Lispリーダーが\
(single escape)と|
(multiple escape)をとても慎重に扱っているということです。Common Lispのシンボルは実はcase-sensitiveです。つまりa
とA
を区別するのです。しかしLispリーダーがエスケープされていないアルファベットを大文字に変換する(デフォルトの場合)ことによってcase-insensitiveであるかのように見せているということです。
ためしにSBCLでsingle escapeとmultiple escapeの挙動を見てみます。
* 'abcd
ABCD
* 'ABCD
ABCD
* 'aBcD
ABCD
* '\abcd
|aBCD|
* '\ab\cd
|aBcD|
* '|abcd|
|abcd|
* 'ABCD
ABCD
* '|aBcD|
|aBcD|
* '|Common Lisp|
|Common Lisp|
2.3.3 The Consing Dot
.
というトークンはドット記法のドットでないならばエラーが送出されるというものです。
2.4.8.1 Sharpsign Backslash
#\a
が文字a
であることは多くのLisperが知っていると思います。
2.4.8.2 Sharpsign Single-Quote
#'+
が(function +)
と同じ意味であることも周知のことと思います。SchemeからLispを学んだ人間はCommon Lispにおいてmap
がmapcar
で、関数を代入するときに#'
が必要であり、lambda
を呼び出すのにfuncall
が必要であることに絶望したことでしょう。
2.4.8.3 Sharpsign Left-Parenthesis
#(1 2 3)
がベクターであることが記述されています。#6(1 2 3)
と記述した場合はベクターの長さが6になります。
2.4.8.4 Sharpsign Asterisk
ビットベクターを記述するためのものです。
* (aref #*0000010000 5)
1
* (aref #*0000010000 6)
0
2.4.8.5 Sharpsign Colon
いわゆるgensym
で作ったシンボルを表すためのものです。マクロでシンボル名の衝突を防ぐために使われます。
2.4.8.6 Sharpsign Dot
読み込み時評価をするためのものです。
* (defvar foo 1)
FOO
* #.foo
1
2.4.8.10 Sharpsign R
n進数表記。#3r100
は9です。
2.4.8.12 Sharpsign A
n次元配列。#2a((1 0) (0 1))
は行列、#3a(((1 2 3) (4 5 6) (7 8 9)) ((10 11 12) (13 14 15) (16 17 18)) ((19 20 21) (22 23 24) (25 26 27)))
はテンソルです。
2.4.8.13 Sharpsign S
構造体。ただし構造体型を別途defstruct
で作らなければなりません。
* (defstruct foo (a 0) (b 0) (c 0))
FOO
* (make-foo :a 1 :b 2 :c 3)
#S(FOO :A 1 :B 2 :C 3)
* #s(foo :a 4 :b 5 :c 6)
#S(FOO :A 4 :B 5 :C 6)
2.4.8.14 Sharpsign P
パス名(ディレクトリパス・ファイルパス)を示すためのものです。
* (user-homedir-pathname)
#P"/home/user1/"
2.4.8.15-16 Sharpsign Equal-Sign, Sharpsign Sharpsign
循環リストを作成したりするためにオブジェクトにラベル付けするためのものです。例えば#1=(1 2 . #1#)
は循環リストなのでこれをインタプリタで直接評価するとスタックオーバーフローを起こすことがあります。(subseq '#1=(1 2 . #1#)) 0 10)
などと記述すると安全にその中身を見ることができます。
* (subseq '#1=(1 2 . #1#) 0 10)
(1 2 1 2 1 2 1 2 1 2)
2.4.8.17 Sharpsign Plus, Sharpsign Minus
C言語のifdef, ifndefと似ています。使い方は、Section 24.1.2.1.1 (Examples of Feature Expressions)を参照してください。
2.4.8.19 Sharpsign Vertical-Bar
複数行コメントを書くのに使います。
* #|これはコメントです|#1
1
2.4.8.20 Sharpsign Less-Than-Sign
特殊なオブジェクトを表示するためのものです。従って、#<
と入力するとエラーになるようになっています。
* (make-string-output-stream)
#<SB-IMPL::STRING-OUTPUT-STREAM {1002991843}>
Lispのインタプリタはリーダーで文字列を解釈し、プリンターでオブジェクトを文字列に変換します。リーダーとプリンターとではできるだけ表記が一致するようになっています。当たり前ですが、リーダーにマクロを追加したときはリーダーだけが変更されるのでプリンターの出力は変更されません。
2.4.8.21 Sharpsign Whitespace
#
は非終端マクロ(non-terminal macro character)なので#
の後に空白が来るとエラーになります。ただしこの組み合わせのマクロをユーザーが追加している場合はそれが呼び出されます。
* (set-dispatch-macro-character #\# #\Space
#'(lambda (s c n) (declare (ignore s))
(declare (ignore c))
(declare (ignore n)) 1))
T
* #
1
ここまでLispリーダーの#
マクロの使い方を見てきて、#
と空白の組み合わせでユーザーマクロ定義することがよくないことだということが分かるかと思います。
2.4.8.22 Sharpsign Right-Parenthesis
Lispリーダーはエラーを送出します。ただしユーザーが(略)
2.4.9 Re-Reading Abbreviated Expressions
LispではオブジェクトをS式に変換してファイル出力して、それを直接ファイル入力するような使い方ができます。ただし*print-level*
, *print-length*
, *print-lines*
が設定されているとS式が..などで省略されるので正しくリードされません(当たり前)。
14. Conses
HyperSpecでリストのことについて知りたければListsではなくConsesの章を参照してください。
注意:「非終端マクロ文字」とは、トークンの中でも使えるマクロという意味ではない
非終端マクロ文字(non-terminating macro character
)と終端マクロ文字(terminating macro character
)の違いは、トークンを終わらせるかどうかということです。なぜならば、HyperSpec「2.2 Reader Algorithm」中で非終端マクロ文字と終端マクロ文字の違いが出るのはステップ8であり、ステップ8はトークンの途中だからです。
証拠をお見せしましょう。SBCLで実際に打ち込んでみます。
* (defvar abc#b0101def 1)
|ABC#B0101DEF|
* (defvar abc;def
1)
ABC
非終端マクロ文字である#
はトークンの途中ではマクロとみなされず、終端マクロ文字である;
はトークンの途中でもそれを打ち切ってマクロを開始しています。
SBCLのリーダーマクロ
SBCLのリーダーマクロはsrc/code/reader.lispの[basic readmacro definitions]以下に書いてあります。例えばシングルクォート('
)のリーダーマクロはこのような感じです。
(defun read-quote (stream ignore)
(declare (ignore ignore))
(list 'quote (read stream t nil t)))
リーダーマクロの関数を処理系が用意しているLispリーダーと紐づけているのはこの部分です。
;; Easy macro-character definitions are in this source file.
(set-macro-character #\" #'read-string)
(set-macro-character #\' #'read-quote)
;; Using symbols makes these traceable and redefineable with ease,
;; as well as avoids a forward-referenced function (from "backq")
(set-macro-character #\( 'read-list)
(set-macro-character #\) 'read-right-paren)
(set-macro-character #\; #'read-comment)
その下に何か書いてあります。
;; (The hairier macro-character definitions, for #\# and #\`, are
;; defined elsewhere, in their own source files.)
これらのソースファイルがそれに該当しそうです。
src/code/sharpm.lisp
src/code/backq.lisp
Lispのマクロは何でもできるわけではない
クォートされたS式はAST(抽象構文木)なので、マクロは構文木の中身を変えることができますが、マクロが適用されるのは、あくまでマクロの中であってマクロの外に影響を及ぼすことはできません。もし、プログラムを大域的に改変したい場合はプログラムのルートにマクロを配置する必要があります。単にdefmacroするだけではなく、プログラムを一つの変数に集約してそれにマクロを適用する操作を必要とします。つまりマクロが全部をやってくれるわけではないのです。
それと同様に、Lispリーダーを改変するリーダーマクロも万能ではありません。それはリーダーマクロの上位にリーダーアルゴリズムがあり、ストリームの全体を掌握するリーダーマクロというものを記述できないためです。読み込まれるストリームの全体に対してLispリーダーとは異なる解釈をさせたい場合は、単にリーダーを自作します。なんでもリードテーブルでできるわけではないのです。