Stateful Result Builders

After having written a few Property Wrappers and Result Builders for different purposes throughout my codebase, there's one thing that keeps jarring me about the latter: lack of parameters.

Whilst a Property Wrapper supports parameters other than wrappedValue and projectedValue in its implementation, a Result Builder consists only of static functions that specify how the received values are to be mutated and added on towards the final result, with no space for customization or extra parameters.


For example, take this StringBuilder (source) that joins up multiple Strings into one:

@resultBuilder
struct StringBuilder {   
    static func buildBlock(_ components: String...) -> String {
        return components.joined(separator: "⭐️")
    }
}

As it stands, there's no chance for customizing what the separator for the joined string should be. We can only hardcode "⭐️", "", " " or anything in between.

However, if buildBlock and all other build functions were non-static, this issue could be elegantly solved as follows:

@resultBuilder
struct StringBuilder {
    var separator: String // could even default to something (e.g. = "⭐️")

    func buildBlock(_ components: String...) -> [String] {
        return components
    }

    func buildFinalResult(_ components: [String]) -> String {
        return components.joined(separator: separator)
    }
}

[...]
@StringBuilder(separator: " ")
var myString: String {
    "Hello"
    "World"
}

// "Hello World"
[...]

I believe this to be a shortcoming of Result Builders that could be overcome with relative ease, if its functions were to become non-static. It also seems rational that Result Builders would allow for it since their syntax and declaration seemingly mirrors that of Property Wrappers.

Upon reading the accepted proposal, I realize that this is a potential future direction included in it and I'm glad to see it's somewhat within scope, so I wanted to open this thread to discuss this idea further and get an impression of what the community's feeling is on the matter (now that the feature has been publicly available for a few months).

Is this something you've come across while writing Result Builders? Do you think it's a shortcoming worth addressing, or otherwise intended as is?

6 Likes

I like this idea! One could easily imagine this in frameworks like SwiftUI which heavily rely on Result Builders, for example @ViewBuilder(.lazy) (even though this would most likely not be possible).

I definitely think this is something we should look into – giving the library and framework designers (who will be the main creators of Result Builders) more choices is always a good idea.

This is something I bumped against recently as well.

I would prefer to deal with the parameters in the ResultBuilder itself but end up using them in the method that uses the ResultBuilder (an init for example)

1 Like

Exactly.

A dirty workaround I came up with is leveraging generics so the result builder can be given parameters statically through its type:

protocol SeparatorProtocol {
    static var separator: String { get }
}

struct WhitespaceSeparator: SeparatorProtocol {
    static var separator: String { " " }
}

@resultBuilder
struct StringBuilder<S: SeparatorProtocol> {   
    static func buildBlock(_ components: String...) -> String {
        return components.joined(separator: S.separator)
    }
}

[...]
@StringBuilder<WhitespaceSeparator>
var myString: String {
    "Hello"
    "World"
}

print(myString) // "Hello World"
[...]

It works, but the amount of boilerplate makes it quite impractical. Not to mention soulcrushingly hacky.

7 Likes
Terms of Service

Privacy Policy

Cookie Policy