この記事は, Lisp Advent Calendar 2019の10日目の記事です.
この記事のライセンスはCC-BYとします.
この記事は, 極簡単なマクロを書いてみる記事です. 使用プログラミング言語は, Common Lispです.
lispのマクロ
Lisp方言の特徴として, マクロというものについて話題に登ることがあると思います.
2015年のCommon Lispのマクロについての記事1には,
最近ではマクロシステムを持つ言語は珍しくない。Rust, Scala, Template Haskell, Mirahなどなど。
とあるので, もしかしたらlisper以外の人の中で(lispの)マクロに興味を持っている人も居るかもしれません. 2
その一方で, 例えば私が仕事で使っているPHPやJavaScriptにはlispのようなマクロが無いので,
「lisperはマクロと言うが, それがどんなものなのか全く知らない」という人もいると思います.
この記事では, 極簡単な3つのマクロを書いてみることでCommon Lispのマクロをほんの少し紹介したいと思います.
関数では書けない
前提として, マクロを使わなくても問題ない場合にはマクロではなくて関数で書くのが概ね良いスタイルであるとされています.3
とはいえ, ある種のものを書く場合にマクロが必要になる場合があります. その一つがif
の上にものを作りたいような場合です.
(defun hoge (cond)
(let ((a 1)
(b 1))
(if cond
(incf a)
(incf b))
(list a b)))
if
は特殊形式と呼ばれるもので, 上の例では, condがnil
でないなら(incf a)
が, condがnil
なら(incf b)
が評価されます. それぞれの場合にもう一方は評価されません.
incf
は引数に与えられた変数の値をインクリメントして書き換える関数です.
CL-USER> (hoge t)
(2 1)
CL-USER> (hoge nil)
(1 2)
さて, if
とは違いcondがnil
の場合に第二引数の式が評価されて, condがnil
でない場合に第三引数の式が評価されるif-not
を定義したいと思います.
関数で定義しようとして以下のように書くとうまく動作しません.
(defun if-not-!? (cond then else)
(if cond
else
then))
これを使って最初のhogeを書き直すと,
(defun hoge-!? (cond)
(let ((a 1)
(b 1))
(if-not-!? cond
(incf b)
(incf a))
(list a b)))
CL-USER> (hoge-!? t)
(2 2)
CL-USER> (hoge-!? nil)
(2 2)
となり期待した動作にはなりません. if-not-!?
の呼び出しの時点で,then
もelse
も評価されてします為です.
この様な時にマクロを用います.
if-not
マクロが最終的に評価されるまでの動作を簡単に書くと,
- 評価される式をマクロによって作成する. (マクロを展開するという.)
- 展開された式を評価する.
マクロの定義では, このマクロがどの様な式に展開されるかということを書きます.
つまり, マクロの返り値(展開形と言う方が良いかもしれません)はCommon Lispの式になります.
マクロはコンパイルする段階で展開されるように出来ているので, マクロを展開する段階では実行時4の情報を扱うことが出来ません.
そのため, マクロの引数もCommon Lispの式です.
elseとthenを入れ替えると良いので以下のように書けます.
(defmacro if-not (cond then else)
(list 'if cond else then))
defmacro
でマクロを定義します.
ここで, cond
, then
, else
はマクロ展開時に評価されたときには, それぞれマクロに与えられた引数つまりCommon Lispの式を返します.
その式を評価した値になるわけでは無いことに注意してください.
あるいは準クォート`
を使うと,
(defmacro if-not (cond then else)
`(if ,cond
,else
,then))
とも書けます. こちらのほうが展開系に近い形で書けますね.
バッククォート(準クォート)は`
, ここでは通常のクォートとほぼ同じ様に働きますが, 準クォートされた式中のカンマ,
が直前にある式は評価するという動作になります.
通常のクォートは, クォートのついた式を評価せずにそのまま返します.
このif-not
マクロを使って先程のうまく行かなかった例を書き直すと
(defun hoge (cond)
(let ((a 1)
(b 1))
(if-not cond
(incf b)
(incf a))
(list a b)))
CL-USER> (hoge t)
(2 1)
CL-USER> (hoge nil)
(1 2)
と期待通りに動作します.
and
if
と同じ様に, 与えられた引数を関数のように先に評価したくないものとしてand
とor
があります.
他の言語5でもそうかと思いますが, これらは短絡評価をするために評価されない引数があります.
CL-USER> (defvar a 0)
A
CL-USER> a
0
CL-USER> (or t (incf a))
T
CL-USER> a
0
と言った感じです.
そのため, この様なものを自分で定義したいときにもマクロとして定義します.
and
の動作は,
- 引数が与えられない時は
t
を返します. - 引数が与えられたら, 左から順番に評価して
nil
となったところで評価をやめて, それ以降の引数を評価せずにnil
を返します. - そうでなければ, 最後の引数を評価した結果を返します.
これをmy-and
として定義してみます. 6
(defmacro my-and (&rest forms)
(reduce (lambda (acc elt) (list 'if acc elt)) forms :initial-value t))
こんな風に簡単に書けますね. これが実際に動くことは皆さんお手元のreplで試してみてください. 7
Common LispのマクロはCommon Lispの式を受け取ってCommon Lispの式を返すと先に書きました.
Common Lispの式はただのリスト(あるいはシンボル)なので, Common Lispのプログラムの中で扱うデータ構造そのものです.
また, マクロ展開時にCommon Lispの機能をフルに使えるので普段リスト操作をするのと全く同様に上の用にコードが書けるわけですね.
or
or
の動作は以下のとおりです.
- 引数が与えられない時は
nil
を返します. - 引数が与えられたら, 左から順番に評価して
nil
ではない値となったところで評価をやめて, それ以降の引数を評価せずにその評価結果を返します. - そうでなければ, 最後の引数を評価した結果を返します.
and
と同じ様にreduceを使いますが, 順序が逆になりますので:from-end
にt
を指定することと与える関数の引数の順序に気を付けましょう.
基本となるパーツは, 「一回だけ評価して結果がnil
でなければその値を返す」なので,
(let ((x elt))
(if x
x
(or-rest))) ; 最後のところで残りを再帰
評価結果をlet
を使って一度変数に退避させるという処理になるので, この様な形になることが分かります.
これをそのまま使ってreduce
を作ると以下のようになります.
(defmacro my-or-?! (&rest forms)
(reduce (lambda (elt acc)
(let ((x ,elt))
(if x
x
,acc)))
forms :initial-value nil :from-end t))
しかしこれではmy-or
の引数にx
を使った式を渡した時に不具合がおきます.
特に副作用のある場合などで問題で,
CL-USER> (defvar x 0)
X
CL-USER> (my-or-?! nil (incf x))
;; エラーとなる
この様なことをすると分かりますが, 期待したどおりに動きません.
この様なlet
はマクロによく登場しますが, (ここでのx
のような)普通のシンボルを使ってしまうと, その名前のことをマクロを使うユーザが知らなければうまく使えないという問題があります.
これでは不便ですね.
実は, Common Lispにはgensym
という関数があって, 他のシンボルと被らない(同じにならない)シンボルを生成することが出来ます.
これを用いると,
(defmacro my-or (&rest forms)
(reduce (lambda (elt acc)
(let ((x (gensym)))
`(let ((,x ,elt))
(if ,x
,x
,acc))))
forms :initial-value nil :from-end t))
最初のlet
は, 次のlet
の中でマクロ展開時に評価されるx
を(gensym)
で生成されるシンボルに束縛します.
言い換えると, このlambda
で書かれた関数の返り値8となる第一要素がlet
となるリスト(準クォートされている部分)のなかで,
カンマで評価されたx
は, その一つ上のlet
で(gensym)
で生成されたシンボルとなります.
これがor
と同じ動きをすることは, お手元のREPL等でご確認ください.
まとめ
if-not
, and
, or
というマクロを実装してみることで, Common Lispのマクロで何が出来るのかということを駆け足で少しだけ紹介しました.
補足 andの展開形
各処理系の実装は参考にしてないけど, 展開系は参考にしました. 9
CL-USER> (macroexpand '(and))
T
T
CL-USER> (macroexpand '(and a))
(THE T A)
T
CL-USER> (macroexpand '(and a b c))
(IF (IF A
B)
C)
T
THE
は無視して, A
は, (IF T A)
だと思うと,
(IF (IF (IF T A) B) C)
この形を見ると, reduce
で処理できることが分かります.
-
他の言語のマクロシステムを知りたい人が居るのではないかという意味. ↩
-
よいリファレンスが見つからなかったので, コメントで補足してくれると嬉しいです. 最近, https://www.cs.umd.edu/~nau/cmsc421/norvig-lisp-style.pdf このドキュメントを紹介してもらって読んで居たのですが, ちょっと古いかな. ↩
-
マクロ展開時ではなくて, 実行時. ↩
-
例えば, JavaScriptの
&&
,||
とか, PHPの&&
,||
,and
,or
等. ↩ -
各処理系の実装と同じかどうかは確認していませんので, ご了承ください. 少なくとも日本時間の2019年12月8日10時27分のSBCLのgithubのmasterブランチとは違った. ↩
-
https://jscl-project.github.io/ こういうのもあります. ↩
-
lambdaの返り値ではない. ↩
-
SBCL 1.5.9 ↩