Result builder attributes in types

Right now, result builder attributes can be added to protocol requirements, properties, and functions. This pitch allows result builder attributes to be added to types. For example, a hypothetical HTML type that comes with an HTMLBuilder result builder can be annotated like so:

@HTMLBuilder struct HTML { ... }

or in an extension:

@HTMLBuilder extension HTML { ... }

A result builder attribute on a type does the following:

  • Closure syntax (including trailing closure syntax) can be used to construct a value of the type directly, using the result builder. For example:
    // Using closure syntax to construct a value
    let someHTML: HTML = {
        ...
    }
     
    // declaring a function that takes a result builder-annotated type
    func someHTMLFunc(html: HTML) {
        ...
    }
     
    // a function call without using a result builder
    someHTMLFunc(html: someHTML)
    
    // a function call using trailing closure syntax
    someHTMLFunc {
       ...
    }
    
  • Result builder syntax is inferred for functions (and computed properties) that return the annotated type, when the function would otherwise not type-check: if it contains multiple statements without a return statement, for example. For example:
    func makeSomeHTML() -> HTML {
        ... // result builder syntax
    }
    

The motivation for this is to solve some ergonomic issues with result builders that currently exist.

One problem is that users are often forced to add result builder attributes themselves. For example, let's say a user wants to make a function that returns some HTML. They try to make a function, but are also forced to learn about HTMLBuilder to use the convenient HTML syntax:

@HTMLBuilder func makeHTML() -> HTML {
    ...
}

A workaround is to add an initializer to allow people to use result builder syntax, but this adds an unnecessary level of nesting:

func makeHTML() -> HTML {
    HTML {
        ...
    }
}

Another problem is that functions are often forced to use unnatural type signatures. Library authors often make use of trailing closures to allow for convenient syntax using result builders. For example, we might want to add a link(to:label:) function to let users form hyperlinks like so:

func link(to: URL, label: @HTMLBuilder () -> HTML) -> HTML {
    ...
}

let html = HTML {
    link(to: "https://www.example.com") {
        span {
            "my link label"
        }
    }
}

Currently, the link function is forced to use a function type for its argument, even though conceptually, it just takes a label of type HTML by value. This comes with some drawbacks:

  • Users are forced to think about result builders to make their own functions that take HTML arguments with convenient syntax. The function signature can be cumbersome to write, especially when writing multiple of these functions.
  • The function cannot be called with a variable, literal, or other expression without it being wrapped in a closure (without overloads). For example, we might want the HTML type to conform to ExpressibleByStringLiteral for convenience, so the user can use a string literal instead:
    link(to: "https://www.example.com", label: "my link")
    

With a result builder attribute on the HTML type, the function signature becomes easier to understand:

func link(to url: URL, label: HTML) -> HTML

Result builder attributes on types can also potentially play nicely with if and switch expressions. For example, result builder syntax can be inferred for if and switch expressions where the "arms" contain multiple statements or don't evaluate to the same type:

let html: HTML = if (condition) {
    link(to: "https://www.example.com", label: "my link")
    link(to: "https://www.example.com", label: "my second link")
} else {
    link(to: "https://www.example.com", label: "my third link")
}

Thoughts on this pitch? Feedback is appreciated.