独学 Common Lisp

Chapter 2. 構文

概要

ANSI Common Lispの第2章のうち、特殊文字 , 用法 , リーダーマクロ について説明します。

  1. 特殊文字
  2. 特殊文字の意味と基本的な用法
  3. リーダーマクロ
    1. set-macro-character
    2. set-dispatch-macro-character
    3. 参考: read-delimited-list

特殊文字

Common Lispにおける以下の文字は特殊な文字として構文上の意味を持ちます。

特殊文字一覧

文字 意味
Whitespace区切り
( リストの始端
) リストの終端
' 引用化(quote)
; コメント化
" 文字列化
` 準引用
, 準引用中における引用効果の打ち消し
# 次に続く文字をリーダーマクロとして利用

特殊文字の意味と基本的な用法

特殊文字の基本的な用法は以下の通りです。S式リストコメント引用について示します。

S式とトークン

Common Lispのプログラムは S式 の集合として構成されます。S式とは以下のようなコードです。

(+ 1 2)
; => 3

半角のスペースやタブ文字などは全て Whitespace となり、区切り(トークン)として解釈されます。上記のコードでは+1の間及び12の間の半角スペース、そして);の間の改行がトークンです。

リスト

S式には厳密な定義がありますが、その代表的なものは「 リスト 」です。リストは(で始まり、)で終わります。

コメント

;以降はコメントとして解釈されます。これは1行コメントで、複数行コメントの場合は#||#を使用します。

#| 複数行の
   コメントです
|#

ただし、複数行コメントはあまり好まれません。Common Lispでは;の数によってコメントの重要性を使い分ける文化があり、;;;;はソースコードの一番上に配置するような重要なコメントを意味します。また、関数や変数にはドキュメントを含めることができるため、定義の説明はコメントではなくドキュメント文字列として定義します。

引用(quote)

引用化とは、ソースコードをそのまま引用することを意味します。

'(+ 1 2)
; => (+ 1 2)

この機能により、ソースコードそのものをデータとして扱うことが可能になるのがCommon Lispの特徴です。なお、引用となると全て大文字に変換されます。

'(abc def)
; => (ABC DEF)

小文字や空白を含めたい場合は|で囲みます。

'(|abc| |Hello, World!|)
; => (|abc| |Hello, World!|)

準引用

引用(quote)は全てのデータをリテラル化しますが、一部を評価したい場合は準引用を使用します。準引用の中では「引用の打ち消し」を行うことができます。

;; 引用打ち消しを行わない場合は、単に引用(quote)と同じ
`(1 (+ 1 1) 3)
; => (1 (+ 1 1) 3)

;; 引用打ち消しの内部では式の評価が行われる
`(1 ,(+ 1 1) 3)
; => (1 2 3)

準引用はマクロの定義に不可欠ですが、マクロ以外でも利用できます。

リーダーマクロ

このような特殊文字の扱いを可能にしているのが「 リーダーマクロ 」です。Common Lispではリーダーマクロを自分で定義することも可能なため、特殊文字を自ら拡張することができます。

リーダーマクロには1文字だけのマクロを扱うset-macro-characterと、#と組み合わせるset-dispatch-macro-characterがあります。

1文字のリーダーマクロ(set-macro-character)

例えば、非常に多くのベクトル(1次元配列)を扱うプログラムで、ベクトルの要素への参照を多用する場合があるとします。長さを変更できないベクトルはsimple-vectorと呼ばれており、svref関数で高速にアクセスできます。

(svref simple-vector index)

この書き方が煩雑だと感じ、以下のように書きたい場合は、文字[に対してリーダーマクロを与えることで実現できます。

[ simple-vector index ]

このようなリーダーマクロの定義は以下の通りです。

(defun svref-reader (stream char)
  "Set the character '[' to `svref` Function.
Need whitespace for token.
(Example: [ simple-vector index ] = (sref simple-vector index))"
  (declare (ignore char))
  (let ((vector-name (read stream t nil t))
        (vector-ref (read stream t nil t))
        (end-bracket (read stream t nil t)))
    (declare (ignore end-bracket))
    (list 'svref vector-name vector-ref)))
(set-macro-character #[ #'svref-reader)

ポイントは3点です。

  1. 1文字のリーダーマクロは 関数 として定義(defun)します。
  2. リーダーマクロ関数は 入力ストリーム文字 を引数に取りますが、文字自体は使わないためignoreします。
  3. set-macro-characterを使って 文字関数 を関連づけます。(これで文字が「特殊文字」になります。)

このファイルを test.lisp などで保存し、(load "test")とすることでCommon Lispのリーダーを拡張できます。例えば、以下のように試してください。

(load "test")

; `defvar`は変数を定義します。`#(`はベクトルを表すリーダーマクロです。
(defvar v1 #(1 2 3))

(svref v1 1)
; => 2

[ v1 1 ]
; => 2

svrefを使ったコードと[を使ったコードが同義になっています。

なお、応用ですが、このリーダーマクロはread-delimited-listを併用することにより、[]の中の空白が不要になります。このページ最下部の参考: read-delimited-listを参照してください。

2文字のディスパッチマクロ(set-dispatch-macro-character)

リーダーマクロが1文字のものしか使えなければ、記号は限られているため拡張機能は乏しくなってしまいます。そこで、2文字を組み合わせたディスパッチリーダーマクロを使うこともできます。

前節の#(1 2 3)というのはベクトル(1次元配列)を作成する定数ですが、このとき#(がディスパッチマクロとして登録されています。同じように、1文字目が#で2文字目を自由に定義して新しいディスパッチマクロを作ってみます(1)。

Common Lispにおいて「文字列」は真の文字列オブジェクトであり、C言語のような数(バイト)の配列ではありませんが、文字列をバイト列として扱った方が便利な時もあります。文字列とバイト列を相互に変換する関数はANSI Common Lispでは定められておらず、各処理系が独自に拡張を行なっているため、実際のプログラミングでは Babel (2)というライブラリを用いて処理系の違いを吸収して扱うことが一般的です。

この解説はANSI Common Lispの標準仕様の解説が目的のため、今回は変換の関数の詳細には立ち入らず、Clozure CLによる例のみを示します。具体的には#v"abc"のように記述すると文字列ではなくバイト列を返すようなディスパッチリーダーマクロ#vを作成します。

(defun byte-string-reader (stream c1 c2)
  "Reader Macro: Convert string to byte-string. Clozure CL only
Example: #v\"abc\" => #(97 98 99)"
  (declare (ignore c1 c2))
  (let ((string (read stream t nil t)))
    (list 'ccl:encode-string-to-octets string)))

(set-dispatch-macro-character ## #\v #'byte-string-reader)

こちらも同じようにloadすると、以下のように扱うことができます。

"abc"
; => "abc"

#v"abc"
; => #(97 98 99)

#v"あいうえお"
; => #(227 129 130 227 129 132 227 129 134 227 129 136 227 129 138)

もう一つ、お遊び系のリーダーマクロを作ってみます。

Common Lispは関数を常に前に記述する「前置記法」が採用されているため、例えば1 + 2(+ 1 2)のように書くことになります。これは要素がたくさんある時には便利なのですが、一般的に数学的な計算を行う際は見にくくなることがあると思います。

そこで、1つ目と2つ目の要素を入れ替えるようなリーダーマクロを作ってみます。

(defun infix-reader (stream c1 c2)
  "Reader Macro: Switching between FIRST and SECOND for infix notation.
Example: #@(1 + 2) => (+ 1 2)"
  (declare (ignore c1 c2))
  (let ((pair (read stream t nil t)))
    (list (second pair) (first pair) (third pair))))

(set-dispatch-macro-character ## #\@ #'infix-reader)

これも同じようにloadすると、以下のように使うことができます。

#@(1 + 2)
; => 3

'#@(1 + 2)
; => (+ 1 2)

#@(#@(1 + 2) * 3)
; => 9

'#@(#@(1 + 2) * 3)
; => (* (+ 1 2) 3)

このようにリーダーマクロはCommon Lispのソースコード解析そのものを拡張することができます。

参考: read-delimited-list

read-delimited-list関数は指定された文字までを一連のリスト(S式)として読み取ります。事前に])と同じ機能として登録しておくことで、リストの終端を示すことができます。

svrefを示すリーダーマクロとして定義したものは、以下のように書き換えることができ、もし使うならこちらの方が推奨されます。(空白の挿入忘れによるエラーがないため)

(set-macro-character #] (get-macro-character #\)))
(defun svref-reader (stream char)
  "Set the character '[' to `svref` Function.
(Example: [simple-vector index] = (sref vector1 1))"
  (declare (ignore char))
  (let ((pair (read-delimited-list #] stream t)))
    (list 'svref (first pair) (second pair))))
(set-macro-character #[ #'svref-reader)

機能としては同じです。


  1. 1文字目は必ずしも#である必要はありませんが、#はディスパッチマクロの1文字目として使われることがすでに予約されているため、誤作動につながりにくくなります。
  2. Babel = https://common-lisp.net/project/babel/. Babelを使用する場合はbabel:string-to-octets関数を使用します。

Copyright (c) 2017-, satoshiweb.net. All rights reserved.