โญResult Builders
Swift โฉ Attributes โฉ
Swift 5.4 Result builders were first introduced as a semi-official language feature called โfunction buildersโ as part of the Swift 5.1 release. Xcode 12.5
The declaration of a result builder type doesnโt have to include any protocol conformance. ๐ Swift Reference
Result builders allow us to create a new value step by step by passing in a sequence of our choosing. ๐ Paul (๐ไพไธ)
The function builders feature of Swift is described in Swift Evolution Proposal. The main goal of function builders is providing DSL like syntax. ๐ Swift with Majid
Swift โฉ Advanced Operators โฉ Result Builders โญ๏ธ (๐ไพไธ)
Swift Reference โฉ Attributes โฉ Declaration Attributes โฉ resultBuilder โญ๏ธ (๐ไพๅ)
SE0289 Result Builders โฉ Result-building methods โญ๏ธ
SwiftUI โฉ
App โฉ SceneBuilder
Scene โฉ Commands โฉ CommandsBuilder
Swift 5.4 features.
ๅ๏ผWhy not make result builder a protocolโ
those buildEither() methods also enable switch statements to be used within result builder contexts, without requiring any additional build methods. ๐ Sundell
๐paiza.io
@resultBuilder
struct StringBuilder {
// โญ๏ธ (mandantory)
static func buildBlock(_ parts: String...) -> String {
parts.joined(separator: "\n")
}
// โญ๏ธ support `if...else...` statement
static func buildEither(first part: String) -> String {
return part
}
static func buildEither(second part: String) -> String {
return part
}
// โญ๏ธ support `for...in` loop
static func buildArray(_ parts: [String]) -> String {
parts.joined(separator: ", ")
}
}
// โญ๏ธ applied on `func`
@StringBuilder func s1() -> String {
"Why settle for a Duke"
"when you can have"
"a Prince?"
}
// โญ๏ธ applied on `var`
@StringBuilder var s2: String {
"Why settle for a Duke"
"when you can have"
"a Prince?"
}
// โญ๏ธ translated by complier to:
// ------------------------------
// StringBuilder.buildBlock(
// "Why settle for a Duke",
// "when you can have",
// "a Prince?"
// )
// ------------------------------
print(s1())
print(s2)
// Why settle for a Duke
// when you can have
// a Prince?
@StringBuilder var s3: String {
"Why settle for a Duke"
"when you can have"
// โญ๏ธ supported by `buildEither(first:)` / `buildEither(second:)`
if .random() { "a Frog?" }
else { "a King?" }
}
print(s3)
@StringBuilder var countDown: String {
// โญ๏ธ supported by `buildArray(_:)`
for i in (0...10).reversed() { "\(i)โฆ" }
"Lift off!"
}
print(countDown)
// 10โฆ, 9โฆ, 8โฆ, 7โฆ, 6โฆ, 5โฆ, 4โฆ, 3โฆ, 2โฆ, 1โฆ, 0โฆ
// Lift off!
๐ paiza.io
// before using @SettingsBuilder
[
Setting("Offline mode", .bool(false)),
Setting("Search page size", .int(25)),
Setting("Experimental", .group([
Setting("Default name", .string("Untitled")),
Setting("Fluid animations", .bool(true))
]))
]
// โญ๏ธ after using @SettingsBuilder
Settings { // โญ๏ธ 1
("Offline mode", false)
("Search page size", 25)
Settings.Group("Experimental") { // โญ๏ธ 2
("Default name", "Untitled")
("Fluid animations", true)
}
}
่ฉ่ซ๏ผ้ๅไพๅญ็จ Result Builder ไผผไนๆฒ่จๅฐไป้บผๅฅฝ่ใ
// ---------------
// Setting
// ---------------
struct Setting {
var name: String
var value: Value
}
extension Setting {
enum Value {
case bool(Bool)
case int(Int)
case string(String)
case group([Setting])
}
init(_ name: String, _ value: Value){
self.init(name: name, value: value)
}
init(_ tuple: Tuple){
self.init(name: tuple.name, value: tuple.value)
}
typealias Tuple = (name: String, value: Value)
}
// -----------------------
// SettingsBuilder
// -----------------------
@resultBuilder
struct SettingsBuilder {
typealias Component = Settings
static func buildBlock(_ components: Component...) -> Component {
Array(components.joined())
}
// -------------------------
// โญ๏ธ buildExpression
// -------------------------
// for tuple
static func buildExpression(_ tuple: Setting.Tuple) -> Component {
[Setting(tuple)]
}
// โญ๏ธ case .bool
static func buildExpression(_ tuple: (name: String, bool: Bool)) -> Component {
[Setting(tuple.name, .bool(tuple.bool))]
}
// โญ๏ธ case .int
static func buildExpression(_ tuple: (name: String, int: Int)) -> Component {
[Setting(tuple.name, .int(tuple.int))]
}
// โญ๏ธ case .string
static func buildExpression(_ tuple: (name: String, string: String)) -> Component {
[Setting(tuple.name, .string(tuple.string))]
}
// for a normal Setting
static func buildExpression(_ setting: Setting) -> Component {
[setting]
}
}
// -----------------
// Settings
// -----------------
typealias Settings = [Setting]
// typealias can be extended โญ๏ธ
extension Settings {
// Settings { ... }
init(
@SettingsBuilder settings: () -> [Setting] // โญ๏ธ 1
) {
self = settings()
}
// Settings.Group { ... }
static func Group(
_ name: String,
@SettingsBuilder settings: () -> [Setting] // โญ๏ธ 2
) -> Setting {
Setting(name: name, value: .group(settings()))
}
}
๐ paiza.io
ๆญค็ฏไพๅปบ็ซไธๅ Drawable ๅๅฎไพ่ฆ็ฏๆๆใ็จๆผ็ตๅ็ๅๅฅใใ
Line ๆฏ Drawable ้้กๅๅฅ็ใๆ็ต็ตๅ็ฎๆจใใ
DrawingBuilder ็จไพๅฏฆ็พใๅฆไฝ็ตๅใ้ไบๅๅฅใ
// ---------------
// Drawable
// ---------------
//
// โข Types for drawing on a single line
// using stars ("*") and text (String)
// ๐ธ Drawable
// defines the requirement for something that can be drawn (as a String)
protocol Drawable: CustomStringConvertible {
func draw() -> String
}
extension Drawable {
var description: String { draw() } // CustomStringConvertible
}
// ๐ธ Line
// represents a single-line drawing.
// serves the top-level container for most drawings.
struct Line: Drawable {
var drawables: [Drawable]
init(_ drawables: [Drawable]) { self.drawables = drawables }
// โญ 5. closure parameter (@DrawingBuilder)
init(@DrawingBuilder content: () -> Drawable) {
self.drawables = [content()]
}
func draw() -> String {
drawables
.map { $0.draw() }
.joined(separator: "")
}
}
// ๐ธ Text
// wraps a string to make it part of a drawing
struct Text: Drawable {
var content: String
init(_ content: String) { self.content = content }
func draw() -> String { content }
}
// ๐ธ Space
struct Space: Drawable {
func draw() -> String { " " }
}
// ๐ธ Stars
struct Stars: Drawable {
var length: Int
init(_ length: Int) { self.length = length }
func draw() -> String { String(repeating: "๐", count: length) }
}
// ๐ธ AllCaps
// converts text in the drawing to uppercase
struct AllCaps: Drawable {
var content: Drawable
// โญ 3. closure parameter (@DrawingBuilder)
init(@DrawingBuilder content: () -> Drawable) {
self.content = content()
}
func draw() -> String { content.draw().uppercased() }
}
// -----------------------
// โญ DrawingBuilder
// -----------------------
// ๐ธ DrawingBuilder
@resultBuilder
struct DrawingBuilder {
/// โญ 1. asupport a block of code
/// ๆฏๅ @DrawingBuilder closure ็ๆๅคๅฑค้ฝๆฏ็ฑๆญคๆนๆณ่็ใ
static func buildBlock(_ drawables: Drawable...) -> Drawable {
Line(drawables)
}
/// โญ 2. support `if-else`, `if-let-else` , `switch`
static func buildEither(first: Drawable) -> Drawable {
return first
}
static func buildEither(second: Drawable) -> Drawable {
return second
}
}
// -----------------------
// โญ Use Cases
// -----------------------
// ๐ธ greeting(for:)
@DrawingBuilder
func greeting(for name: String? = nil) -> Drawable {// โญ @DrawingBuilder
Stars(3)
Space()
Text("Hello")
Space()
AllCaps { // โญ 3. init (@DrawingBuilder)
if let name = name { Text(name + "!") } // โฎ
else { Text("World!") } // โฏ โญ 2. buildEither
}
Space()
Stars(2)
}
print(greeting())
print(greeting(for: "swift taylor"))
// ๐๐๐ Hello WORLD! ๐๐
// ๐๐๐ Hello SWIFT TAYLOR! ๐๐
// ---------------------------------
// โญ DrawingBuilder extension
// ---------------------------------
extension DrawingBuilder {
/// โญ 4. support `for-in` , `.map()`
static func buildArray(_ components: [Drawable]) -> Drawable {
Line(components)
}
}
// -----------------------
// โญ Use Cases
// -----------------------
let stars = Line { // โญ 5. @DrawingBuilder
Text("Stars:")
// โญ 4. buildArray
for i in 1...3 { // โฎ
Space() // โญ๏ธ 1. buildBlock
Stars(i) // โ
} // โฏ
}
print(stars)
// Stars: ๐ ๐๐ ๐๐๐
ๆญคไพ่ชชๆ result building methods ๅฐๅบๆฏๅฆไฝๅท่ก็ตๅ็ไปปๅใ ๐ Result-Building Methods
๐ paiza.io
// --------------------
// ArrayBuilder
// --------------------
@resultBuilder
struct ArrayBuilder {
// log message index โญ๏ธ
static var index = 0
typealias Component = [Int]
typealias Expression = Int
// helper function โญ๏ธ
static func reportAndReturn<Input, Result>(
_ input:Input, _ result: Result, _ token: String
) -> Result
{
index += 1
print("\(index)] build \(token): \(input) -> \(result)")
return result
}
// translate Expression into Component โญ๏ธ
static func buildExpression(_ element: Expression) -> Component {
reportAndReturn(element, [element], "expression")
}
// support `if` โญ๏ธ
static func buildOptional(_ component: Component?) -> Component {
reportAndReturn(component, component ?? [], "optional ")
}
// support `if-else`, `if-let-else` โญ๏ธ
static func buildEither(first component: Component) -> Component {
reportAndReturn(component, component, "1st ")
}
static func buildEither(second component: Component) -> Component {
reportAndReturn(component, component, "2nd ")
}
// support `for-in` loop โญ๏ธ
static func buildArray(_ components: [Component]) -> Component {
reportAndReturn(components, Array(components.joined()), "array ")
}
// translate block statement into Component (required) โญ๏ธ
static func buildBlock(_ components: Component...) -> Component {
reportAndReturn(components, Array(components.joined()), "block ")
}
}
// --------------------
// test run
// --------------------
@ArrayBuilder var c1: [Int] { // 3
let _ = print("hi") // `print` in builder โญ๏ธ
10 // 2
}
var c2 = ArrayBuilder.buildExpression(5) // 1
// 1] build expression: 5 -> [5] // ้ๅ็ซ็ถๅ
ๅท่ก โญ๏ธ
// hi
// 2] build expression: 10 -> [10]
// 3] build block : [[10]] -> [10]
print(c1) // [10]
print(c2) // [5]
var n = 19
@ArrayBuilder var c3: [Int] { // 7
if n < 12 {
21
} else { // 5, 6
22 // 4
}
}
// 4] build expression: 22 -> [22]
// 5] build block : [[22]] -> [22]
// 6] build 2nd : [22] -> [22]
// 7] build block : [[22]] -> [22]
print(c3) // [22]
n = 51
@ArrayBuilder var c4: [Int] { // 15 ({)
if n < 12 {
let _ = print("level: 1, 1st")
31
} else { // 13 ({), 14 (else)
let _ = print("level: 1, 2nd")
if n == 19 {
let _ = print("level: 2, 1st")
32
} else { // 11 ({), 12 (else)
let _ = print("level: 2, 2nd")
if n > 50 { // 9 ({), 10 (if)
let _ = print("level: 3, 1st")
33 // 8
} else {
let _ = print("level: 3, 2nd")
34
}
}
}
}
print(c4)
// level: 1, 2nd
// level: 2, 2nd
// level: 3, 1st
// 8] build expression: 33 -> [33]
// 9] build block : [[33]] -> [33]
// 10] build 1st : [33] -> [33]
// 11] build block : [[33]] -> [33]
// 12] build 2nd : [33] -> [33]
// 13] build block : [[33]] -> [33]
// 14] build 2nd : [33] -> [33]
// 15] build block : [[33]] -> [33]
/*
่ฆๅ๏ผ็ฑๆๅ
งๅฑคๅพๅคๅๆบฏ โญ๏ธ
------------------------------
โข ๆๅ
งๅฑค ๏ผbuild Expression
โข ้ๅฐ { ๏ผbuild Block
โข ้ๅฐ if ๏ผbuild 1st / Optional
โข ้ๅฐ else๏ผbuild 2nd
โข ้ๅฐ for ๏ผbuild Array
*/
n = 51
@ArrayBuilder var c5: [Int] { // 20 ({)
if n < 12 {
let _ = print("level: 1, 1st")
41
} else if n == 19 {
let _ = print("level: 2, 1st")
42
} else if n > 50 { // 17 ({), 18 (if), 19 (else)
let _ = print("level: 3, 1st")
43 // 16
} else {
let _ = print("level: 3, 2nd")
44
}
}
// level: 3, 1st
// 16] build expression: 43 -> [43]
// 17] build block : [[43]] -> [43]
// 18] build 1st : [43] -> [43]
// 19] build 2nd : [43] -> [43]
// 20] build block : [[43]] -> [43]
print(c5) // [43]
@ArrayBuilder var c6: [Int] { // 24 ({)
if (n % 2) == 1 { // 22 ({), 23 (if)
51 // 21
}
}
// 21] build expression: 51 -> [51]
// 22] build block : [[51]] -> [51]
// 23] build optional : Optional([51]) -> [51]
// 24] build block : [[51]] -> [51]
print(c6) // [51]
@ArrayBuilder var c7: [Int] { // ({) 32
for i in 1...3 { // ({) 26, 28, 30 | 31 (for)
100 + i // (E) 25, 27, 29
}
}
// 25] build expression: 101 -> [101]
// 26] build block : [[101]] -> [101]
// 27] build expression: 102 -> [102]
// 28] build block : [[102]] -> [102]
// 29] build expression: 103 -> [103]
// 30] build block : [[103]] -> [103]
// 31] build array : [[101], [102], [103]] -> [101, 102, 103]
// 32] build block : [[101, 102, 103]] -> [101, 102, 103]
print(c7) // [101, 102, 103]
Last updated