2021 Swift
Write a DSL in Swift using result builders
Some problems are easier to solve by creating a customized programming language, or “domain-specific language.” While creating a DSL traditionally requires writing your own compiler, you can instead use result builders with Swift 5.4 to make your code both easier to read and maintain. We’ll take you through best practices for designing a custom language for Swift: Learn about result builders and trailing closure arguments, explore modifier-style methods and why they work well, and discover how you can extend Swift’s normal language rules to turn Swift into a DSL. To get the most out of this session, it’s helpful (though not necessary) to have some experience writing SwiftUI views. You won’t need to know anything about parser or compiler implementation.
Watch at developer.apple.com ↗Code shown on screen · 46 snippets
FavoriteSmoothies view
struct FavoriteSmoothies: View {
private var model: FrutaModel
var body: some View {
SmoothieList(smoothies: model.favoriteSmoothies)
.overlay(
Group {
if model.favoriteSmoothies.isEmpty {
Text("Add some smoothies!")
.foregroundColor(.secondary)
.frame(maxWidth: .infinity,
maxHeight: .infinity)
}
}
)
.navigationTitle("Favorites")
}
} FavoriteSmoothies view (hypothetical alternative)
// Hypothetical code--not actually supported by SwiftUI
struct FavoriteSmoothies: View {
private var model: FrutaModel
var body: some View {
var list = SmoothieList(smoothies: model.favoriteSmoothies)
let overlay: View
if model.favoriteSmoothies.isEmpty {
var text = Text("Add some smoothies!")
text.foregroundColor = .secondary
var frame = Frame(subview: text)
frame.maxWidth = .infinity
frame.maxHeight = .infinity
overlay = frame
} else {
overlay = EmptyView()
}
list.addOverlay(overlay)
list.navigationTitle = "Favorites"
return list
}
} FavoriteSmoothies view
struct FavoriteSmoothies: View {
private var model: FrutaModel
var body: some View {
SmoothieList(smoothies: model.favoriteSmoothies)
.overlay(
Group {
if model.favoriteSmoothies.isEmpty {
Text("Add some smoothies!")
.foregroundColor(.secondary)
.frame(maxWidth: .infinity,
maxHeight: .infinity)
}
}
)
.navigationTitle("Favorites")
}
} FavoriteSmoothies view
struct FavoriteSmoothies: View {
private var model: FrutaModel
var body: some View {
SmoothieList(smoothies: model.favoriteSmoothies)
.overlay(
Group {
if model.favoriteSmoothies.isEmpty {
Text("Add some smoothies!")
.foregroundColor(.secondary)
.frame(maxWidth: .infinity,
maxHeight: .infinity)
}
}
)
.navigationTitle("Favorites")
}
} Simple result builder example
VStack {
Text("Title").font(.title)
Text("Contents")
} Simple result builder example + struct VStack
VStack {
Text("Title").font(.title)
Text("Contents")
}
struct VStack<Content: View>: View {
…
init( content: () -> Content) {
self.content = content()
}
} Simple result builder example + struct VStack + trailing closure applied
VStack /* .init(content: */ {
Text("Title").font(.title)
Text("Contents")
} /* ) */
struct VStack<Content: View>: View {
…
init( content: () -> Content) {
self.content = content()
}
} Simple result builder example + struct VStack + trailing closure applied + enum ViewBuilder
VStack /* .init(content: */ {
Text("Title").font(.title)
Text("Contents")
/* return // TODO: build results using ‘ViewBuilder’ */
} /* ) */
struct VStack<Content: View>: View {
…
init( content: () -> Content) {
self.content = content()
}
}
enum ViewBuilder {
static func buildBlock(_: View...) -> some View { … }
} Simple result builder example + struct VStack + trailing closure applied + enum ViewBuilder + result builder applied
VStack /* .init(content: */ {
/* let v0 = */ Text("Title").font(.title)
/* let v1 = */ Text("Contents")
/* return ViewBuilder.buildBlock(v0, v1) */
} /* ) */
struct VStack<Content: View>: View {
…
init( content: () -> Content) {
self.content = content()
}
}
enum ViewBuilder {
static func buildBlock(_: View...) -> some View { … }
} Fruta's smoothie lists, pre-DSL
// Fruta’s Smoothie lists
extension Smoothie {
static let berryBlue = Smoothie(
id: "berry-blue",
title: "Berry Blue",
description: "Filling and refreshing, this smoothie will fill you with joy!",
measuredIngredients: [
MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)),
MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)),
MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups))
],
hasFreeRecipe: true
)
static let carrotChops = Smoothie(…)
static let crazyColada = Smoothie(…)
// Plus 12 more…
}
extension Smoothie {
private static let allSmoothies: [Smoothie] = [
.berryBlue,
.carrotChops,
.crazyColada,
// Plus 12 more…
]
static func all(includingPaid: Bool = true) -> [Smoothie] {
if includingPaid {
return allSmoothies
}
logger.log("Free smoothies only")
return allSmoothies.filter { $0.hasFreeRecipe }
}
} Fruta's smoothie lists, pre-DSL (hypothetical alternative)
// Fruta’s Smoothie lists (hypothetical alternative)
extension Smoothie {
static let berryBlue = Smoothie(
id: "berry-blue",
title: "Berry Blue",
description: "Filling and refreshing, this smoothie will fill you with joy!",
measuredIngredients: [
MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)),
MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)),
MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups))
],
hasFreeRecipe: true
)
static let carrotChops = Smoothie(…)
static let crazyColada = Smoothie(…)
// Plus 12 more…
}
extension Smoothie {
static func all(includingPaid: Bool = true) -> [Smoothie] {
var allSmoothies: [Smoothie] = [
.berryBlue,
.carrotChops,
]
if includingPaid {
allSmoothies += [
.crazyColada,
// Plus more
]
} else {
logger.log("Free smoothies only")
}
return allSmoothies
}
} Fruta's smoothie lists, pre-DSL
// Fruta’s Smoothie lists
extension Smoothie {
static let berryBlue = Smoothie(
id: "berry-blue",
title: "Berry Blue",
description: "Filling and refreshing, this smoothie will fill you with joy!",
measuredIngredients: [
MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)),
MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)),
MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups))
],
hasFreeRecipe: true
)
static let carrotChops = Smoothie(…)
static let crazyColada = Smoothie(…)
// Plus 12 more…
}
extension Smoothie {
private static let allSmoothies: [Smoothie] = [
.berryBlue,
.carrotChops,
.crazyColada,
// Plus 12 more…
]
static func all(includingPaid: Bool = true) -> [Smoothie] {
if includingPaid {
return allSmoothies
}
logger.log("Free smoothies only")
return allSmoothies.filter { $0.hasFreeRecipe }
}
} Near-final DSL design
// DSL top-level design
static func all(includingPaid: Bool = true) -> [Smoothie] {
Smoothie(
// TODO: Change these parameters
id: "berry-blue",
title: "Berry Blue",
description: "Filling and refreshing, this smoothie will fill you with joy!",
measuredIngredients: [
Ingredient.orange.measured(with: .cups).scaled(by: 1.5),
Ingredient.blueberry.measured(with: .cups),
Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
]
)
Smoothie(…)
if includingPaid {
Smoothie(…)
Smoothie(…)
} else {
logger.log("Free smoothies only")
}
} Possible DSL description/ingredient designs (start)
// Possible DSL description/ingredient designs
Smoothie(
id: "berry-blue",
title: "Berry Blue",
description: "Filling and refreshing, this smoothie will fill you with joy!",
measuredIngredients: [
Ingredient.orange.measured(with: .cups).scaled(by: 1.5),
Ingredient.blueberry.measured(with: .cups),
Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
]
) Possible DSL description/ingredient designs (modifiers)
// Possible DSL description/ingredient designs
Smoothie(id: "berry-blue", title: "Berry Blue")
.description("Filling and refreshing, this smoothie will fill you with joy!")
.ingredient(Ingredient.orange.measured(with: .cups).scaled(by: 1.5))
.ingredient(Ingredient.blueberry.measured(with: .cups))
.ingredient(Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)) Possible DSL description/ingredient designs (all marker types)
// Possible DSL description/ingredient designs
Smoothie {
ID("berry-blue")
Title("Berry Blue")
Description("Filling and refreshing, this smoothie will fill you with joy!")
Recipe(
Ingredient.orange.measured(with: .cups).scaled(by: 1.5),
Ingredient.blueberry.measured(with: .cups),
Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
)
} Possible DSL description/ingredient designs (some marker types)
// Possible DSL description/ingredient designs
Smoothie(id: "berry-blue", title: "Berry Blue") {
Description("Filling and refreshing, this smoothie will fill you with joy!")
Recipe(
Ingredient.orange.measured(with: .cups).scaled(by: 1.5),
Ingredient.blueberry.measured(with: .cups),
Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
)
} Possible DSL description/ingredient designs (no marker types)
// Possible DSL description/ingredient designs
Smoothie(id: "berry-blue", title: "Berry Blue") {
"Filling and refreshing, this smoothie will fill you with joy!"
Ingredient.orange.measured(with: .cups).scaled(by: 1.5)
Ingredient.blueberry.measured(with: .cups)
Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
} Final DSL design
// DSL top-level design
static func all(includingPaid: Bool = true) -> [Smoothie] {
Smoothie(id: "berry-blue", title: "Berry Blue") {
"Filling and refreshing, this smoothie will fill you with joy!"
Ingredient.orange.measured(with: .cups).scaled(by: 1.5)
Ingredient.blueberry.measured(with: .cups)
Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
}
Smoothie(…) { … }
if includingPaid {
Smoothie(…) { … }
} else {
logger.log("Free smoothies only")
}
} Basic SmoothieArrayBuilder
enum SmoothieArrayBuilder {
static func buildBlock(_ components: Smoothie...) -> [Smoothie] {
return components
}
} How ‘buildBlock(…)’ works
// How ‘buildBlock(…)’ works
static func all(includingPaid: Bool = true) {
/* let v0 = */ Smoothie(id: "berry-blue", title: "Berry Blue") { … }
/* let v1 = */ Smoothie(id: "carrot-chops", title: "Carrot Chops") { … }
// …more smoothies…
/* return SmoothieArrayBuilder.buildBlock(v0, v1, …) */
} Basic SmoothieArrayBuilder
enum SmoothieArrayBuilder {
static func buildBlock(_ components: Smoothie...) -> [Smoothie] {
return components
}
} Smoothie initializer (incomplete)
extension Smoothie {
init(id: Smoothie.ID, title: String, /* FIXME */ _ makeIngredients: () -> (String, [MeasuredIngredient])) {
let (description, ingredients) = makeIngredients()
self.init(id: id, title: title, description: description, measuredIngredients: ingredients)
}
} SmoothieArrayBuilder with simple ‘if’ statements (incorrect)
enum SmoothieArrayBuilder {
static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] {
return component ?? []
}
static func buildBlock(_ components: Smoothie...) -> [Smoothie] {
return components
}
} How ‘if’ statements work with ‘buildOptional(_:)’
// How ‘if’ statements work with ‘buildOptional(_:)’
static func all(includingPaid: Bool = true) {
/* let v0 = */ Smoothie(id: "berry-blue", …) { … }
/* let v1 = */ Smoothie(id: "carrot-chops", …) { … }
/* let v2: [Smoothie] */
if includingPaid {
/* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … }
/* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … }
/* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1)
v2 = SmoothieArrayBuilder.buildOptional(v2_block) */
}
/* else {
v2 = SmoothieArrayBuilder.buildOptional(nil)
} */
/* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */
} SmoothieArrayBuilder with simple ‘if’ statements (incorrect)
enum SmoothieArrayBuilder {
static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] {
return component ?? []
}
static func buildBlock(_ components: Smoothie...) -> [Smoothie] {
return components
}
} Why didn’t our ‘buildOptional(_:)’ work?
// Why didn’t our ‘buildOptional(_:)’ work?
static func all(includingPaid: Bool = true) {
/* let v0 = */ Smoothie(id: "berry-blue", …) { … }
/* let v1 = */ Smoothie(id: "carrot-chops", …) { … }
/* let v2: [Smoothie] */
if includingPaid {
/* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … }
/* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … }
/* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1)
v2 = SmoothieArrayBuilder.buildOptional(v2_block) */
}
/* else {
v2 = SmoothieArrayBuilder.buildOptional(nil)
} */
/* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */
} SmoothieArrayBuilder with simple ‘if’ statements (still incorrect)
enum SmoothieArrayBuilder {
static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] {
return component ?? []
}
static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] {
return components.flatMap { $0 }
}
} Why didn’t our ‘buildOptional(_:)’ work?
// Why didn’t our ‘buildOptional(_:)’ work?
static func all(includingPaid: Bool = true) {
/* let v0 = */ Smoothie(id: "berry-blue", …) { … }
/* let v1 = */ Smoothie(id: "carrot-chops", …) { … }
/* let v2: [Smoothie] */
if includingPaid {
/* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … }
/* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … }
/* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1)
v2 = SmoothieArrayBuilder.buildOptional(v2_block) */
}
/* else {
v2 = SmoothieArrayBuilder.buildOptional(nil)
} */
/* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */
} The ‘buildExpression(_:)’ method
// The ‘buildExpression(_:)’ method
static func all(includingPaid: Bool = true) {
/* let v0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "berry-blue", …) { … } /* ) */
/* let v1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "carrot-chops", …) { … } /* ) */
/* let v2: [Smoothie] */
if includingPaid {
/* let v2_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "crazy-colada", …) { … } /* ) */
/* let v2_1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "hulking-lemonade", …) { … } /* ) */
/* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1)
v2 = SmoothieArrayBuilder.buildOptional(v2_block) */
}
/* else {
v2 = SmoothieArrayBuilder.buildOptional(nil)
} */
/* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */
} SmoothieArrayBuilder with simple ‘if’ statements (correct)
enum SmoothieArrayBuilder {
static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] {
return component ?? []
}
static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] {
return components.flatMap { $0 }
}
static func buildExpression(_ expression: Smoothie) -> [Smoothie] {
return [expression]
}
} SmoothieArrayBuilder with ‘if’-‘else’ statements
enum SmoothieArrayBuilder {
static func buildEither(first component: [Smoothie]) -> [Smoothie] {
return component
}
static func buildEither(second component: [Smoothie]) -> [Smoothie] {
return component
}
static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] {
return component ?? []
}
static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] {
return components.flatMap { $0 }
}
static func buildExpression(_ expression: Smoothie) -> [Smoothie] {
return [expression]
}
} How ‘if’-‘else’ statements work with ‘buildEither(…)’
// How ‘if’-‘else’ statements work with ‘buildEither(…)’
static func all(includingPaid: Bool = true) -> [Smoothie] {
/* let v0: [Smoothie] */
if includingPaid {
/* let v0_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { … } /* ) */
/* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0)
v0 = SmoothieArrayBuilder.buildEither(first: v0_block) */
}
else {
/* let v0_0 = SmoothieArrayBuilder.buildExpression( */ logger.log("Only got free smoothies!") /* ) */
/* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0)
v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */
}
/* return SmoothieArrayBuilder.buildBlock(v0) */
} How more complicated statements work with ‘buildEither(…)’
// How more complicated statements work with ‘buildEither(…)’
var v0: [Smoothie]
switch userRegion {
case .americas:
// ...smoothies omitted...
/* let v0_block = SmoothieArrayBuilder.buildBlock(...parameters omitted...)
v0 = SmoothieArrayBuilder.buildEither(first:
SmoothieArrayBuilder.buildEither(first: v0_block)) */
case .asiaPacific:
// ...smoothies omitted...
/* let v0_block = SmoothieArrayBuilder.buildBlock(…)
v0 = SmoothieArrayBuilder.buildEither(first:
SmoothieArrayBuilder.buildEither(second: v0_block)) */
case .eastAtlantic:
// ...smoothies omitted...
/* let v0_block = SmoothieArrayBuilder.buildBlock(…)
v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */
} SmoothieArrayBuilder with ‘if’-‘else’ statements
enum SmoothieArrayBuilder {
static func buildEither(first component: [Smoothie]) -> [Smoothie] {
return component
}
static func buildEither(second component: [Smoothie]) -> [Smoothie] {
return component
}
static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] {
return component ?? []
}
static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] {
return components.flatMap { $0 }
}
static func buildExpression(_ expression: Smoothie) -> [Smoothie] {
return [expression]
}
} How ‘if’-‘else’ statements work with ‘buildEither(…)’
// How ‘if’-‘else’ statements work with ‘buildEither(…)’
static func all(includingPaid: Bool = true) -> [Smoothie] {
/* let v0: [Smoothie] */
if includingPaid {
/* let v0_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { … } /* ) */
/* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0)
v0 = SmoothieArrayBuilder.buildEither(first: v0_block) */
}
else {
/* let v0_0 = SmoothieArrayBuilder.buildExpression( */ logger.log("Only got free smoothies!") /* ) */
/* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0)
v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */
}
/* return SmoothieArrayBuilder.buildBlock(v0) */
} SmoothieArrayBuilder with support for ‘Void’ results
enum SmoothieArrayBuilder {
static func buildEither(first component: [Smoothie]) -> [Smoothie] {
return component
}
static func buildEither(second component: [Smoothie]) -> [Smoothie] {
return component
}
static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] {
return component ?? []
}
static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] {
return components.flatMap { $0 }
}
static func buildExpression(_ expression: Smoothie) -> [Smoothie] {
return [expression]
}
static func buildExpression(_ expression: Void) -> [Smoothie] {
return []
}
} Modifier-style methods on Ingredient and MeasuredIngredient
extension Ingredient {
func measured(with unit: UnitVolume) -> MeasuredIngredient {
MeasuredIngredient(self, measurement: Measurement(value: 1, unit: unit))
}
}
extension MeasuredIngredient {
func scaled(by scale: Double) -> MeasuredIngredient {
return MeasuredIngredient(ingredient, measurement: measurement * scale)
}
} Closures and result builders
// Closures and result builders
static func all(includingPaid: Bool = true) -> [Smoothie] {
/* let v0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) {
"Filling and refreshing, this smoothie will fill you with joy!"
Ingredient.orange.measured(with: .cups).scaled(by: 1.5)
Ingredient.blueberry.measured(with: .cups)
Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
} /* ) */
/* let v1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) {
"Packed with vitamin A and C, Carrot Chops is a great way to start your day!"
Ingredient.orange.measured(with: .cups).scaled(by: 1.5)
Ingredient.carrot.measured(with: .cups).scaled(by: 0.5)
Ingredient.mango.measured(with: .cups).scaled(by: 0.5)
} /* ) */
/* return SmoothieArrayBuilder.buildBlock(v0, v1) */
} Smoothie initializer (final) and SmoothieBuilder (initial)
extension Smoothie {
init(id: Smoothie.ID, title: String, _ makeIngredients: () -> (String, [MeasuredIngredient])) {
let (description, ingredients) = makeIngredients()
self.init(id: id, title: title, description: description, measuredIngredients: ingredients)
}
}
enum SmoothieBuilder {
static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) {
return (description, components)
}
} Accepting different types
// Accepting different types
Smoothie(…) /* @SmoothieBuilder */ {
/* let v0 = */ "Filling and refreshing, this smoothie will fill you with joy!"
/* let v1 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5)
/* let v2 = */ Ingredient.blueberry.measured(with: .cups)
/* let v3 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
/* return SmoothieBuilder.buildBlock(v0, v1, v2, v3) */
} Smoothie initializer (final) and SmoothieBuilder (initial)
extension Smoothie {
init(id: Smoothie.ID, title: String, _ makeIngredients: () -> (String, [MeasuredIngredient])) {
let (description, ingredients) = makeIngredients()
self.init(id: id, title: title, description: description, measuredIngredients: ingredients)
}
}
enum SmoothieBuilder {
static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) {
return (description, components)
}
} SmoothieBuilder without the string
// SmoothieBuilder without the string
Smoothie(…) /* @SmoothieBuilder */ {
// "Filling and refreshing, this smoothie will fill you with joy!"
/* let v0 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5)
/* let v1 = */ Ingredient.blueberry.measured(with: .cups)
/* let v2 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
/* return SmoothieBuilder.buildBlock(v0, v1, v2) */
}
extension SmoothieBuilder {
static func buildBlock(_ description: String, _ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { … }
} How Swift improves diagnostics
// How Swift improves diagnostics
func fn0() throws {}
func fn1() rethrows {}
func fn2() {}
func fn3() deinit {}
func fn4() try {} SmoothieBuilder without the string
// SmoothieBuilder without the string
Smoothie(…) /* @SmoothieBuilder */ {
// "Filling and refreshing, this smoothie will fill you with joy!"
/* let v0 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5)
/* let v1 = */ Ingredient.blueberry.measured(with: .cups)
/* let v2 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)
/* return SmoothieBuilder.buildBlock(v0, v1, v2) */
}
extension SmoothieBuilder {
static func buildBlock(_ description: String, _ ingredients: ManagedIngredients...)
-> (String, [ManagedIngredients]) { … }
@available(*, unavailable, message: "missing ‘description’ field")
static func buildBlock(_ ingredients: ManagedIngredients...)
-> (String, [ManagedIngredients]) { fatalError() }
} Smoothie initializer (final) and SmoothieBuilder (with error handling)
extension Smoothie {
init(id: Smoothie.ID, title: String, _ makeIngredients: () -> (String, [MeasuredIngredient])) {
let (description, ingredients) = makeIngredients()
self.init(id: id, title: title, description: description, measuredIngredients: ingredients)
}
}
enum SmoothieBuilder {
static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) {
return (description, components)
}
@available(*, unavailable, message: "first statement of SmoothieBuilder must be its description String")
static func buildBlock(_ components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) {
fatalError()
}
} Resources
Related sessions
-
33 min -
41 min