Swift で書けるメタプログラミング向けのテンプレートエンジン gysb を作った

メタプログラミングとテンプレートエンジン

プログラミングにおいて、そのプログラミング言語の機能だけではプログラムを書くのが大変だったり難しいときに、外部のツールなどを用いて、「プログラムを出力するプログラム」を作成して対応することがあります。これをメタプログラミングと呼びます。

pump

例えば、昔からあって有名なものとしては、 Google が作った pump というツールがあります。
pump は主に C++ のメタプログラミングのために設計されたツールで、ユーザーは専用のテンプレート言語でテンプレートを書き、それをツールにかけて C++ プログラムを生成します。このテンプレート言語は専用の変数宣言や if, for など限られた機能だけが使えます。pump 自体は python で実装されており、部分的には python の式も書けるようになっていて、簡単な計算などを組み込むこともできます。ツール自体は C++ 専用というわけではなく、テキストテンプレートエンジンなので、他の言語のメタプログラミングにも使用できます。

pump は googletestchromium の内部で使われています。

gyb

Apple は Swift コンパイラの開発において、メタプログラミング用のツールとして gyb を作りました。
こちらは主に Swift のメタプログラミングのために設計されています。これも pump と同様 python で実装されていますが、独自のテンプレート言語ではなく、ほぼそのまま python を書くことができるようになっていて、 pump と比べるとより自由なメタプログラミングが行なえます。

gyb の利用例としては、 swift 標準ライブラリの Array の実装 に使われている他、こちらも同様に他の言語にも使用可能なため、 C++ コードの生成 にも使われています。

gyb の例

gyb については moaible さんが gyb を解説したブログ記事 がわかりやすいです。簡単に例を示すため、この記事からコードを引用します。

以下のようなテンプレートを書きます。

%{
  intTypes = [8,16,32,64]
}%

% for intType in intTypes:
    % for sign in ['','U']:

/// Extension that adds a few additional functionalities to ${sign}Int${intType}
extension ${sign}Int${intType} {

    /// Returns a ${sign}Int${intType} with all ones
        %if sign == '':
    public static var allOnes:Int${intType} {
       return Int${intType}(bitPattern: UInt${intType}.max)
    }
        %else:
    public static var allOnes:UInt${intType} {
       return UInt${intType}.max
    }
        %end
}
    %end
%end

%{ }% で挟まれた領域や、 % から始まる行が、テンプレート生成を制御するための python コードを書いている部分、 ${ } は python の式が文字列として展開される部分、その他の部分は生成する Swift コードの断片です。 Swift のコードの部分を python の if 文で挟んだり for ループで囲んだりすることで、条件に応じて Swift のコードを書き換えたり、ループが繰り返された分だけ Swift のコードが繰り返されたりします。

このテンプレートコードを gyb コマンドで処理すると、次のような Swift コードが生成されます。

/// Extension that adds a few additional functionalities to Int8
extension Int8 {

    /// Returns a Int8 with all ones
    public static var allOnes:Int8 {
       return Int8(bitPattern: UInt8.max)
    }
}

/// Extension that adds a few additional functionalities to UInt8
extension UInt8 {

    /// Returns a UInt8 with all ones
    public static var allOnes:UInt8 {
       return UInt8.max
    }
}

/// Extension that adds a few additional functionalities to Int16
extension Int16 {

    /// Returns a Int16 with all ones
    public static var allOnes:Int16 {
       return Int16(bitPattern: UInt16.max)
    }
}

/// Extension that adds a few additional functionalities to UInt16
extension UInt16 {

    /// Returns a UInt16 with all ones
    public static var allOnes:UInt16 {
       return UInt16.max
    }
}

/// Extension that adds a few additional functionalities to Int32
extension Int32 {

    /// Returns a Int32 with all ones
    public static var allOnes:Int32 {
       return Int32(bitPattern: UInt32.max)
    }
}

/// Extension that adds a few additional functionalities to UInt32
extension UInt32 {

    /// Returns a UInt32 with all ones
    public static var allOnes:UInt32 {
       return UInt32.max
    }
}

/// Extension that adds a few additional functionalities to Int64
extension Int64 {

    /// Returns a Int64 with all ones
    public static var allOnes:Int64 {
       return Int64(bitPattern: UInt64.max)
    }
}

/// Extension that adds a few additional functionalities to UInt64
extension UInt64 {

    /// Returns a UInt64 with all ones
    public static var allOnes:UInt64 {
       return UInt64.max
    }
}

このように、 分岐やループが処理された結果、少しずつ異なる Swift コードをまとめて生成できます。

gyb についての国内の情報は少ないですが、その他にも以下のような事例があります。

これらの事例からもわかるように、様々な型のバリエーションに対応したコードを生成するために使われる事が多いです。 Swift のジェネリクスが今後改良されていけば、 gyb を使用しなくとも Swift の言語機能で対応できる場面が増えていくと思いますが、知っておくと便利なこともあるでしょう。また、高度な自動生成に関しては、ジェネリクスの範囲を超えるので、メタプログラミングの需要の一部は残り続けると思います。

gyb の問題点

このように gyb はとても便利なツールですが、致命的な問題点があります。それは、 python を書かなければならないという事です。 Swift プログラマの中には python を知らない人も居ます。メタプログラミングをしたいほど Swift が好きなのに、その Swift を書くために python を書くのはおもしろくありません。

Swift で書ける gysb

そこで、 Swift で書けるメタプログラミング用のテンプレートエンジンである gysb を作成しました。
gysb の文法は gyb から丸パクリです。 %{ }% で制御を書き、 ${ } で式展開を書きます。ただ、それを swift で書けます。

先程の moaible さんのサンプルを gysb で書くと以下のようになります。

%{
  let intTypes = [8,16,32,64]
}%

% for intType in intTypes {
    % for sign in ["", "U"] {

/// Extension that adds a few additional functionalities to ${sign}Int${intType}
extension ${sign}Int${intType} {

    /// Returns a ${sign}Int${intType} with all ones
        % if sign == "" {
    public static var allOnes: Int${intType} {
       return Int${intType}(bitPattern: UInt${intType}.max)
    }
        % } else {
    public static var allOnes: UInt${intType} {
       return UInt${intType}.max
    }
        % }
}
    % }
% }

これの処理結果は gyb の場合と全く同じです。

swift なので、テンプレート中で変数名を間違えたりすれば、コンパイルエラーとして検出されます。

gysb の設計

gysb はテンプレート構文こそ gyb と同じですが、実装は全く独自になっています。 gysb それ自体ももちろん Swift で実装されています。

gysb ではテンプレートファイルをいったん swift ソースにコンパイルします。そして、その生成された swift ソースを改めてコンパイル、実行すると、テンプレート処理結果のテキストが出力されます。もちろんこの2段コンパイル実行は gysb が内部で行うので、ユーザとしては gysb コマンドを1度呼び出すだけです。

例えば、先程の例のテンプレートをコンパイルした結果は以下のようになります。

func write(_ s: String) {
    print(s, terminator: "")
}

  let intTypes = [8,16,32,64]
write("\n")
 for intType in intTypes {
     for sign in ["", "U"] {
write("\n")
write("/// Extension that adds a few additional functionalities to ")
write(String(describing: sign))
write("Int")
write(String(describing: intType))
write("\n")
write("extension ")
write(String(describing: sign))
write("Int")
write(String(describing: intType))
write(" {\n")
write("\n")
write("    /// Returns a ")
write(String(describing: sign))
write("Int")
write(String(describing: intType))
write(" with all ones\n")
         if sign == "" {
write("    public static var allOnes: Int")
write(String(describing: intType))
write(" {\n")
write("       return Int")
write(String(describing: intType))
write("(bitPattern: UInt")
write(String(describing: intType))
write(".max)\n")
write("    }\n")
         } else {
write("    public static var allOnes: UInt")
write(String(describing: intType))
write(" {\n")
write("       return UInt")
write(String(describing: intType))
write(".max\n")
write("    }\n")
         }
write("}\n")
     }
 }

このように、テンプレートにおいて制御コードだった部分はそのまま Swift コードに、テンプレートにおいて Swift コードだった部分は Swift 上の文字列リテラルに、ちょうど反転するように変換されます。あとはこれを実行すれば処理結果のテキストが得られます。

このテンプレートコンパイル自体も一種のメタプログラミングといえるでしょう。つまり、 Swift メタプログラミングをするために、 Swift メタプログラミングをする Swift のプログラムを書いた、という事になります。楽しいですね。