Function Builder メモ
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を走らせるための関数を実装します。ついでにInt
をBlockProtocol
に適合させ、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))