プログラミング
Macの物語:〜だからこうなった物語
The Story of Mac: A Just-So Story (gigamonkeys.com)
要約
Lispのマクロは、コードを生成するコードとして機能します。この記事では、「Mac」という架空のプログラマーの物語を通して、マクロがどのようにしてコード生成を自動化し、プログラマーの負担を軽減したかを解説します。また、マクロの実行時(マクロ展開時)と通常のコードの実行時(ランタイム)の違いについても説明しています。
全文翻訳
Copyright © 2003-2005, Peter Seibel
8. マクロ:独自のものを定義する
さて、独自のカスタムマクロを作成する時間です。前の章で触れた標準マクロは、マクロで何ができるかの一部を示唆していますが、それは始まりに過ぎません。Common Lispがマクロをサポートしているのは、すべてのLispプログラマーが標準制御構造の独自のバリアントを作成できるからであり、Cが関数をサポートしているからといって、すべてのCプログラマーがC標準ライブラリの単純なバリアントを記述できるわけではありません。マクロは、コア言語と標準ライブラリの上に抽象化を作成し、あなたが表現したいことを直接表現できるようにするためのものです。
マクロの適切な理解における最大の障壁は、皮肉なことに、それらが言語に非常にうまく統合されていることです。多くの点で、それらは単なる奇妙な種類の関数のように見えます――Lispで書かれ、引数を取り、結果を返し、そして気を散らす詳細を抽象化することを可能にします。しかし、これらの多くの類似点にもかかわらず、マクロは関数とは異なるレベルで動作し、まったく異なる種類の抽象化を作成します。
マクロと関数の違いを理解すれば、言語におけるマクロの緊密な統合は大きな利点となります。しかし、それまでは、新しいLisperにとって頻繁な混乱の原因となっています。次の物語は、歴史的または技術的な意味で真実ではありませんが、マクロがどのように機能するかを考える方法を提供することで、混乱を軽減しようとしています。
Macの物語:〜だからこうなった物語
昔々、遠い昔、Lispプログラマーの会社がありました。実際、それは非常に昔のことだったので、Lispにはマクロがありませんでした。関数で定義できないものや特殊演算子で行えないものは、毎回すべて記述する必要があり、それはかなり面倒でした。残念ながら、この会社のプログラマーは――優秀でしたが――非常に怠惰でもありました。しばしば、プログラムの途中で――大量のコードを書くという退屈さが耐えられなくなると――代わりに、そこに書く必要のあるコードを説明するメモを書きました。さらに残念なことに、彼らは怠惰だったので、メモで説明されたコードを実際に書くことを嫌いました。すぐに、会社には、まだ書く必要のあるコードについてのメモでいっぱいだったため、誰も実行できないプログラムの大きな山ができました。
絶望した上司たちは、Macという名のジュニアプログラマーを雇いました。彼の仕事は、メモを見つけ、必要なコードを書き、プログラムのその場所に挿入することでした。Macはプログラムを実行しませんでした――もちろん、まだ完成していなかったので、できませんでした。しかし、たとえ完成していたとしても、Macはそれらにどのような入力を与えるべきかを知らなかったでしょう。そこで彼はメモの内容に基づいてコードを書き、元のプログラマーに送り返しました。
Macの助けにより、すべてのプログラムはすぐに完成し、会社はそれらを販売して莫大な利益を上げました――あまりにも多くの利益を上げたため、プログラミングスタッフを倍増させることができました。しかし、なぜか誰もMacを助ける人を雇おうとしませんでした。すぐに彼は一人で数十人のプログラマーを支援していました。ソースコードの中からメモを探すのにすべての時間を費やすのを避けるために、Macはプログラマーが使用していたコンパイラに小さな変更を加えました。それ以降、コンパイラがメモに遭遇するたびに、彼はそのメモを電子メールで受け取り、置き換えコードを電子メールで送り返すのを待ちました。残念ながら、この変更があっても、Macはプログラマーに追いつくのに苦労しました。彼はできる限り注意深く作業しましたが、時には――特にメモが不明瞭な場合――間違いを犯すこともありました。
しかし、プログラマーは、メモをより正確に書くほど、Macが正しいコードを送り返してくる可能性が高くなることに気づきました。ある日、言葉で欲しいコードを説明するのに苦労していたプログラマーの一人が、メモの中に、欲しいコードを生成するLispプログラムを含めました。それはMacにとって問題ありませんでした。彼はプログラムを実行し、結果をコンパイラに送りました。
次の革新は、あるプログラマーがプログラムの先頭に、関数定義と「Mac、ここではコードを書かないで、この関数は後で取っておいてください。他のメモで使います」というコメントを含むメモを置いたときに来ました。同じプログラムの他のメモには、「Mac、このメモを、シンボルxとyを引数としてその関数を実行した結果に置き換えてください」といったものがありました。
この手法は非常に速く広まり、数日後にはほとんどのプログラムに、他のメモのコードによってのみ使用される関数定義を含む数十個のメモが含まれるようになりました。Macが即時の応答を必要としない定義のみを含むメモを簡単に見つけられるようにするために、プログラマーは標準的な接頭辞を付けました:「Macのための定義、読み取り専用」。これは――プログラマーはまだ非常に怠惰だったので――すぐに「DEF. MAC. R/O」と短縮され、さらに「DEFMACRO」となりました。
すぐに、メモにはMacのための実際の英語は何も残っていませんでした。彼が一日中やっていたことは、コンパイラからのDEFMACROメモとDEFMACROで定義された関数への呼び出しを含む電子メールを読み、応答することだけでした。メモの中のLispプログラムがすべての実際の作業を行っていたので、電子メールに追いつくことは問題ありませんでした。Macは突然たくさんの時間を手に入れ、白い砂浜、澄んだ青い海、そして小さな紙の傘の付いた飲み物について空想してオフィスに座っていました。
数ヶ月後、プログラマーたちはMacをしばらく見かけないことに気づきました。彼のオフィスに行くと、そこには薄いほこりの層、熱帯の様々な場所への旅行パンフレットが散乱した机、そして電源の切れたコンピューターがありました。しかし、コンパイラはまだ動作していました――どうしてでしょうか?Macがコンパイラに最後の変更を加えたことが判明しました。コンパイラは、メモをMacに電子メールで送信する代わりに、DEFMACROメモによって定義された関数を保存し、他のメモで呼び出されたときにそれらを実行するようになりました。プログラマーたちは、Macがもうオフィスに来ないことを上司に伝える必要はないと判断しました。そのため、今日に至るまで、Macは給料を受け取り、時折、熱帯の様々な場所からプログラマーに絵葉書を送っています。
マクロ展開時間 対 実行時間
マクロを理解する鍵は、コードを生成するコード(マクロ)と、最終的にプログラムを構成するコード(それ以外すべて)との区別を非常に明確にすることです。マクロを書くとき、あなたはコンパイラがコードを生成するために使用するプログラムを書いており、そのコードがコンパイルされます。すべてのマクロが完全に展開され、結果のコードがコンパイルされた後でのみ、プログラムは実際に実行できます。マクロが実行される時間はマクロ展開時間と呼ばれます。これは、マクロによって生成されたコードを含む通常のコードが実行される実行時間とは区別されます。
この区別をしっかりと念頭に置くことが重要です。なぜなら、マクロ展開時に実行されるコードは、実行時に実行されるコードとは非常に異なる環境で実行されるからです。つまり、マクロ展開時には、実行時に存在するデータにアクセスする方法はありません。彼が取り組んでいたプログラムを実行できなかったMacのように、マクロ展開時に実行されるコードは、ソースコードに固有のデータのみを扱うことができます。たとえば、プログラムのどこかに次のソースコードが表示されると仮定します:
(defun foo (x) (when (> x 10) (print 'big)))
通常、xはfooへの呼び出しで渡される引数を保持する変数と考えるでしょう。しかし、マクロ展開時には、例えばコンパイラがWHENマクロを実行しているときなど、利用可能なデータはソースコードだけです。プログラムはまだ実行されていないため、fooへの呼び出しはなく、したがってxに関連付けられた値もありません。代わりに、コンパイラがWHENに渡す値は、ソースコードを表すLispリスト、すなわち(> x 10)と(print 'big)です。WHENが前の章で見たように、次のようなマクロで定義されていると仮定します:
(defmacro when (condition &rest body) `(if ,conditi