# SwiftのDSL

# DSLとは

DSLとはドメイン固有言語といい、ある特定の問題を処理するために特化したコンピュータ言語のことを言います。DSLには、処理をする対象ごとに様々な言語があります。例えば、Webページを構築するためのHTML、スタイルに特化したCSS、データベースへの問い合わせに特化したSQLなどもDSLと言えます。

元になる汎用的な言語が存在し、その文法や機能を使って定義したDSLを 組み込みDSL (embedded DSL) といいます。反対に既存の言語とは関係なく、独立して定義されてるDSLを 外部DSL (external DSL)といいます。

Swiftはこの組み込みDSLを活用し、プログラムの中に組み込みDSLを記述できる機能を提供しています。

Swiftが組み込みDSLとして活用している例が、SwiftUIです。SwiftUIはユーザーインターフェースを構築するために特化した組み込みDSLと言えます。

例えば、文字を表示してフォントを太字にしたい場合は次のように書くことができます。

Text("Hello")
  .fontWeight(.bold)

Swiftが提供するDSLを使うことで、シンプルかつ直感的にUIを構築することができます。

# 関数ビルダ

関数ビルダは、Swift 5.1で導入された機能です。関数ビルダは、記述された要素を組み合わせて関数の呼び出しを生成します。SwiftUIの組み込みDSLを実装するために関数ビルダが活用されています。

# 定義方法

関数ビルダを生成するには、@_functionBuilder 属性を記述して定義します。

次の例では、構造体に @_functionBuilder を記述して関数ビルダとして定義しています。buildBlock メソッドは 必須 で定義します。受け取った引数の値を元に処理を記述します。

@_functionBuilder struct Builder {
  static func buildBlock(_ args: Int...) -> [Int] {
    return args
  }
}

上記の例では、受け取ったInt型の 可変長引数 をそのまま返却しています。

# 使用方法

定義した関数ビルダは、@ + 関数ビルダの名前 を記述すると使うことができます。

次の例では、前節で定義した Builder を使用します。関数ビルダの後に、関数を定義します。

@Builder
func createIntArray() -> [Int] {}

関数の内部では改行区切りで数値を記述します。これは、式文 (expression statement) といい、改行; 区切りで一つの文として解釈されます。

この一つ一つの式文が、関数ビルダの引数として渡されることになります。

@Builder
func createIntArray() -> [Int] {
  1
  2
  3
}

実際に実行してみましょう。createIntArray を実行すると、内部で式文が引数として渡された関数ビルダの処理(buildBlock)が呼び出され、Int型の配列が返ってきているのが分かります。

let intArray = createIntArray() // [1, 2, 3]

あの式文が内部でどのように呼び出されているかと言うと、コンパイラによって変換されたコードをみると分かります。関数ビルダが付与されたcreateIntArray関数は、コンパイラによって次のような関数に変換されています。

func createIntArray() -> [Int] {
  let _a = 1
  let _b = 2
  let _c = 3
  return Builder.buildBlock(_a, _b, _c)
}

改行区切りで記述した値が、関数ビルダのbuildBlockの引数として渡されているのです。そのため、可変長引数を受け取るbuildBlockが配列をそのまま返しているということなります。

では、次に上記の例をクロージャを使って、引数として式文を渡してみましょう。

まず、createIntArray関数の引数にクロージャを定義します。

func createIntArray(_ closure: () -> [Int]) -> [Int] {
  return closure()
}

次に、関数ビルダを引数の手前に定義します。

func createIntArray(@Builder _ closure: () -> [Int]) -> [Int] {
  return closure()
}

createIntArray関数を呼び出すときは、トレイリングクロージャ を使用してクロージャを渡します。

クロージャ内部では、式文が使えるので数値を改行区切りで記述することができます。

func createIntArray(@Builder _ closure: () -> [Int]) -> [Int] {
  return closure()
}

let intArray = createIntArray {
  1
  2
  3
} // [1,2,3]

呼び出し元で指定した数値がInt型の配列として返ってきたのが分かります。

# SwiftUIを理解する

SwiftUIは、Xcode 11から利用可能になりました。SwiftUIの組み込みDSLを使うことで、簡潔に、そして直感的にユーザーインターフェースを構築することができます。

SwiftUIの記法を初めてみたときは、内部でどのように処理されているのか想像しづらいと思います。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello")
        }
    }
}

しかし、次の機能でコードを分解することで、内部でどのように解釈されているのか理解することができます。

# 暗黙的なreturn

暗黙的なreturn は、関数の章でも説明しました。関数やコンピューテッドプロパティの中で式が一つしかないときはreturnを省略できるというものです。

例を見ると、ContentView 構造体の中で body というコンピューテッドプロパティが定義されています。return文がありませんが、これは式が一つしかないので省略されています。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello")
            Text("Hello2")
            Text("Hello3")
        }
    }
}

省略しないで書くと、次のようになります。

struct ContentView: View {
    var body: some View {
        return VStack {
            Text("Hello")
            Text("Hello2")
            Text("Hello3")
        }
    }
}

関数と同様にコンピューテッドプロパティの中でもreturnを省略できるので、例文のように書くことができるのです。

# 関数ビルダ

関数ビルダは、今章でも説明しました。

クロージャを使うことによって改行区切りで式文を引数として渡して、関数を実行することができます。

例文を見ると、VStackトレイリングクロージャ でクロージャを渡して、内部で式文を定義しているのが分かります。

VStack {
  Text("Hello")
  Text("Hello2")
  Text("Hello3")
}

VStackの実装を見ると、引数の最後に、@ViewBuilder という関数ビルダのカスタム属性が付与されたクロージャが定義されています。関数ビルダが付与されているので、呼び出し元のクロージャ内で式文が使えます。





 



@frozen public struct VStack<Content> : View where Content : View {
    @inlinable public init(
      alignment: HorizontalAlignment = .center, 
      spacing: CGFloat? = nil, 
      @ViewBuilder content: () -> Content)
    public typealias Body = Never
}

そして、この @ViewBuilder の内部をみると buildBlock の記述があるのが分かります。この buildBlock に式文が引数として渡され、処理されているのです。

@_functionBuilder public struct ViewBuilder {
    public static func buildBlock() -> EmptyView

    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}

extension ViewBuilder {

    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
}

...

buildBlockに渡すことができる引数の数は、最大10個までとなっています。

この VStack の引数のクロージャ部分がコンパイラに変換されると、次のような関数を実行しているということになります。

ViewBuilder.buildBlock(
  Text("Hello"), 
  Text("Hello2"), 
  Text("Hello3")
)

このように関数ビルダの特徴を活用することで、SwiftUIは簡潔に書くことができるようになっているのです。