🦉

マクロにコンパイラマクロを書くお話。

2021/09/04に公開

マクロにコンパイラマクロを書くお話。

Meta notes.

対象読者

  • 「マクロにコンパイラマクロを書く」?意味わからん。という人。

Introduction.

マクロマクロと申しますがCommon Lispにマクロは複数ございます。
いわゆるマクロにシンボルマクロ、リーダーマクロ、そしてコンパイラマクロです。

本稿ではコンパイラマクロのみを取り上げ、中でもマクロにコンパイラマクロを書く事例について記します。

Compiler macro.

そもコンパイラマクロとはコンパイル時にのみ機能するマクロです。

通常は関数に対して定義されます。

具体例として+関数を考えてみましょう。

+関数は引数がない場合は0に評価されます。

引数がいくつあるかについては通常コンパイル時に判明します。
なら、コンパイル時に引数がないのが判明している場合0に畳み込んでおけば+を呼び出す実行コストを減らせます。

同様に引数が一つの場合、その数に評価されます。(e.g. (+ 3) => 3
なら、同様にコンパイル時に引数が一つであると判明したなら引数自身に置き換えれば+を呼び出す実行コストを削減できます。

このような最適化を行う場合利用されるのがコンパイラマクロです。

Why compiler macro rather than macro.

関数とマクロは並列に存在できません。

前節で記したような最適化を行うにはマクロの力が必要です。
しかしながら+を(関数ではなく)マクロにしてしまうと様々な弊害が起こります。

マクロは第一級オブジェクトではないため高階関数に渡したり返したりできません。

そこで+を関数として保持したままマクロの力を得る手段として定義されたのがコンパイラマクロです。

先にも記しましたが、コンパイラマクロはコンパイル時にのみ機能するマクロです。

コンパイラマクロのラムダリスト内では&ENVIRONMENTラムダリストキーワードを使用できます。
これは環境への問い合わせができることを意味します。

環境への問い合わせでは

  • 定数であるか否かの確認。
  • 変数の型の確認。
  • 関数、マクロ、特殊形式の評価後の型の確認。

などが可能です。

これらの情報を利用することでより効率的なコードを生成できます。

Use case for compiler macro for macro.

通常マクロにコンパイラマクロを書く必要はありません。
というのもコンパイラマクロにできることはマクロにもできますから。

それでも筆者はマクロにコンパイラマクロを書くという選択をする場合があります。

具体的には処理を切り分けたいときです。

ここでいう処理は「コード生成」と「最適化(主に枝刈り)」です。

「コード生成」は通常のマクロで行います。

しかしながらもし展開フォームが条件分岐を含み、かつ引数フォームが定数であったり、引数フォームの型が推論できたり伝播する場合、けして到達しない枝が生成されえます。

(if nil
  (then)
  (else))

上のコードで(THEN)はけして呼ばれません。

このような場合、コードの枝刈りを行ったほうがメモリ効率の面でも実行効率の面でもよろしゅうございます。

以下のコードにしてしまうわけです。

(else)

筆者はこのような枝刈りをコンパイラマクロに任せることが多うございます。

上のような小さな例ではマクロ内で一緒にやってしまうことも多うございますが、生成されるフォームが大きい場合や、注意すべき最適化点が複数ある場合などはコンパイラマクロに切り出したほうが読みやすくなります。

Conclusion.

SBCLはとてもかしこいコンパイラなので、ときに「Deleting unreachable code」というノートを吐きます。

筆者は少々神経質なきらいがあるので、そのようなコンパイラノートは殲滅させています。

あなたがおおらかな人であるなら、単にコンパイラノートを無視したり、握りつぶしたり(Muffling)すればそれでいいかもしれません。

しかしながら、自身のコードに対する解像度を上げたいなら、コンパイラノートの殲滅は良い手段となりえます。
そのような場合、コンパイラマクロが良き相棒となることもあるでしょう。

Discussion