Write a DSL in Swift using result builders
Swift & UI 高级 20m

用 Result Builder 在 Swift 中写 DSL

Write a DSL in Swift using result builders

2021年6月10日

在 Apple 官方观看视频

一句话判断

@resultBuilder(之前的 @functionBuilder)是 SwiftUI 的 ViewBuilder 背后的魔法——理解它之后你就能创建自己的 DSL,把复杂的数据构建逻辑隐藏在优雅的语法背后。

这场 Session 讲了什么

Session 深入讲解了 Swift 5.4 中正式引入的 @resultBuilder 特性。Result Builder 是一种编译器转换机制:你在一个标记了 @resultBuilder 的结构体中定义如何”累积”多个表达式,编译器会自动把你闭包中的多个语句转换成对累积方法的调用。

SwiftUI 的 @ViewBuilder 就是最典型的 result builder——你在 VStack 的闭包中写多个视图,编译器把它们转换成 buildBlock(view1, view2, view3) 的调用。

Session 用一个”图表构建 DSL”的例子,从零开始构建一个 result builder。覆盖了所有 builder 方法:buildBlockbuildOptionalbuildEitherbuildArraybuildExpressionbuildFinalResult

值得深挖的点

Result Builder 不是语法糖

它是一种编译器插件。当你写 @MyBuilder func makeThings() -> [Thing] 时,闭包中的每个表达式都被编译器改写。x 变成 buildExpression(x)if/else 变成 buildEither(first: ...)buildEither(second: ...),可选值变成 buildOptional(...)。这意味着你可以完全控制表达式的求值方式——你甚至可以在 buildExpression 中做类型转换、验证或收集错误。

错误收集模式

Result Builder 的一个巧妙用法是用 buildExpressionbuildBlock 收集所有错误而不是遇到第一个错误就停止。buildExpression 可以返回 Result<T, Error> 类型,buildBlock 可以累积所有错误到一个数组。这在构建配置文件解析器或表单验证 DSL 时非常有用——用户一次看到所有错误,而不是修一个再看下一个。

代码片段

构建一个图表 DSL

// 定义 Result Builder
@resultBuilder
struct ChartBuilder {
    // 处理单个表达式
    static func buildExpression(_ element: ChartElement) -> [ChartElement] {
        [element]
    }
    
    // 处理多个表达式的组合
    static func buildBlock(_ components: [ChartElement]...) -> [ChartElement] {
        components.flatMap { $0 }
    }
    
    // 处理 if 语句
    static func buildOptional(_ component: [ChartElement]?) -> [ChartElement] {
        component ?? []
    }
    
    // 处理 if/else 语句
    static func buildEither(first component: [ChartElement]) -> [ChartElement] {
        component
    }
    
    static func buildEither(second component: [ChartElement]) -> [ChartElement] {
        component
    }
}

// 使用 DSL
@ChartBuilder
func makeChart(showLegend: Bool) -> [ChartElement] {
    Bar(title: "一月", value: 42)
    Bar(title: "二月", value: 58)
    Line(points: [(0, 42), (1, 58)])
    if showLegend {
        Legend(position: .bottom)
    }
}

带错误收集的验证 DSL

@resultBuilder
struct ValidationBuilder {
    static func buildExpression(_ rule: ValidationRule) -> [ValidationError] {
        rule.validate()  // 返回错误数组,可能为空
    }
    
    static func buildBlock(_ errors: [ValidationError]...) -> [ValidationError] {
        errors.flatMap { $0 }
    }
    
    static func buildFinalResult(_ errors: [ValidationError]) -> ValidationResult {
        errors.isEmpty ? .valid : .invalid(errors)
    }
}

// 使用
@ValidationBuilder
func validateForm(name: String, email: String, age: Int) -> ValidationResult {
    NotEmpty(name, field: "姓名")
    IsValidEmail(email, field: "邮箱")
    GreaterThan(age, minimum: 18, field: "年龄")
}
// 一次返回所有验证错误

支持循环的 Result Builder

@resultBuilder
struct HTMLBuilder {
    static func buildExpression(_ text: String) -> String { text }
    
    static func buildExpression(_ node: HTMLNode) -> String { node.render() }
    
    static func buildBlock(_ components: String...) -> String {
        components.joined()
    }
    
    // 支持 for 循环
    static func buildArray(_ components: [String]) -> String {
        components.joined()
    }
}

// 使用
@HTMLBuilder
func makeList(items: [String]) -> String {
    "<ul>"
    for item in items {
        "<li>\(item)</li>"
    }
    "</ul>"
}

最佳实践

何时用 Result Builder: 当你有一个”累积多个表达式”的模式时。典型场景:UI 声明(HTML/SwiftUI)、配置文件定义、验证规则组合、测试断言组合。不要为了炫技而用——如果简单的数组字面量就能解决问题,不需要 builder。

API 设计: builder 方法应该处理所有需要的类型转换(通过 buildExpression 的重载)。对外暴露的 API 应该只有一个闭包参数标记了 @YourBuilder,其他参数提供上下文。

还有什么值得关注

  • Swift 5.5 的 Regex builder 也是基于 result builder 实现的。
  • buildPartialBlock 是 Swift 5.6 引入的新方法,让 builder 支持可变参数不需要为每个数量写重载。
  • Result Builder 可以和 async 结合——buildExpression 可以处理 async 表达式。
WWDC 2021