この記事は, 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 ↩