簡単なマクロの紹介

この記事は, [Lisp Advent Calendar 2019](https://qiita.com/advent-calendar/2019/lisp)の10日目の記事です.

この記事のライセンスは[CC-BY](https://creativecommons.org/licenses/by/4.0/deed.ja)とします.

この記事は, 極簡単なマクロを書いてみる記事です. 使用プログラミング言語は, Common Lispです.

## lispのマクロ

Lisp方言の特徴として, マクロというものについて話題に登ることがあると思います.

2015年のCommon Lispのマクロについての記事[^keen]には,

> 最近ではマクロシステムを持つ言語は珍しくない。Rust, Scala, Template Haskell, Mirahなどなど。

とあるので, もしかしたらlisper以外の人の中で(lispの)マクロに興味を持っている人も居るかもしれません. [^lisp-macro]

その一方で, 例えば私が仕事で使っているPHPやJavaScriptにはlispのようなマクロが無いので,
「lisperはマクロと言うが, それがどんなものなのか全く知らない」という人もいると思います.

この記事では, 極簡単な3つのマクロを書いてみることでCommon Lispのマクロをほんの少し紹介したいと思います.

[^keen]: [マクロについて整理してみる κeenのHappy Hacκing Blog](https://keens.github.io/blog/2015/07/04/makuronitsuiteseirishitemiru/)
[^lisp-macro]: 他の言語のマクロシステムを知りたい人が居るのではないかという意味.

## 関数では書けない

前提として, マクロを使わなくても問題ない場合にはマクロではなくて関数で書くのが概ね良いスタイルであるとされています.[^function-macro]

とはいえ, ある種のものを書く場合にマクロが必要になる場合があります. その一つが`if`の上にものを作りたいような場合です.

```common-lisp:example.lisp
(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`は引数に与えられた変数の値をインクリメントして書き換える関数です.

```common-lisp:repl
CL-USER> (hoge t)
(2 1)
```

```common-lisp:repl
CL-USER> (hoge nil)
(1 2)
```

さて, `if`とは違いcondが`nil`の場合に第二引数の式が評価されて, condが`nil`でない場合に第三引数の式が評価される`if-not`を定義したいと思います.

関数で定義しようとして以下のように書くとうまく動作しません.

```common-lisp:machigai.lisp
(defun if-not-!? (cond then else)
  (if cond
      else
      then))
```

これを使って最初のhogeを書き直すと,

```common-lisp:example.lisp
(defun hoge-!? (cond)
  (let ((a 1)
        (b 1))
    (if-not-!? cond
               (incf b)
               (incf a))
    (list a b)))
```

```common-lisp:repl
CL-USER> (hoge-!? t)
(2 2)
```

```common-lisp:repl
CL-USER> (hoge-!? nil)
(2 2)
```

となり期待した動作にはなりません. `if-not-!?`の呼び出しの時点で,`then`も`else`も評価されてします為です.

この様な時にマクロを用います.

[^function-macro]: よいリファレンスが見つからなかったので, コメントで補足してくれると嬉しいです. 最近, https://www.cs.umd.edu/~nau/cmsc421/norvig-lisp-style.pdf このドキュメントを紹介してもらって読んで居たのですが, ちょっと古いかな.

## if-not

マクロが最終的に評価されるまでの動作を簡単に書くと,

1. 評価される式をマクロによって作成する. (マクロを展開するという.)
2. 展開された式を評価する.

マクロの定義では, このマクロがどの様な式に展開されるかということを書きます.
つまり, マクロの返り値(展開形と言う方が良いかもしれません)はCommon Lispの式になります.

マクロはコンパイルする段階で展開されるように出来ているので, マクロを展開する段階では実行時[^running]の情報を扱うことが出来ません.
そのため, マクロの引数もCommon Lispの式です.

elseとthenを入れ替えると良いので以下のように書けます.

```common-lisp:if-not.lisp
(defmacro if-not (cond then else)
  (list 'if cond else then))
```

`defmacro`でマクロを定義します.
ここで, `cond`, `then`, `else`はマクロ展開時に評価されたときには, それぞれマクロに与えられた引数つまりCommon Lispの式を返します.
その式を評価した値になるわけでは無いことに注意してください.

あるいは準クォート`` ` ``を使うと,

```common-lisp:if-not.lisp
(defmacro if-not (cond then else)
  `(if ,cond
       ,else
       ,then))
```

とも書けます. こちらのほうが展開系に近い形で書けますね.
バッククォート(準クォート)は`` ` ``, ここでは通常のクォートとほぼ同じ様に働きますが, 準クォートされた式中のカンマ`,`が直前にある式は評価するという動作になります.
通常のクォートは, クォートのついた式を評価せずにそのまま返します.

この`if-not`マクロを使って先程のうまく行かなかった例を書き直すと

```common-lisp:example.lisp
(defun hoge (cond)
  (let ((a 1)
        (b 1))
    (if-not cond
       (incf b)
       (incf a))
    (list a b)))
```

```common-lisp:repl
CL-USER> (hoge t)
(2 1)
```

```common-lisp:repl
CL-USER> (hoge nil)
(1 2)
```

と期待通りに動作します.

[^running]: マクロ展開時ではなくて, 実行時.

## and

`if`と同じ様に, 与えられた引数を関数のように先に評価したくないものとして`and`と`or`があります.

他の言語[^short-circuit]でもそうかと思いますが, これらは短絡評価をするために評価されない引数があります.

```common-lisp:repl
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`として定義してみます. [^sbcl]

```common-lisp:my-and.lisp
(defmacro my-and (&rest forms)
  (reduce (lambda (acc elt) (list 'if acc elt)) forms :initial-value t))
```

こんな風に簡単に書けますね. これが実際に動くことは皆さんお手元のreplで試してみてください. [^jscl-repl-browser]

Common LispのマクロはCommon Lispの式を受け取ってCommon Lispの式を返すと先に書きました.

Common Lispの式はただのリスト(あるいはシンボル)なので, Common Lispのプログラムの中で扱うデータ構造そのものです.

また, マクロ展開時にCommon Lispの機能をフルに使えるので普段リスト操作をするのと全く同様に上の用にコードが書けるわけですね.

[^short-circuit]: 例えば, JavaScriptの`&&`,`||`とか, PHPの`&&`, `||`, `and`, `or`等.
[^sbcl]: 各処理系の実装と同じかどうかは確認していませんので, ご了承ください.  少なくとも日本時間の2019年12月8日10時27分のSBCLのgithubのmasterブランチとは違った.
[^jscl-repl-browser]: https://jscl-project.github.io/ こういうのもあります.

## or

`or`の動作は以下のとおりです.

- 引数が与えられない時は`nil`を返します.
- 引数が与えられたら, 左から順番に評価して`nil`ではない値となったところで評価をやめて, それ以降の引数を評価せずにその評価結果を返します.
- そうでなければ, 最後の引数を評価した結果を返します.

`and`と同じ様にreduceを使いますが, 順序が逆になりますので`:from-end`に`t`を指定することと与える関数の引数の順序に気を付けましょう.

基本となるパーツは, 「一回だけ評価して結果が`nil`でなければその値を返す」なので,

```common-lisp:pseudo-code
(let ((x elt))
  (if x
      x
      (or-rest))) ; 最後のところで残りを再帰
```

評価結果を`let`を使って一度変数に退避させるという処理になるので, この様な形になることが分かります.

これをそのまま使って`reduce`を作ると以下のようになります.

```common-lisp:bad-my-or.lisp
(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`を使った式を渡した時に不具合がおきます.

特に副作用のある場合などで問題で,

```common-lisp:repl
CL-USER> (defvar x 0)
X
CL-USER> (my-or-?! nil (incf x))
;; エラーとなる
```
この様なことをすると分かりますが, 期待したどおりに動きません.

この様な`let`はマクロによく登場しますが, (ここでの`x`のような)普通のシンボルを使ってしまうと, その名前のことをマクロを使うユーザが知らなければうまく使えないという問題があります.

これでは不便ですね.

実は, Common Lispには`gensym`という関数があって, 他のシンボルと被らない(同じにならない)シンボルを生成することが出来ます.

これを用いると,

```common-lisp:my-or.lisp
(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`で書かれた関数の返り値[^lambda]となる第一要素が`let`となるリスト(準クォートされている部分)のなかで,
カンマで評価された`x`は, その一つ上の`let`で`(gensym)`で生成されたシンボルとなります.

これが`or`と同じ動きをすることは, お手元のREPL等でご確認ください.


[^lambda]: lambdaの返り値ではない.

## まとめ

`if-not`, `and`, `or`というマクロを実装してみることで, Common Lispのマクロで何が出来るのかということを駆け足で少しだけ紹介しました.

## 補足 andの展開形

各処理系の実装は参考にしてないけど, 展開系は参考にしました. [^sbcl-and]

```common-lisp:repl
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)`だと思うと,

```common-lisp:expanded
(IF (IF (IF T A) B) C)
```

この形を見ると, `reduce`で処理できることが分かります.

[^sbcl-and]: SBCL 1.5.9