Result Builder to support for-loops

Overview:

I am trying to use @resultBuilder, however I have run into an issue while trying to support for loops

Problem:

I get the following compilation error at the line of of the for loop:
error: ResultBuilder.playground:29:5: error: cannot pass array of type '[String]' as variadic arguments of type 'String'

Xcode's auto-completion Signature of buildArray

Using Xcode's auto-completion the buildArray's signature is as follows

static func buildArray(_ components: [[String]]) -> [String] {
}

Fix (not sure it is the right way):

Instead of using Xcode's autocompletion signature I have defined the buildArray as follows:

static func buildArray(_ components: [[String]]) -> String { 
 //some logic to return string
}

Questions:

  1. Is the auto-completion signature wrong?
  2. Can buildArray be of any signature or it needs to be based on buildBlock?
  3. Is my Fix acceptable?

Note: Apple Swift version 5.4

Code:

@resultBuilder
struct List {
    static func buildBlock(_ components: String...) -> [String] {
        components
    }
    
    static func buildArray(_ components: [[String]]) -> [String] {
        components.map { $0.joined(separator: "\n") }
    }
}

func numberedList(@List content: () -> [String]) -> [String] {
    content()
}

func buildList(from strings: [String]) -> String {
    strings.enumerated().map { "\($0.offset). \($0.element)" }.joined(separator: "\n")
}

let x = numberedList {
    "aaaa"
    "bbbb"
    "cccc"

//Following for loop causes a compilation error: cannot pass array of type '[String]' as variadic arguments of type 'String'
    for _ in 1...10 {
        "qqqq"
        "aaaa"
    }
}

print(buildList(from: x))

The Component in your builder is not String, it's actually [String], so the buildBlock should actually accept components: [String]..., and to translate a single String you need buildExpression.

Here's the full builder:

@resultBuilder
struct List {
    static func buildBlock(_ components: [String]...) -> [String] {
        Array(components.joined())
    }

    static func buildExpression(_ expression: String) -> [String] {
        [expression]
    }
    
    static func buildArray(_ components: [[String]]) -> [String] {
        Array(components.joined())
    }
}
1 Like

@ExFalsoQuodlibet Thanks a ton!!

Your code works great, but I have some doubts.

Doubts:

  1. I thought the components was String.... Is there a reason why you it is [String]...?

Without the for loop part, it seemed like the component was String...

    "aaaa"
    "bbbb"
    "cccc"
  1. In your code you are converting every String to [String]. Just wondering if that was necessary, was my fix valid?

My fix was:

static func buildArray(_ components: [[String]]) -> String { 
 //some logic to return string
}

The Component needs to support the results from:

  • buildExpression (default to Expression type),
  • buildEither (for if-else block),
  • buildBlock (for do block), and
  • buildArray (for for-in block), etc.

In so far, String can only potentially support the result from buildExpression. If you'd like to use [String] for other results, you should use [String] (or enum if you want single-String to remain single).


That said, it's possible to support multiple Component type, which may be useful to restrict the order of the component appearances.

func buildBlock(_: Header, _: Body, _: Footer) -> FinalResult { ... }

...

// You need to do
{
  Header
  Body
  Footer
}

Though I don't think it's very useful in this case.

1 Like

@Lantua Thanks a lot!!

Still learning this, based on my crude understanding (I could be wrong), the value returned by buildArray needs to be Component type. Component is defined by buildBlock

In my case buildBlock was as follows:

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

So Component is String

Then buildArray would need to return Component, in this case String

static func buildArray(_ components: [[String]]) -> String { 
 //some logic to return string
}

So the entire buildArray needs to build down to a single Component, so the buildBlock can process it

The definition of buildBlock is:

static func buildBlock(_ components: Component...) -> Component { ... }

It takes a variadic list of Component instances, and return a single Component instance.

So in your case it must be either

static func buildBlock(_ components: String...) -> String { ... }

or

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

In your case it's probably easier to use [String] as Component. If you want to return directly a String from you builder function, you can add:

static func buildFinalResult(_ component: [String]) -> String { ... }
1 Like

Personally, I wouldn't use the term Component as though each builder has exactly one. It's quite possible to have many (looking at you, ViewBuilder).

In any case, if you have: buildArray(_: [C1]) -> C2, you'd need at least:

  • buildBlock(...) -> C1, for block inside for-loop,
  • buildBlock(..., _: C2, ...) -> C3 for block containing for-loop.

And it's far easier if C1, C2, and C3 are all [String], which would be your "Component" type. In that case, you can also just use buildBlock(_: [String]...) -> [String] for both buildBlock.

1 Like

@ExFalsoQuodlibet @Lantua Thank a lot!!!

I think I am beginning to understand a little better.
Now I understand why they used protocol in the Advanced Operators — The Swift Programming Language (Swift 5.7) it does give some flexibility with protocol as they could be different types.

Thanks a ton!! saved my day ... am sure I will have more questions as I start to use them more :slight_smile:

Consider also that if you have

static func buildArray(_ components: [[String]]) -> String { 
 //some logic to return string
}

then the entire for loop would be rendered down to a single String, which is not necessarily what you'd want. In general I would advise to initially design builders with a single Component in mind, and think about how to support it in all builders functions, adding buildExpressions when some specific expression must be resolved into a Component instance. Then you can expand the definition of Component as @Lantua mentioned, if you need special behavior, like specific ordering of elements in the builder function, or different behavior based of which "components" are merged in buildBlock.

1 Like

I think that is a good approach, boiling it down to a string I was finding it difficult to build the logic to achieve what I wanted. Thanks a lot!!