Swift5.1で追加されたFunction Builderについて、localにコード片を書きなぐっていたので忘れ去られないうちにメモ。Function Builderの実装例を雑に書いていきます。

Function Builderとは

Function Builderを使うことでSwift内にちょっとしたDSLを定義できます。例えば次のようなコードを書いたとします。

let block = buildBlock {
    if true {
        10
        if true {
            20
            do {
                30
            }
        }
        50
    } else {
        40
    }
}

このとき得られる値blockは次のようになります(見やすさのため型に関する情報を一部省略しています)。

Either<......>.c0(
    Pair<Pair<Int, Either<Pair<Int, Int>, Empty>>, Int>(
        c0: Pair<Int, Either<Pair<Int, Int>, Empty>>(
            c0: 10,
            c1: Either<Pair<Int, Int>, Empty>.c0(
                Pair<Int, Int>(
                    c0: 20,
                    c1: 30
                )
            )
        ),
        c1: 50
    )
)

Function Builderの実装例

これを実現するために、どのようにFunction Builderを実装していけばよいか説明していきます。

1. 集合の型の定義

初めに、Block内における意味のあるまとまりを表現するための型を定義します。

/// 空集合
struct Empty { }

/// 直積
struct Pair<C0, C1> {
    let c0: C0
    let c1: C1
}

/// 直和
enum Either<C0, C1> {
    case c0(C0)
    case c1(C1)
}

/// あとで使う buildIf 関数用のinitializer
extension Either where C1 == Empty {
    init(from optional: C0?) {
        if let val = optional {
            self = .c0(val)
        } else {
            self = .c1(.init())
        }
    }
}

2. 集合の型の一般化

適当なProtocolを生やして集合の各型を一般化します。

protocol BlockProtocol { }

extension Empty: BlockProtocol { }

extension Pair: BlockProtocol where C0: BlockProtocol, C1: BlockProtocol { }

extension Either: BlockProtocol where C0: BlockProtocol, C1: BlockProtocol { }

3. Function Builderの実装

定義した集合の型を用いて、Function Builderで使われる関数を実装していきます。@_functionBuilderで構造体を注釈しています。

@_functionBuilder
struct BlockBuilder {
    /// 空集合用
    static func buildBlock() -> Empty { return .init() }

    /// 1要素用
    static func buildBlock<C>(_ c: C) -> C { return c }

    /// 2要素分をPairにまとめる
    static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> Pair<C0, C1> { return .init(c0: c0, c1: c1) }

    /// 3要素分をPairにまとめる
    static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> Pair<Pair<C0, C1>, C2> { return .init(c0: .init(c0: c0, c1: c1), c1: c2) }

    /// do block用
    static func buildDo<C>(_ c: C) -> C { return c }

    /// if block用 (else節がないとき)
    static func buildIf<C>(_ c: C?) -> Either<C, Empty> { return .init(from: c) }

    /// if-else block用(if節の方を持つ)
    static func buildEither<T, F>(first: T) -> Either<T, F> { return .c0(first) }

    /// if-else block用(else節の方を持つ)
    static func buildEither<T, F>(second: F) -> Either<T, F> { return .c1(second) }
}

この定義では3要素まで(≒3行)を扱うことができます。

Function Builder実行用の関数の実装

最後に、実装したFunction Builderを走らせるための関数を実装します。ついでにIntBlockProtocolに適合させ、Function BuilderでInt値を扱えるようにします。

func buildBlock<Body>(@BlockBuilder block: () -> Body) -> Body where Body: BlockProtocol {
    block()
}

extension Int: BlockProtocol { }

Function Builderを使う

前節で実装したFunction Builderを実際に使ってみます。(地味なのでもう少し実用例のあるやつにすればよかった。。。。)

let block1 = buildBlock {
    10
    20
}
// block1: Pair<Int, Int>(c0: 10, c1: 20)

let block2 = buildBlock {
    do {
        30
    }
}
// block2: 30

let block3 = buildBlock {
    if true {
        10
        20
    }
}
// block3: Either.c0(Pair<Int, Int>(c0: 10, c1: 20))

let block4 = buildBlock {
    if false {
        10
        20
    } else {
        30
        40
    }
}
// block4: Either.c1(Pair<Int, Int>(c0: 30, c1: 40))

let block5 = buildBlock {
    if true {
        10
        if true {
            20
            do {
                30
            }
        }
        50
    } else {
        40
    }
}
// block5: 冒頭の例と同じなので省略

Function Builder用の関数を実装する際、このようにprintなどを埋め込むと、どういった過程でBlockが構築されていくのかわかり面白いです。

static func buildBlock() -> Empty { print("buildBlock()"); return .init() }
static func buildBlock<C>(_ c: C) -> C { print("buildBlock(_:) with \(c)"); return c }
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> Pair<C0, C1> { print("buildBlock(_:_:) with \(c0) and \(c1)"); return .init(c0: c0, c1: c1) }
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> Pair<Pair<C0, C1>, C2> { print("buildBlock(_:_:_:) with \(c0), \(c1) and \(c2)"); return .init(c0: .init(c0: c0, c1: c1), c1: c2) }
static func buildDo<C>(_ c: C) -> C { print("buildDo(_:) with \(c)"); return c }
static func buildIf<C>(_ c: C?) -> Either<C, Empty> { print("buildIf(_:) with \(c)"); return .init(from: c) }
static func buildEither<T, F>(first: T) -> Either<T, F> { print("buildEither(first:) with \(first)"); return .c0(first) }
static func buildEither<T, F>(second: F) -> Either<T, F> { print("buildEither(second:) with \(second)"); return .c1(second) }

例えば、冒頭の例の場合だとこんな感じになります。直感的には、1行目から順にstackに積んでいき、各pathで終端に達したらpopしつつ、定義した集合で包んでいく感じです。(ゆるふわ表現)

// 再掲
let block5 = buildBlock {
    if true {
        10
        if true {
            20
            do {
                30
            }
        }
        50
    } else {
        40
    }
}
buildBlock(_:) with 30
buildDo(_:) with 30
buildBlock(_:_:) with 20 and 30
buildIf(_:) with Optional(Pair<Swift.Int, Swift.Int>(c0: 20, c1: 30))
buildBlock(_:_:_:) with 10, Either.c0(Pair<Swift.Int, Swift.Int>(c0: 20, c1: 30)) and 50
buildEither(first:) with Pair<Pair<Int, Either<Pair<Int, Int>, Empty>>, Int>(c0: Pair<Int, Either<Pair<Int, Int>, Empty>>(c0: 10, c1: Either<Pair<Int, Int>, Empty>.c0(Pair<Int, Int>(c0: 20, c1: 30))), c1: 50)
buildBlock(_:) with Either.c0(Pair<Pair<Int, Either<Pair<Int, Int>, Empty>>, Int>(c0: Pair<Int, Either<Pair<Int, Int>, Empty>>(c0: 10, c1: Either<Pair<Int, Int>, Empty>.c0(Pair<Int, Int>(c0: 20, c1: 30))), c1: 50))

参考