働くプログラマ

マルチパラダイムと .NET (第 5 部): 自動メタプログラミング

Ted Neward

Ted Neward先月は、オブジェクトについて詳しく説明し、特に、継承に基づく共通性と可変性の分析の "軸" について見てきました。C#、Visual Basic など、近代のオブジェクト指向 (OO) 言語で使用できる共通性/可変性の形式が継承に限られるわけではありませんが、間違いなく、継承は OO パラダイムの中核を担っています。また、先月説明したように、継承によって、すべての問題に常に最善の解決策が提供されるわけではありません。

これまでの要点をまとめると、C# と Visual Basic は手続き型言語であり、OO 言語でもありますが、明らかに話はそれで終わりではありません。この 2 つの言語は "メタプログラム" 言語でもあります。つまり、Microsoft .NET Framework 開発者は、それぞれの言語を使用して、自動、反映、生成といったさまざまな方式で、プログラムからプログラムを構築することができます。

自動メタプログラミング

メタプログラミングを支える主な考え方はシンプルです。手続き型プログラミングまたは OO プログラミングの従来の構造では、ソフトウェア設計に関する問題をすべて解決できるとは言えません。少なくとも、満足のいくようには解決されません。たとえば、開発者は、基本的な欠陥を挙げるため、特定のスロットのリストに項目を挿入し、正確な順序で項目を見れるように、ある特定の型の番号付きリストを保持するデータ構造が必要になることがよくありました。場合によっては、パフォーマンス上の理由から、リストをノードのリンク リストにすることもあります。つまり、番号付きのリンク リストが必要でしたが、リスト内に格納する項目は厳密に型指定する必要があります。

C++ を経験してから .NET Framework に取り組んだ開発者は、この問題の 1 つの解決策を把握しています。それは、型をパラメーター化する、ジェネリックとも呼ばれる方法です。ただし、初期の Java を経験してから .NET Framework に取り組んだ開発者には、テンプレートが考案されるかなり前に、別の解決策がありました (最終的にはこれが Java プラットフォームになりました)。この解決策とは単純に、必要なリストの実装を必要に応じて記述するというものです (図 1 参照)。

図 1 リストの実装を必要に応じて作成する例

Class ListOfInt32

  Class Node
    Public Sub New(ByVal dt As Int32)
      data = dt
    End Sub
    Public data As Int32
    Public nextNode As Node = Nothing
  End Class

  Private head As Node = Nothing

  Public Sub Insert(ByVal newParam As Int32)
    If IsNothing(head) Then
      head = New Node(newParam)
    Else
      Dim current As Node = head
      While (Not IsNothing(current.nextNode))
        current = current.nextNode
      End While
        current.nextNode = New Node(newParam)
    End If
  End Sub

  Public Function Retrieve(ByVal index As Int32)
    Dim current As Node = head
    Dim counter = 0
    While (Not IsNothing(current.nextNode) And counter < index)
      current = current.nextNode
      counter = counter + 1
    End While

    If (IsNothing(current)) Then
      Throw New Exception("Bad index")
    Else
      Retrieve = current.data
    End If
  End Function
End Class

間違いなく、この方法では "同じことを繰り返さない" (DRY: Don’t Repeat Yourself) テストに失敗します。設計でこの種の新しいリストが必要になるたびに、リストを "手動" で作成する必要があり、時間がたつにつれて問題になるのは明らかです。複雑な方法ではありませんが、特に、多くの機能が必要になる場合は、こうしたリストをそれぞれ作成するのは、厄介で時間がかかります。

もちろん、開発者がこのようなコードを記述する必要があると言っているわけではありません。そこで、コード生成という解決策に目を向けることになります。このような解決策を "自動メタプログラミング" と呼ぶことがあります。必要な型にカスタマイズされるクラスを作り出すように設計されたプログラムなど、別のプログラムからコード生成を簡単に実行できます (図 2 参照)。

図 2 自動メタプログラミングの例

Sub Main(ByVal args As String())
  Dim CRLF As String = Chr(13).ToString + Chr(10).ToString()
  Dim template As String =
   "Class ListOf{0}" + CRLF +
   "  Class Node" + CRLF +
   "    Public Sub New(ByVal dt As {0})" + CRLF +
   "      data = dt" + CRLF +
   "    End Sub" + CRLF +
   "    Public data As {0}" + CRLF +
   "    Public nextNode As Node = Nothing" + CRLF +
   "  End Class" + CRLF +
   "  Private head As Node = Nothing" + CRLF +
   "  Public Sub Insert(ByVal newParam As {0})" + CRLF +
   "    If IsNothing(head) Then" + CRLF +
   "      head = New Node(newParam)" + CRLF +
   "    Else" + CRLF +
   "      Dim current As Node = head" + CRLF +
   "      While (Not IsNothing(current.nextNode))" + CRLF +
   "        current = current.nextNode" + CRLF +
   "      End While" + CRLF +
   "      current.nextNode = New Node(newParam)" + CRLF +
   "    End If" + CRLF +
   "  End Sub" + CRLF +
   "  Public Function Retrieve(ByVal index As Int32)" + CRLF +
   "    Dim current As Node = head" + CRLF +
   "    Dim counter = 0" + CRLF +
   "    While (Not IsNothing(current.nextNode) And counter < index)"+ CRLF +
   "      current = current.nextNode" + CRLF +
   "      counter = counter + 1" + CRLF +
   "    End While" + CRLF +
   "    If (IsNothing(current)) Then" + CRLF +
   "      Throw New Exception()" + CRLF +
   "    Else" + CRLF +
   "      Retrieve = current.data" + CRLF +
   "    End If" + CRLF +
   "  End Sub" + CRLF +
   "End Class"

    If args.Length = 0 Then
      Console.WriteLine("Usage: VBAuto <listType>")
      Console.WriteLine("   where <listType> is a fully-qualified CLR typename")
    Else
      Console.WriteLine("Producing ListOf" + args(0))

      Dim outType As System.Type =
        System.Reflection.Assembly.Load("mscorlib").GetType(args(0))
      Using out As New StreamWriter(New FileStream("ListOf" + outType.Name + ".vb",
                                              FileMode.Create))
        out.WriteLine(template, outType.Name)
      End Using
    End If

このようなクラスを作成したら、コンパイルしてプロジェクトに追加するか、バイナリとして再利用できるように固有のアセンブリにコンパイルします。

もちろん、生成される言語が、コード ジェネレーターを記述した言語と同じである必要はありません。開発者はデバッグ中に頭の中で簡単かつ明確に区別できるため、実際には、言語が異なる方が役立つことがよくあります。

共通性と可変性のメリットとデメリット

共通性と可変性の分析において、自動メタプログラミングは興味深い意味を持ちます。図 2 の例では、自動メタプログラミングは、構造と動作 (上記クラスの主要部分) を共通性に、データと型の行を可変性に配置します。つまり、可変性は、生成されるクラスに格納される型になります。おわかりのように、ListOf 型では必要な型を切り替えることができます。

ただし、自動メタプログラミングでは、必要に応じて、この関係を逆転することもできます。Visual Studio に付属する Text Template Transformation Toolkit (T4) などの機能豊富なテンプレート作成言語を使用すると、コード生成テンプレートをソース開発時に決定することができます。したがって、テンプレートでは、データと構造の行を共通性に、構造と動作の行を可変性に配置することも可能です。実際には、コード テンプレートがあまりに複雑になる場合 (そして、そのまま続けるのは適切でない場合)、共通性を完全に取り除き、データ、構造、動作などをすべて可変性に配置することができます。ただし、このような手法は、たいていすぐに手に負えなくなるため、通常は回避すべきです。このことから、自動メタプログラミングの重要な現実の 1 つが明らかになります。自動メタプログラミングには本質的な構造上の制限がないため、過度な柔軟性を求める余り、ソース コード テンプレートが制御不能にならないように、共通性と可変性を明示的に選択します。たとえば、図 2 の ListOf の例を取り上げると、構造と動作は共通性に、格納されるデータ型は可変性に配置されています。構造や動作に可変性を導入しようとすると、危険であり、混乱に陥る可能性があると考える必要があります。

コード生成には、特に保守に関する領域で、重大なリスクがある程度伴うことは明らかです。バグ (図 2 の ListOf の例における同時実行のバグなど) を発見しても、これを修正するのは簡単なことではありません。テンプレートは確実に修正できますが、既に生成されたコードは修正されません。生成済みのソースはそれぞれ再生成する必要がありますが、追跡や自動確認は困難です。また、テンプレートから生成したコードのカスタマイズを許可しない限り、基本的には、生成後のファイルに手動で加えた変更は失われます。こうした上書きの危険性は、部分クラスを使用して、開発者が生成されるクラスの "半分" (またはもう半分) に入力できるようにすることで軽減できます。また、拡張メソッドを使用して、開発者が型を編集する必要なく、型の既存のファミリにメソッドを "追加" できるようにして軽減することもできます。ただし、部分クラスはテンプレートの先頭に配置する必要があり、拡張メソッドでは既存の動作を置き換えないようにいくつかの制限を課します。そして、いずれの方法も、負の可変性を実現するのに適したメカニズムではありません。

コード生成

コード生成 (自動メタプログラミング) は、C プリプロセッサ マクロから C# T4 エンジンに至るまで、長年プログラミングの一部を担ってきました。そして、この考え方の概念的な簡潔さを求めて、今後も進展を続けることでしょう。ただし、コンパイラ構造が存在しない、拡張プロセス中に確認が行われない (もちろん、コード ジェネレーターで確認できれば話は別ですが、想像するより大変です)、有用な方法で負の可変性を捕捉できないといった、重要な欠陥があります。.NET Framework には、コード生成を簡単に行えるメカニズムがいくつか用意されています。多くの場合、このようなメカニズムは、他の Microsoft 開発者の問題を軽減するために導入されました。ただし、コード生成における潜在的な落とし穴をすべて取り除くことは、絶対にできないでしょう。

にもかかわらず、自動メタプログラミングはいまだに、幅広く使用されるメタプログラミング形式の 1 つです。C# には、C++ や C と同様、プリプロセッサ マクロが含まれています (マクロを使用して "小規模テンプレート" を作成するのは、C++ にテンプレートが用意される前から一般的でした)。加えて、大規模フレームワークやライブラリの一部としてメタプログラミングを使用するのは、特に、プロセス間通信のシナリオ (Windows Communication Foundation によって生成されるクライアントとサーバーのスタブなど) において一般的です。他のツールキットでは、アプリケーションの初期段階を容易にするために、自動メタプログラミングを使用して "スキャフォールディング" を提供します (このことについては、ASP.NET MVC で確認できます)。実際のところ、ほぼ間違いなくすべての Visual Studio プロジェクトでは、作業開始の際、大半のユーザーが新しいプロジェクトを作成したり、プロジェクトにファイルを追加したりするために使用する "プロジェクト テンプレート" および "項目テンプレート" という形式で、自動メタプログラミングを使用しています。これらはほんの一例にすぎません。自動メタプログラミングは、明らかな欠陥や危険性があるにもかかわらず、コンピューター サイエンスにおける他のたくさんの機能と同じように、便利で使いやすいツールとしてデザイナー ツールボックスに備えられています。さいわい、プログラマのツールボックスにある唯一のメタツールというわけではありません。

Ted Neward は、Microsoft .NET Framework および Java のエンタープライズ プラットフォーム システムを専門とする独立企業 Neward & Associates の社長を務めています。これまでに 100 個を超える記事を執筆している Ted は、C# MVP であり、INETA の講演者でもあります。さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox、2010 年、英語) もその 1 つです。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com (英語のみ) です。ブログを blogs.tedneward.com (英語) に公開しています。