目次


メタプログラミング技法 第2回:Schemeを使用したメタプログラミング

マクロ機能で大型プロジェクトを単純化するコードを生成する

Comments

メタプログラミング技法 第1回:メタプログラミングとは」では、

  • コード生成プログラムで解決するのに最適な問題を調べました。
    • データ・テーブルを事前に生成しておく必要のあるプログラム
    • 関数としてまとめることができないくらい多くのボイラープレート・コードが設定されているプログラム
    • 非常に冗長な技法を必要とする言語を使用しているプログラム
  • 次に、メタプログラミングの方式とその使用例を見ました。
    • 一般的なテキスト置換方式
    • 特定領域プログラムと関数ジェネレータ
  • 次に、テーブル作成に特化したインスタンスを確認しました。
  • また、Cで静的テーブルを作成するコード生成プログラムを書きました。
  • 最後に、Schemeを紹介し、Scheme言語そのものの一部である構造体を使用して、C言語で直面した問題をどのようにして解決できるかを見ました。

この記事では、Schemeマクロのプログラミング方法と、それによって大規模なプログラミング・タスクを大幅に容易にする方法を説明します。

Schemeでのsyntax-caseマクロの作成

syntax-caseマクロはSchemeの標準的な部分ではありませんが、最も広く使用されているタイプのマクロであり、ハイジニックな(hygienic、清潔な)形式と非ハイジニックな形式の両方が可能であり、標準的なsyntax-rulesマクロとも密接な関係があります。

syntax-caseマクロは、リスト1の形式に従います。

リスト1.syntax-caseマクロの一般的な形式
(define-syntax macro-name
   (lambda (x)
     (syntax-case x (other keywords go here if any)
       (
         ;;First Pattern
         (macro-name macro-arg1 macro-arg2)
         ;;Expansion of macro (one or multiple forms)
         ;;(syntax is a reserved word)
         (syntax (expansion of macro goes here))
       )
       (
         ;;Second Pattern -- a 1-argument version
         (macro-name macro-arg1)
         ;;Expansion of macro
         (syntax (expansion of macro goes here))
       )
 )))

この形式が行うのは、変換に使用されるキーワードとなるmacro-nameを定義することです。lambdaで定義される関数は、マクロ・トランスフォーマーが式xを拡張に変換するために使用する関数です。

syntax-caseは、最初の引数として式xをとります。2番目の引数は、構文パターン内で文字通り使用されるキーワードのリストです。パターンに使用されているその他の識別子は、テンプレート変数として使用されます。次に、syntax-caseは、一連のパターン/トランスフォーマーの組み合わせを取ります。ひとつずつ処理して、入力形式とパターンを照合し、一致した場合は、それに関連付けられた拡張を生成します。

単純な例を見てみましょう。Schemeにあるものよりも冗長なifステートメントを書くとします。また、2つの変数のうち大きい方を返すと仮定します。コードは次のようになります。

(if (> a b) a b)

Scheme以外のプログラマーにとっては、どれが「then」ブランチで、どれが「else」ブランチなのかを示すものは何もありません。これをわかりやすくするために、「then」と「else」キーワードを追加した独自のカスタムifステートメントを作成することができます。次のようになります。

(my-if (> a b) then a else b)

リスト2は、この操作を実行するマクロの例です。

リスト2.拡張ifステートメントを定義するマクロ
;;define my-if as a macro
(define-syntax my-if
  (lambda (x)
    ;;establish that "then" and "else" are keywords
    (syntax-case x (then else)
      (
        ;;pattern to match
        (my-if condition then yes-result else no-result)
        ;;transformer
        (syntax (if condition yes-result no-result))
       )
)))

このマクロを実行すると、my-if式と次のようなテンプレートを照合します(言い換えると、マクロ呼び出しとマクロ定義パターンを照合します)。

(my-if  (> a b)  then     a      else    b)
   |       |      |       |       |      |
   |       |      |       |       |      |
   v       v      v       v       v      v
(my-if condition then yes-result else no-result)

したがって、変換式にconditionが含まれていると、(> a b) に置き換えられます。(> a b) がリストであるかどうかは関係ありません。それを含んでいるリスト内の1つの要素であり、パターン内ではひとまとまりとして扱われます。結果の構文は、これらの各部分を新しい式に並べ替えただけです。

この変換は「実行前」の「マクロ展開時」と呼ばれるタイミングで行われます。多くのコンパイラー・ベースのScheme実装では、マクロ展開時はコンパイル時です。これは、マクロがプログラムの開始時またはコンパイル時に一度しか実行されず、けっして再評価されないことを意味します。したがって、my-ifステートメントが実行時のオーバーヘッドとなることはなく、実行時には単純なifに変換されます。

次の例では、有名なswap!マクロを実行します。これは、2つの識別子の値を交換する単純なマクロです。リスト3に、このマクロの使用例を示します。

リスト3.swap!マクロによる識別子の値の交換
(define a 1)
(define b 2)
(swap! a b)
(display "a is now ")(display a)(newline)
(display "b is now ")(display b)(newline)

この単純なマクロ(リスト4)は、新しい一時変数を導入することによって交換を実装します。

リスト4.swap!マクロの定義
;;Define a new macro
(define-syntax swap!
  (lambda (x)
    ;;we don't have any keywords this time
      (syntax-case x ()
        (
          (swap! a b)
          (syntax
            (let ((c a))
              (set! a b)
              (set! b c)))
        )
)))

ここではcという新しい変数が導入されています。しかし、交換される引数の1つがcという名前のときはどうなるのでしょうか。

syntax-caseは、マクロが展開されるときに、cをユニークな未使用の変数名と置き換えることによって、この問題を解決します。したがって、構文トランスフォーマーがこの問題をすべて処理します。

syntax-caseはletを置き換えないことに注意してください。letはグローバルに定義された識別子だからです。

導入された変数名を、競合しない名前に交換するというアイデアを「ハイジーン」(hygiene)といい、結果のマクロを「ハイジニック・マクロ」といいます。ハイジニック・マクロは、既存の変数名を台無しにする心配なく、どこでも安全に使用できます。多種多様なメタプログラミング・タスクに応じて、この機能はマクロを予測可能にし、扱いやすくします。

識別子の導入

ハイジニック・マクロはマクロ内での変数名の導入を安全にしますが、マクロを非ハイジニックにしたい場合もあります。たとえば、マクロを呼び出す人が使用できる変数をスコープに導入するマクロを作成するとします。これは非ハイジニック・マクロになります。マクロがユーザーのコードの名前空間を汚染するからです。ただし、この機能が便利なケースも少なくありません。

単純な例として、マクロ内で使用するいくつかの数学定数の定義を導入するマクロを書くとします(もちろん、これには他の方法を使用した方がよいのですが、ここでは単純な例として使用しています)。リスト5のようなマクロ呼び出しを使用して、piとeを定義するとします。

リスト5.数学定数マクロの呼び出し
(with-math-defines
	(* pi e))

これを以前のマクロのようにセットアップしようとすると、失敗します。

リスト6.動作しない数学定数マクロ
(define-syntax with-math-defines
  (lambda (x)
    (syntax-rules x ()
      (
        (with-math-defines expression)
        (syntax
          (let ( (pi 3.14) (e 2.71828) )
               expression))
      )
)))

この式は動作しません。理由は、すでに述べたように、Schemeは、囲んでいるスコープまたはネストされているスコープ内の他の名前と競合しないように、piとeの名前を変更するからです。したがって、新しい名前が付けられて、コード (* pi e) は未定義の変数を参照することになります。マクロを呼び出す開発者が使用できるリテラル・シンボルを導入する手段が必要です。

Schemeの自動ハイジーンによって変更されないコードをマクロに導入するためには、コードをシンボルのリストから、パターン変数に割り当てて、変換後の式に挿入できる構文オブジェクトに変換しなければなりません。これを実現するには、基本的にマクロの「let」ステートメントであるwith-syntaxを使用します。基本的な形式は同じですが、こちらは構文オブジェクトをテンプレート変数に代入するために使用されます。

新しいテンプレート変数を作成できるようにするには、シンボルと式を「リスト」表現(構文の作成方法)や、より抽象的な「構文オブジェクト」表現に変換したり、再変換して元に戻すことができなければなりません。以下の関数は、このような変換を行います。

  • datum->syntax-objectは、リストを、より抽象的な構文オブジェクト表現に変換します。
    • この関数の最初のパラメーターは、通常 (syntax k) であり、構文コンバーターによる構文の修正を容易にする、ちょっとした魔法の式です。
    • 2番目のパラメーターは、構文オブジェクトに変換する必要がある式です。
    • 結果は、with-syntaxでテンプレート変数に代入できる構文オブジェクトです。
  • syntax-object->datumは、datum->syntax-objectとは逆の処理です。これは、構文オブジェクトを取り、それをSchemeの通常のリスト処理関数で操作できる式に変換します。
  • syntaxは、テンプレート変数と定数式から成る変換式を取り、結果の構文オブジェクトを返します。

この例では、テンプレート変数にリテラル値を入れることが目的なので、syntaxとsyntax-object->datumを組み合わせて使用します。そして、式を操作し、datum->syntax-objectを使用して、with-syntaxでテンプレート変数に代入できる構文オブジェクトに戻します。その場合、最終的な変換式では、新しいテンプレート変数を他のものと同様に使用することができます。

実際には、Scheme構文を、操作できるリストに変換し、そのリストを操作した後、出力用のScheme構文式に戻します。

リスト7は、これらの関数を使用して数学シンボルを定義するマクロ定義を示しています。

リスト7.動作する数学定数マクロ
(define-syntax with-math-defines
  (lambda (x)
    (syntax-case x ()
      (
        ;;Pattern
        (with-math-defines expression)

        ;;with-syntax defines new pattern variables
        (with-syntax
          (
            (expr ;;the new pattern variable
              ;;convert expression into a syntax object
              (datum->syntax-object
                ;;syntax domain magic
                (syntax k)
                ;;expression to convert
                `(let ( (pi 3.14) (e 2.72))
                      ;;Insert the code for the "expression" template
                      ;;variable here.
                      ,(syntax-object->datum (syntax expression))))))
          ;;Use the newly-created "expr" pattern
          ;;variable as the resulting expression
          (syntax expr))
      )
)))

Schemeに慣れていない場合は、バッククォート(準引用といいます)はクォート演算子に似ていると考えてください。バッククォートでは、前にコンマ(アンクォート演算子といいます)を付けることによって、引用されないデータを含めることができます。これにより、式をボイラープレート・コードにつないで、全体を最終的な変換として構文オブジェクトに戻すことができます。

新しい変数を既存の構文オブジェクトに明示的につなぎ合わせたので、これらの名前が変更される可能性はありません。また、datum->syntax-object内の式 (syntax k) は必要ですが、本質的には無意味であることに注意してください。これは構文プロセッサーのちょっとした「魔法」を呼び出して、式を処理すべき文脈をdatum->syntax-object関数に知らせるために使用されます。常に、(syntax k) と書かれます。

非ハイジニック・マクロでの問題は、導入された変数がコード内の他の変数を上書きしたり、上書きされる場合があることです。このため、非ハイジニック・マクロの混在は特に危険です。マクロが他のマクロが使用している変数を認識せず、互いの変数を台無しにする恐れがあるからです。したがって、非ハイジニック・マクロは、通常の関数やハイジニック・マクロを使用して同じ効果を達成する手段がないときのみ使用すべきであり、使用する場合は、マクロのシンボル導入を入念に文書化すべきです。

ボイラープレート・マクロの構築

大きなアプリケーションで書かれているコードの多くは、書くのが煩雑なボイラープレート・コードです。ボイラープレート・コードにバグが見つかった場合、ボイラープレートが使用されている箇所をすべて見つけて、コードを書き直すのは非常に困難です。これは、ボイラープレート・コードが、非ハイジニック・マクロが役立つ数少ない事例の1つであることを意味します。

ボイラープレート・コードの大部分は、関数で使用される変数をセットアップするだけです。したがって、ボイラープレート・マクロでは、共通のバインディングのセットと、おそらくその他のハウスキーピング・タスクを導入すべきです。

たとえば、多くの独立したCGIスクリプトで構成されるCGIアプリケーションを構築すると仮定しましょう。ほとんどのCGIアプリケーションでは、状態の大部分はデータベースに格納されますが、セッションIDだけはクッキーを通じて各スクリプトに渡されます。

ただし、ほぼすべてのページで、その他の標準的な情報(ユーザー名、グループ番号、作業中の現在のジョブなど、その他の関連情報)を知る必要があります。さらに、ユーザーが適切なクッキーを持っていない場合は、ユーザーをリダイレクトする必要があります。リスト8は、標準的なボイラープレートとなるコードの例です(架空のWebサーバー関数の前にはwebserver:が付いています)。

リスト8.Webアプリケーションのボイラープレート・コード
            (define (handle-cgi-request req)
  (let (
        (session-id (webserver:cookie req "sessionid")))
    (if (not (webserver:valid-session-id session-id))
        (webserver:redirect-to-login-page)
        (let (
              (username (webserver:username-for-session session-id))
              (group (webserver:group-for-user username))
              (current-job (webserver:current-job-for-user username)))
          ;;Code for processing goes here
          ))))

このうちの一部はプロシージャーで処理できますが、バインディングはそうは行きません。ただし、そのほとんどをマクロにすることができます。マクロは、次のように実装できます。

リスト9.ボイラープレート・コードのマクロ
            (define-syntax cgi-boilerplate
  (lambda (x)
    (syntax-case x ()
      (
        (cgi-boilerplate expr)
        (datum->syntax-object
          (syntax k)
          `(let (
                 (session-id (webserver:cookie req "sessionid")))
                (if (not (webserver:valid-session-id session-id))
                    (webserver:redirect-to-login-page)
                    (let (
                          (username (webserver:username-for-session session-id))
                          (group (webserver:group-for-user username))
                          (current-job (webserver:current-job-for-user username)))
                        ,(syntax-object->datum (syntax expr))))))
      )
)))

これで、次のようにして、ボイラープレート・コードに基づいて新しいフォームを作成することができます。

(define (handle-cgi-request req)
  (cgi-boilerplate
   (begin
     ;;Do whatever I want here
     )))

また、変数を明示的に定義していないので、新しい変数定義をボイラープレートに追加しても呼び出し規則には影響せず、まったく新しい関数を作成しなくても、新しい機能を追加することができます。

どんな大型プロジェクトでも、通常は、作成されるバインディングのために、削除すると機能しなくなるテンプレートが必ず存在します。ボイラープレート・マクロを使用すると、そのようなテンプレート・コードの保守が大幅に容易になります。

同様に、ボイラープレートで定義された変数を利用する他の標準的マクロを作成することができます。このようなマクロを使用すると、変数のバインディング、派生、およびパラメーターの受け渡しを書いたり書き直したりする必要がなくなるので、タイピングを大幅に減らすことができます。また、そのようなコードでのエラー発生の可能性も少なくなります。

ただし、ボイラープレート・マクロは万能薬ではないことを理解しておいてください。次のような多くの重大な問題が発生する可能性があります。

  • 以前にマクロで定義された変数名を導入することで、バインディングを誤って上書きする。
  • マクロの入出力が明示的ではなく暗黙的なので、問題の追跡が難しい。

これらは、ボイラープレート・マクロとともに、いくつかの事を実行することによって、かなり回避することができます。

  • マクロに明確なラベルを付け、変数名がボイラープレート・コードに由来することを示す命名規則を設けます。たとえば、マクロには-mを、ボイラープレート内で定義された変数には-bを付けます。
  • すべてのボイラープレート・マクロ、特に導入された変数バインディングとバージョン間の変更のすべてを入念に文書化します。
  • ボイラープレート・マクロは、反復性の節減が暗黙的な機能のマイナス面を上回っているときだけ使用するようにします。

特定領域言語でのマクロの使用

プログラミングで本当に必要なのは小さな特定領域言語であることが多く、現在、多くの特定領域言語が使用されています。

  • 構成ファイル
  • HTMLなどのWebマークアップ言語
  • ジョブ制御言語

これらの言語は、必ずしもチューリング完全ではありません(汎用チューリング・マシンに匹敵する計算パワーがある場合、言い換えると、そのシステムと汎用チューリング・マシンが互いをエミュレートできる場合、チューリング完全と言えます)。これらの共通点は、汎用プログラミング言語で明示的に処理しなければならない多くの暗黙の前提条件と暗黙の状態があることです。Schemeでは、特殊化された特定領域言語として動作するマクロを定義できるので、両方の世界のメリットを享受できます。

最初の例として、構成ファイル内でさまざまなセキュリティ・ドメインを扱うセキュリティ構成ファイルを考えてみましょう。いくつかの異なるセキュリティ・ドメインがあり、それぞれアクセス制御とアクセス制限が異なります。

多くのシステムには、宣言型のセキュリティがすでにあります。特に、J2EEには、これから述べるような宣言型セキュリティ機能がいくつかあります。

リスト10.J2EEの宣言型セキュリティ機能
<![CDATA[
<security-constraint>
  <web-resource-collection>
      <web-resource-name>Test Resource</web-resource-name>
      <description>This is an example Resource</description>
      <url-pattern>/Test</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>USERS</role-name>
  </auth-constraint>
</security-constraint>
]]>

このコードでは、特定のユーザーの役割(ロール)に基づいて、特定のURLへのアクセスを制限し、ログインしていないユーザーに対しては、使用すべき認証メカニズムを知らせます。これと同じことを、Schemeのマクロでも行うことができます。このようなことができるマクロを定義すればよいのです(宣言型セキュリティ・マクロ)。

(resource "Test Resource" "This is an example resource" "/Test"
   (auth-constraints (role "USERS")))

リスト10は、以前のマクロ呼び出しのためのマクロ定義を示しています(前にwebserver:が付いている関数は、Webサーバーによって提供される架空の関数です)。

リスト11.宣言型セキュリティ・マクロの作成
;;This macro creates expressions which check the validity
;;of the authentication credentials in the variable "credentials"
;;and reports and redirects unauthorized access.
(define-syntax auth-constraints
  (lambda (x)
    (syntax-case x (auth-constraints time role)
      (
        ;;This causes the constraints to be processed one at a
        ;;time within a (begin) clause.
        (auth-constraints constraint1 constraint2 ...)
        (syntax
          (begin
            (auth-constraints constraint1)
            (auth-constraints constraint2 ...)))
      )
      (
        ;;This gives the expansion for the role checking mechanism
        ;;(note that "credentials" is defined in the "resource" macro below)
        (auth-constraints (role rolename ...))
        (syntax
          (if
            (not
              (webserver:is-in-role-list credentials (list rolename ...)))
            (webserver:report-unauthorized)
            #f))
      )
      (
        ;;Allows a time-based checking
        (auth-constraints (time beginning ending))
        (syntax
          (let (
                (now (webserver:getunixtime)))
               (if
                 (or (< now beginning) (> now ending))
                 (webserver:report-unauthorized) #f)))
      )
      (
        ;;Unknown case -- assume it is code or is transformed by
        ;;another macro
        (auth-constraints unknown)
        (syntax unknown)
      )
)))

;;Each resource definition expands to a function to check
;;credentials.It piggy-backs onto the macros defined above,
;;which make up the body of the credential-checking function.
;;This sets up the "credentials" parameter which is used in the
;;expressions above
(define-syntax resource
  (lambda (x)
    (syntax-case x ()
      (
        (resource name description url security-features)
        (with-syntax
          (
            ;;This builds the function to check security information
            (security-function
              (datum->syntax-object
                (syntax k)
                `(lambda (credentials)
                   ,@(syntax-object->daturm (syntax security-features))))
          (syntax
            (webserver:add-security-function
              name description url security-function)))))))

これらのマクロについては、少し説明が必要です。まず、新しい構造体「...」が導入されています。この表記は、基本的に「前のとおりに繰り返す」ことを意味します。マクロ・パターンでも、式でも使用できます。

resourceマクロは、基本的に、セキュリティ・クレデンシャルを処理する関数を作成した後、それを引数としてwebserver:add-security-functionに渡します。これは、auth-constraintsマクロによって使用されるcredentialsを単一の引数とする関数を定義します。

auth-constraintsマクロは、もう少し複雑です。2つの形式のいずれか、すなわち、処理すべき単一の制約を取るか、処理すべき制約のリストを取ります。マクロの最初のセクションで、制約ケースのリストを複数の単一の制約ケースに分割します。...は、同様の形式が続く可能性を示すために使用されます。マクロ展開後に結果が再びマクロ展開されるという利点を活かして、それ以上展開が行われなくなるまで続けることができます。auth-constraintsの反復展開を追っていくと、それが本当に個々のauth-constraintsマクロのリストに展開され、残りのマクロ形式によって個別に処理されるのがわかります。

auth-constraintsには、この例では使用されていない2つの機能も含んでいます。1つ目は、時刻に基づいた承認メカニズムであり、2つ目は、他のマクロとコードによってさらに展開できることです。時刻に基づく承認メカニズムは、このメカニズムに複数のタイプの制約を追加できる一例に過ぎません。この展開オプションは、この後の例で使用します。

これらのマクロがリスト11に示されているセキュリティ宣言をどのように展開するかを、以下に示します。

リスト12.Scheme宣言型セキュリティの展開
(webserver:add-security-function
  "Test Resource"
  "This is an example resource"
  "/Test"
  (lambda (credentials)
    (begin
      (if (not (webserver:is-in-role-list credentials (list "USERS")))
        (webserver:report-unauthorized)
        #f))))

ここから明らかな疑問が生じます。

  • これを、なぜ、わざわざマクロとして実装したのか。
  • Javaによって使用されるXML宣言では、なぜだめなのか。

XMLの宣言型セキュリティ・ファイルのようなデータ言語よりもマクロ・セットが優れているのは、次の2つの点です。

  • 宣言情報は、ランタイムに使用されるたびに変換されるのではなく、コンパイル時に命令形に変換されるので、高速なコードになります。
  • さらに重要なことに、宣言型言語では表現力が足りない場合、プログラミング言語の表現力を使用して、ファイルに命令型ステートメントを含めることもできます。

最初の機能は便利であり、2番目の機能は貴重です。いずれにしても、マクロは通常のコードに展開されるので、宣言型言語がニーズに合わない場合は、いつでも命令型プログラミングに戻ることができます。実際、変換が詳しく文書化されていれば、宣言型ステートメントと命令型ステートメントを混在させた構成も可能です。

たとえば、ユーザーのリンク元ドメインを迷惑IPアドレスの外部リストと照合するとします。これを、宣言型セキュリティ機能と命令型セキュリティ機能を組み合わせて行うと、次のようになります。

(resource "Test Resource" "This is an example resource" "/Test"
  (auth-constraints
    (role "USERS")
    (if (rogue-ip-list:contains (webserver:ip-address credentials))
      (webserver:report-unauthorized)
      #f)))

このようにすれば、プログラミングの柔軟性が極限まで高まります。特定領域のサブ言語を使用して宣言型のプログラミングを行うことができますが、そのサブ言語ではニーズが十分に満たされない場合は、フル機能のプログラミング言語に戻ることができます。

まとめ

メタプログラミングには、大規模なコンピューター・プログラミングにおける多くの用途があります。この記事では、Schemeでメタプログラミングを行うために必要なツールに触れ、メタプログラミングの例もいくつか示しました。メタプログラミング技法をいくつかのアプリケーション分野に応用しました。

  • 構文をすっきりさせる
  • ボイラープレートの自動生成
  • 宣言型サブプログラムの作成

Schemeでは、マクロ機能を使用して、ほぼどんな種類の特定領域言語でも定義できます。ツールはそこにあります。後は、マクロ展開と通常のコードのどちらを使用すれば、より容易に、よりきれいに機能を実装できるか決めるだけです。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=226861
ArticleTitle=メタプログラミング技法 第2回:Schemeを使用したメタプログラミング
publish-date=01262006