Is anyone else copy-pasting the same @resultBuilder methods everywhere?

i feel as if i am always copy-and-pasting the same “DefaultArrayBuilder” methods:

{
    static 
    func buildExpression(_ element:Element) -> [Element]
    {
        [element]
    }
    static 
    func buildExpression(_ elements:[Element]) -> [Element]
    {
        elements
    }
    static 
    func buildExpression<S>(_ elements:S) -> [Element]
        where S:Sequence, S.Element == Element 
    {
        [Element].init(elements)
    }
    static 
    func buildBlock(_ elements:[Element]...) -> [Element]
    {
        elements.flatMap{ $0 }
    }
    static 
    func buildArray(_ elements:[[Element]]) -> [Element]
    {
        elements.flatMap{ $0 }
    }
    static 
    func buildOptional(_ element:[Element]?) -> [Element]
    {
        element ?? []
    }
    static 
    func buildEither(first:[Element]) -> [Element]
    {
        first
    }
    static 
    func buildEither(second:[Element]) -> [Element]
    {
        second
    }
}

within a single project, these usually end up evolving into an ArrayBuilder protocol with an Element associatedtype that the actual result builders conform to. but it still gets duplicated across projects. has anyone else had the same experience?

5 Likes

Here's what I do:

protocol ArrayBuilder
{
  associatedtype Element
}

extension ArrayBuilder
{
  // all those buildXxx methods
}

@resultBuilder
struct ThingBuilder: ArrayBuilder
{
  typealias Element = Thing

  // any extra stuff
}
3 Likes

this is exactly what i do. it’s become a pattern

3 Likes

I also had to duplicate this code multiple times in different projects. Could we maybe put a DefaultArrayBuilder protocol into the standard library? I think that would be very useful.

Maybe it could be a bit more flexible than @David_Catmull's solution. I'm thinking about e.g. having an associated type for the expression and the final result.

The protocol could look somewhat like this:

protocol DefaultArrayBuilder {
    associatedtype Element
    
    associatedtype Expression = Element
    associatedtype FinalResult = [Element]
    
    static func buildExpression(_ element: Expression) -> [Element]
    static func buildFinalResult(_ elements: [Element]) -> FinalResult
}

extension DefaultArrayBuilder {
    static func buildBlock(_ elements: [Element]...) -> [Element] {
        elements.flatMap { $0 }
    }
    
    static func buildArray(_ elements: [[Element]]) -> [Element] {
        elements.flatMap { $0 }
    }
    
    static func buildOptional(_ element: [Element]?) -> [Element] {
        element ?? []
    }
    
    static func buildEither(first: [Element]) -> [Element] {
        first
    }
    
    static func buildEither(second: [Element]) -> [Element] {
        second
    }
    
    static func buildLimitedAvailability(_ elements: [Element]) -> [Element] {
        elements
    }
}

extension DefaultArrayBuilder where Expression == Element {
    static func buildExpression(_ expression: Expression) -> [Element] {
        [expression]
    }
}

extension DefaultArrayBuilder where FinalResult == [Element] {
    static func buildFinalResult(_ elements: [Element]) -> FinalResult {
        elements
    }
}

Having this protocol in the standard library could also help with discoverability of the result builder functions. It's nice that SourceKit is autocompleting them now, but having a simple example implementation would definitely be helpful as well.

2 Likes

I have encountered this too, I feel like an array builder like this would be a welcome addition to the std lib.

My solution was to make the builder itself generic, so instead of

ThingBuilder: DefaultArrayBuilder { 
    typealias Element == Thing
}

I did this, once:

ArrayBuilder<Element> {
    associatedtype Expression = Element
    associatedtype FinalResult = [Element]
    // etc.
}

and I usually add an Array "initializer" for good measure.

extension Array {
    static func build(@ArrayBuilder<Self.Element> build: () -> Self) -> Self {
        return build()
    }
}

i used to do that, but i found that i often wanted to customize the buildFinalResult methods, so gradually started doing the same thing @David_Catmull does.

2 Likes

Happened here too. We have typeable resultbuilders for array, set and dict, but it is the array one that is most of the time is used.
It would make sense to see this being part of the standard library.

3 Likes