24/7 twenty-four seven

iOS/OS X application programing topics.

SwiftマクロでPowerAssertを実装する

現在Swiftにマクロを導入しようという提案がSwift Evolutionのレビュー中*1です。 SwiftによってSwiftの構文を拡張できる、いわゆるメタプログラミングと呼ばれる機能です。

実はマクロの他にもSwiftでメタプログラミングを実現する機能の提案が複数提案*2*3されています。 Swift 6はメタプログラミングの時代になるかもしれません。

現代的なプログラミング言語のマクロ

みなさんはマクロと聞いて、どのような機能を想像しますか? C言語のマクロは、プリプロセッサと呼ばれるコンパイル前のプログラムによってプログラムのソースコードに置換や文字列連結を行う機能でした。 原理的には単なる文字列操作なので、プログラムの構造や型を破壊する可能性がありました。 最初のマクロに関する投稿に対しての否定的なコメントは、C言語のマクロのような機能をSwiftに導入することは危険だという意見が多かったようです。

しかし、現代のプログラミング言語では、マクロの機能は大きく異なっています。 C++に導入されたテンプレートやRustに導入されたマクロは、プログラムの構造を操作する機能です。 たとえばRustのマクロは、プログラムのAST(抽象構文木)を操作する機能です。 ASTはプログラムの構造を抽象化した木構造で、プログラムのコンパイルや解析などに利用します。 Rustのマクロは、プログラムのASTを操作することで、プログラムの構造を変更できます。 このようなマクロは、プログラムの構造を操作する機能であるため、C言語のマクロのような危険性はありません。 また、マクロを使うことでプログラムの構造をより柔軟に操作できるようになります。

Swiftのマクロはコンパイラによる型チェックが行われるため、型安全であることが期待できます。 Swiftではマクロを導入することで、一部のDSLをより簡潔に記述できるようになったり、機械的なボイラープレートの記述をなくせたりということが利点として挙げられています。

Swiftにおけるマクロの実装

Swiftのマクロが単なる文字列操作でないことを理解するために、Swiftのマクロをどのように実装するのかをみてみましょう。

Swift Coreチームは、現在Compiler Control Statementsと呼ばれる#file#colorLiteralなどの機能をマクロで再実装することを検討しているようです。 提案書にはマクロのサンプルコードとして現在は標準の言語機能として組み込まれている#colorLiteralをマクロ機能で再実装したもの*4が公開されています。 #colorLiteralマクロは次のように使用します。

let backgroundColor: UIColor
  = #colorLiteral(red: 0.5, green: 0.5, blue: 0.25, alpha: 1.0)

もともとのCompiler Control Statementsである#colorLiteralはXcode上でカラーピッカーを用いて色を選択できたり、実際の色がソースコード上に表示され確認できるというIDEのサポートを前提とした機能で、実際にマクロとして動作するわけではありません。 今のところSwiftマクロの実装では#colorLiteralマクロは次のようなソースコードの形に展開するように実装しています。

let backgroundColor: UIColor
  = .init(_colorLiteralRed: 0.5, green: 0.5, blue: 0.25, alpha: 1.0)

では、このような動作を実現する#colorLiteralマクロの実装をみてみましょう。

// Declaration of #colorLiteral
@expression macro colorLiteral(
  red: Float, green: Float, blue: Float, alpha: Float
) -> _ColorLiteralType
  = SwiftBuiltinMacros.ColorLiteralMacro

// Implementation of #colorLiteral
struct ColorLiteralMacro: ExpressionMacro {
  /// Replace the label of the first element in the tuple with the given
  /// new label.
  func replaceFirstLabel(
    of tuple: TupleExprElementListSyntax, with newLabel: String
  ) -> TupleExprElementListSyntax{
    guard let firstElement = tuple.first else {
      return tuple
    }

    return tuple.replacing(
      childAt: 0, with: firstElement.withLabel(.identifier(newLabel)))
  }

  static func expansion(
    of node: MacroExpansionExprSyntax, in context: MacroExpansionContext
  ) -> MacroResult<ExprSyntax> {
    let argList = replaceFirstLabel(
      of: node.argumentList, with: "_colorLiteralRed"
    )
    let initSyntax: ExprSyntax = ".init(\(argList))"
    if let leadingTrivia = node.leadingTrivia {
      return MacroResult(initSyntax.withLeadingTrivia(leadingTrivia))
    }
    return initSyntax
  }  
}

まず次にに示すようにマクロの型宣言があります。

@expression macro colorLiteral(
  red: Float, green: Float, blue: Float, alpha: Float
) -> _ColorLiteralType
  = SwiftBuiltinMacros.ColorLiteralMacro

このコードは#colorLiteralというマクロはFloatの引数を3つ受け取り、戻り値として_ColorLiteralTypeを返すというインターフェースであることを定義しています。 文法は関数の定義とほとんど同様です。関数を示すfuncの代わりに@expressionmacroという2つのキーワードを使用します。 マクロであることを示すだけなら@expressionは不要ですが、これは将来マクロの種類が増えた場合に各マクロを区別するために用意されています。

マクロの可能性を探る

ここまででSwiftのマクロがどのようなものか、そしてどのように実装されているかを解説してきました。 単なる文字列操作ではなく、プログラムの構造を操作する機能であることがわかりました。

前述のサンプルコードは型チェックが働くものの、結果的に単なる文字列操作と変わらないと感じるかもしれません。 そこで少し高度で、かつ実践的な例をひとつ考えてみましょう。

マクロを使ってPower Assertを実装する

Power Assertは、プログラムのテストが失敗したときにテスト条件の評価結果を分かりやすく表示する機能です。 たとえば次のようなテストコードがあったとします。

func testPowerAssert() {
  let a = 1
  let b = 2
  let c = 3
  #powerAssert(max(a, b) == c)
}

このテストコードはabのうち大きい方がcと等しいことをテストしています。 このテストが失敗したときにPower Assertは次のようにテスト条件の評価結果をアスキーアートを使ってわかりやすく表示します。

#powerAssert(max(a, b) == c)
             |   |  |  |  |
             7   4  7  |  12
                       false

Swiftのマクロは構文木を受け取って構文木を返す機能です。 ソースコードの変数や式といった構造を受け取れるのでPower Assertの機能を実装するために必要な引数のひとつひとつの情報をマクロの内部で扱えます。

具体的には#powerAssertマクロに渡された引数の各要素を評価して、引数の位置とともに保存するコードを生成します。 マクロに渡された式全体がfalseのときにテストが失敗したとして、それぞれの変数や式の値をアスキーアートで表示するコードを生成します。

最初のテストコードに含まれるマクロ#powerAssert(max(a, b) == c)は、マクロによって実際は次のようなコードに変換されます。

PowerAssert.Assertion(#"#powerAssert(max(a, b) == c)"#, line: 14)
  .assert(max(a, b) == c)
  .capture(expression: max(a, b), column: 13)
  .capture(expression: a.self, column: 17)
  .capture(expression: b.self, column: 20)
  .capture(expression: max(a, b) == c, column: 23)
  .capture(expression: c.self, column: 26
  .render()

このコードを実行すると前述のアスキーアートを表示する結果が得られます。 これはコンパイル時にソースコードの構造に対して変更を適用できるというSwiftのマクロならではの活用例です。単純な文字列操作では、Swiftの構文解析器を実装するなどしなければ、このような機能は実現できません。

さらに本Power Assertの実装は、後に提出されたSwift Evolutionのプロポーザル*5にマクロのユースケースのひとつとして紹介*6されています。

このSwift Power Assertの実装はGitHubリポジトリ*7で公開しています。興味があったら参考にしてください。

参考資料