Play with macro


詳細
Lispのマクロは異常に強いと言える.私が接触したマクロは約3種類です.
  • C言語のマクロで、機能が最も弱いが最も多く使われているマクロとほぼ言える.非常に簡単な構文解析のみを行い、テキストの置換を行います.しかし実際にはこのような簡単なマクロはC/C++に多くの余分な能力をもたらしたが、これまで専門的な文献や教材の詳細な説明もなかったようで、経験豊富なプログラマーたちがソースコードを通じてマクロに関する知識を相互に伝え合い、多くの面で異なるコンパイラでの結果が異なる.だから、これまでマクロもその部分でよく使われてきた使い方だけが広く受け入れられ、使われています.実際、興味があればboost.を見に行ってもいいです.preprocessor、マクロは多くのことをすることができます.
  • 1つはC++のテンプレートで、私はこれをマクロと見なしています.テンプレートもコードを操作しているので、実行期間には存在しません.C++のテンプレートは元のマクロよりもずっと強く、文法分析が行われ、編特化などの特性で予想外の能力を持っています.私はC++テンプレートが最初にマクロに取って代わる初志があるかどうか分かりませんが、今から見れば全然この方向を歩いていないようです.テンプレートもマクロもそれぞれの用途があります.テンプレート+インライン関数+定数などの特性はマクロにある程度取って代わることができるが,マクロにはまだ生存する場所がある.テンプレートは自分の本当の方向を見つけたばかりのようで、今有名な「モデルプログラミング」(GP)に発展した.私もテンプレートについて理解し始めたばかりの状態ですが、テンプレートはまだ発展初期のようで、OOPのような技術ではなく、先に研究した理論があって、やっと実現しました.テンプレートは無心に柳を挿して陰になっているようです.テンプレートという特性を作ってから、突然元のテンプレートにはこのような使い方があることに気づきました.様々な特性を実現する方法が発掘され、様々な特性が奮い立っています!しかし、初心はそんなことをするためのものではないからでしょう.技術的にそれらのモデル技術を強く実現することができても、多くの気まずいところがあります.例えば、コンパイルエラーが発生すると、エラー情報は風馬牛に及ばず、コンパイル速度が遅すぎて、テンプレートの拡張後の結果を見るのが不便で、デバッグが不便になります.またテンプレートの様々な変態の使い方は、コンパイラを簡単に切ることもできます.そのため、テンプレート関連の技術はさらに発展してこそ、工業に広く投入されるのではないでしょうか.そうしないと、学院派の高級おもちゃとしてしか使えません.来年はC++0 xが出てくるので、とても楽しみです.
  • 最後はLispのマクロです.古いCマクロでもC++の新しいテンプレートでも、元の言語とは独立した別の言語であり、コンパイル期間で実行されます(前処理もコンパイル期間に含めると).Lispのマクロとそれらの最大の違いは、LispマクロとLisp自体が同じ言語であり、完全に同じであり、マクロがコンパイル期間中に実行されているだけである.Lispの強力なテーブル処理能力を加えることで、自分の好きなようにコントロールすることができます(Lisp言語自体はテーブルで構成されています).これにより、Lispのマクロは、テキスト置換ベースのマクロやより高度なテンプレートなどの技術とは完全に異なる独特の威力を持つことが多い.

  • マクロの一般的な用途の1つは、「新しい言語」を定義することです.Steve BourneはUnix Version 7のshellを書いたとき(有名なBourne Shell)マクロでC言語を「変えた」Algol-68:
    #define STRING char *
    #define IF if (
    #define THEN ) {
    #define ELSE } else {
    #define FI ;}
    #define WHILE while (
    #define DO ) {
    #define OD ;}
    #define INT int
    #define BEGIN {
    #define END }
    

    彼はコードを書きました
    INT compare(s1, s2)
        STRING s1;
        STRING s2;
    BEGIN
        WHILE *s1++ == *s2
        DO IF *s2++ == 0
            THEN return (0);
            FI
        OD
            return(*--s1 - *s2);
    END
    

    もちろん彼のこのようなやり方は多くの人に抗議され、Bourne Shellのコードも維持しにくい模範とされてきた.ここでこの例を挙げるのは、マクロを使って言語を別の形にするのはよくないことを説明するためではありません.実際、syntax sugarまで、DSL(Domain Specific Language)まで、マクロのこのような特性をあちこちで使用する必要があります.Lispのこのような強大なマクロは事をもっと普遍的にして、事実上、Common Lispの中で、1つの強大なloopマクロがあって、それはとても柔軟で、更に命令式の言語の風格に似ている(例えば私達の熟知しているforwhileおよびreturnなど)を採用して、Lispの本来のmapのスタイルではなく(そのため、保守的なLisperたちには受け入れられていない)、また、Lispプログラム(多くの人がLispの接頭辞式を拒否する理由として)を書くために接尾辞式を使うことができるマクロもあります.DSLの分野では、Lispも通常の他の言語のやり方とは異なり、Lisp言語自体に基づいてマクロとテーブル操作を通じてより高いレベルの言語アプリケーションを構築し、結果的にDSL自体はLispであり、より高いレベルにあるにすぎないが、すべての最下層のLispの機能を使用することができる.Paul GrahamはProgramming Bottom-Upで述べた.
    Lispのマクロを書くときは、通常、期待していた本来の姿と期待していた結果の姿を書くだけで、残りの作業が容易になります.次に、Elispで作ったばかりのマクロを例に簡単に紹介します.前のブログでsmart-snippetを紹介しましたが、各modeにsnippetを簡単に定義できます.しかしながら、多くの場合、いくつかの異なるmodeは、同じsnippetを定義することができ、例えば、c-modec++-modeおよびjava-modeは、同じsnippetを使用することができる.しかしながら、従来のsnippetコードは、いくつかの異なるmodeに対して同じsnippetおよびkey−bindingを容易に定義することができない.この機能を実現することを考えて、定義するときにこのように書くことができることを望んでいます.
    (smart-snippet-with-abbrev-tables
     (java-mode-abbrev-table
      c++-mode-abbrev-table
      c-mode-abbrev-table)
     
     ("if" "if ($${condition})n{$>n$>$.n}$>" 'bol?)
     ("else" "elsen{$>n$>$.n}$>" 'bol?))
    

    2つのsnippetをそれぞれ3つのmodeに定義する機能を実現し、手書きで書くと次のようになります.
    (progn
      (smart-snippet-abbrev
       'java-mode-abbrev-table
       "if"
       "if ($${condition})n{$>n$>$.n}$>"
       'bol?)
     
      (smart-snippet-abbrev
       'java-mode-abbrev-table
       "else"
       "elsen{$>n$>$.n}$>"
       'bol?)
      
      (smart-snippet-abbrev
       'c++-mode-abbrev-table
       "if"
       "if ($${condition})n{$>n$>$.n}$>"
       'bol?)
      
      (smart-snippet-abbrev
       'c++-mode-abbrev-table
       "else"
       "elsen{$>n$>$.n}$>"
       'bol?)
      
      (smart-snippet-abbrev
       'c-mode-abbrev-table
       "if"
       "if ($${condition})n{$>n$>$.n}$>"
       'bol?)
      
      (smart-snippet-abbrev
       'c-mode-abbrev-table
       "else"
       "elsen{$>n$>$.n}$>"
       'bol?))
    

    まず、最初のパラメータは表で、後ろのパラメータとデカルト積をする感じに似ています.これは二重ネストされたループが必要で、問題はありません.loopマクロを使用して完成することができます.おそらくこのようになります.
    (defun double-loop (list1 &rest list2)
      (loop for i in list1
            collect (loop for j in list2
                          collect (list 'func i j))))
    
    (double-loop '(1 2 3) 4 5)を呼び出すと、次のような結果が得られます.
    (((func 1 4)
      (func 1 5))
     ((func 2 4)
      (func 2 5))
     ((func 3 4)
      (func 3 5)))
    
    (func 1 4)という構造が得られ,少し変更すれば単一のsnippetを定義するのに利用できる.しかし、ここでは私たちが望んでいるわけではありません.ネストされたループですが、最後のテーブル(つまりfuncの関数呼び出し)が同じレベルにある必要があります.余分なテーブル構造を削除するには、flatten-1が必要です.
    (defun flatten-1 (list)
      (cond ((atom list) list)
      ((listp (car list))
       (append (car list)
         (flatten-1 (cdr list))))
      (t (append (list (car list))
           (flatten-1 (cdr list))))))
    
    (flatten-1 (double-loop '(1 2 3) 4 5))を呼び出すと、希望の結果が得られます.
    ((func 1 4)
     (func 1 5)
     (func 2 4)
     (func 2 5)
     (func 3 4)
     (func 3 5))
    

    マクロとして書くことができます.
    (defmacro smart-snippet-with-abbrev-tables
      (abbrev-tables &rest snippets)
      `(progn
         ,@(smart-snippet-flatten-1
      (loop for table in abbrev-tables
            collect (loop for snippet in snippets
              collect (append
                 (list
                  'smart-snippet-abbrev
                  table)
                 snippet))))))
    

    次に、M-x pp-eval-last-sexpを使用して次のコードを実行する効果を見ることができます.
    (macroexpand '(smart-snippet-with-abbrev-tables
     (java-mode-abbrev-table
      c++-mode-abbrev-table
      c-mode-abbrev-table)
    
     ("if" "if ($${condition})n{$>n$>$.n}$>" 'bol?)
     ("else" "elsen{$>n$>$.n}$>" 'bol?)))
    

    次のような結果が得られます.
    (progn
      (smart-snippet-abbrev
       java-mode-abbrev-table
       "if"
       "if ($${condition})n{$>n$>$.n}$>"
       'bol?)
     
      (smart-snippet-abbrev
       java-mode-abbrev-table
       "else"
       "elsen{$>n$>$.n}$>"
       'bol?)
     
      (smart-snippet-abbrev
       c++-mode-abbrev-table
       "if"
       "if ($${condition})n{$>n$>$.n}$>"
       'bol?)
     
      (smart-snippet-abbrev
       c++-mode-abbrev-table
       "else"
       "elsen{$>n$>$.n}$>"
       'bol?)
     
      (smart-snippet-abbrev
       c-mode-abbrev-table
       "if"
       "if ($${condition})n{$>n$>$.n}$>"
       'bol?)
     
      (smart-snippet-abbrev
       c-mode-abbrev-table
       "else"
       "elsen{$>n$>$.n}$>"
       'bol?))
    

    結果に近いが、唯一の不足は、'c-mode-abbrev-tableがないのではなく、quoteを使用するべきだということだ.これも簡単です.最初のパラメータとして、すべてのabbrev-tableを含むテーブルの各要素にquoteを加える関数を書きます.
    (defun smart-snippet-quote-element (list)
      (loop for item in list
      collect (list 'quote item)))
    

    最後にこの関数を加えると、完全版smart-snippet-with-abbrev-tablesマクロが得られます.
    (defmacro smart-snippet-with-abbrev-tables
      (abbrev-tables &rest snippets)
      (let ((tables (smart-snippet-quote-element abbrev-tables)))
        `(progn
           ,@(smart-snippet-flatten-1
        (loop for table in tables
        collect (loop for snippet in snippets
                collect (append
                   (list
              'smart-snippet-abbrev
              table)
                   snippet)))))))
    

    同じツール関数を使って、smart-snippet-with-keymapマクロを作りました.そして、それらの構造は似ています.私はこの構造を抽象化して、新しいマクロを定義することができます.例えば、smart-snippet-def-withと呼ばれ、このマクロを使用してwith-abbrev-tableマクロとwith-keymapマクロを定義します.コードの繰返し度合いを最小限に抑えるために使用されます(実際には、def-xxxというマクロはElispで非常に一般的です).
    もちろん、マクロの使い方はこれに限られません.Paul Grahamは彼の『On Lisp』という本でLisp Macroのテクニックをたくさん説明しています.興味があれば、探して読んでみてください.