ResultBuilder, buildPartialBlock, reducing and buildArray

I'm trying to use the new syntax of resultBuilder to make something like a "ViewBuilder" but I'm having difficulty adding for loops.

I have the protocol. I have the aggregating type. I have a working result builder, all in the working playground below. link to github

I need for loops, so I'm trying to add functions like:

public static func buildArray<L:Layer>(_ components: [L]) -> some Layer {
        magicallyReduceToTupleStack(components)
 }

//or 
public static func buildArray(_ components: [any Layer]) -> some Layer {
        evenMoreMagicallyReduceToTupleStack(components)
 }

With the idea of being able to do something like:

let oneKind = Assembly {
   for _ in 0...3 {
     Circle()
   }
}

let multiKind = Assembly {
   for _ in 0...3 {
     Circle()
     Square()
   }
}

It's not going well. I feel like I must be missing something obvious because the resultBuilder can already take in a heterogeneous list of Layers via the two partialBuildBlocks. Can I write buildArray using them some how?

Any tips?

Full working playground:

import Foundation

public protocol Layer {
    associatedtype Content:Layer
    var content: Content { get }
}

//Indicate a leaf by conforming it to renderable.
public protocol RenderableLayer {
    var id:String { get }
    func render()
    typealias Content = Never
}

extension RenderableLayer {
    func render() {
        print("\(self.id)")
    }
}

public extension Layer where Content == Never {
    var content: Never { fatalError("This should never be called.") }
}

extension Never: Layer {
    public var id:String { "Never" }
    public typealias Content = Never
}


public extension Layer {
    func _render()  {
        if let bottom = self as? RenderableLayer {
            //print("Found a bottom \(id)")
            bottom.render()
        } else {
            //print("Not yet. \(id)")
            content._render()
        }
    }
}

@resultBuilder
public enum LayerBuilder {

    public static func buildPartialBlock<L: Layer>(first: L) -> some Layer {
        first
    }
    
    public static func buildPartialBlock<L0: Layer, L1: Layer>(accumulated: L0, next: L1) -> some Layer {
        Tuple2Layer(first: accumulated, second: next)
    }
}

struct Tuple2Layer<First:Layer, Second:Layer>: Layer, RenderableLayer {
    var id:String { "Tuple" }
    var first: First
    var second: Second
    
    init(first:First, second:Second) {
        self.first = first
        self.second = second
    }
    
    func render() {
        first._render()
        second._render()
    }
}

struct Assembly<Content:Layer>:Layer {
    var content: Content
    
    public init(@LayerBuilder content: () -> Content) {
        self.content = content()
    }
}

struct Circle:Layer, RenderableLayer {
    var id:String { "Circle" }
}

struct Square:Layer, RenderableLayer {
    var id:String { "Square" }
}

struct Triangle:Layer, RenderableLayer {
    var id:String { "Triangle" }
}

let insert = Assembly {
    Triangle()
    Triangle()
    Triangle()
}

let test = Assembly {
    Circle()
    Square()
    insert
    Circle()
    Circle()
}

print(test)
test._render()

Thanks!

It's impossible to use TupleStack for this purpose because the result type TupleStack<L, R> depends on the size of the array, which is unknown at compile time. You should create a new type like:

struct ArrayLayer<Element: Layer>: Layer {
  ...
  init(_ array: [Element]) {
    ...
  }
}

and use it in LayerBuilder:

public static func buildArray<L: Layer>(_ components: [L]) -> some Layer {
  ArrayLayer(components)        
}
1 Like

And that would be the obvious thing I was missing. THANK YOU. I'll do that then post the working.

New Problem

The below code gives the error: "Generic parameter 'τ_1_0' could not be inferred"

struct ArrayLayer<Element:Layer>:Layer, RenderableLayer  {
    var id: String { "ArrayLayer" }

    var elements: [Element]

    public init(from elements:[Element]) {
        self.elements = elements
    }

    func render() {
        for element in elements {
            element._render()
        }
    }
}

extension LayerBuilder {
    static func buildArray<L:Layer>(_ components: [L]) -> ArrayLayer<L> {
        ArrayLayer(from: components)
    }
}

let singularType = Assembly {
    for _ in 0...3 {
        Circle()
    }
}

//MARK: If with else
print(singularType)
singularType._render()

Anything jump out?

This ugly hunk of type erasing code gets the exact same error


struct Tuple2AnyLayer: Layer, RenderableLayer {
    var id:String { "Tuple" }
    var first: any Layer
    var second: any Layer
    
    init<L0:Layer, L1:Layer>(first:L0, second:L1) {
        self.first = first
        self.second = second
    }
    
    func render() {
        first._render()
        second._render()
    }
}

extension LayerBuilder {
    static func buildArray<L:Layer>(_ components: [L]) ->  some Layer {
        if components.count < 3 {
            fatalError("Gotta be more than 3 for now.")
        }
        let start = Tuple2AnyLayer(first:Tuple2AnyLayer(first: components[0], second: components[1]), second: components[2])
        let theRest = components.suffix(components.count-3)
        
        if theRest.isEmpty {
            return start
        }
        
        return theRest.reduce(start, {Tuple2AnyLayer(first: $0, second: $1)})
    }
}


let singularType = Assembly {
    Square()
    for _ in 0...3 {
        Circle() 
    }//ERROR HERE
    Square()
}

Related? - Improved Result Builder Implementation in Swift 5.8 - #15 by dispatchqueue

For giggles, confirmed that ArrayLayer works in the more conventional way (all this code continues to be in the gist.)

extension LayerBuilder {
    //These make an array work.
    static func buildExpression<L:Layer>(_ components: [L]) -> ArrayLayer<L> {
        ArrayLayer(from: components)
    }
    
    static func buildExpression<L:Layer>(_ component: L) -> some Layer {
        component
    }
}

func spewCircles() -> [Circle] {
    var array:[Circle] = []
    for _ in 0...3 {
        array.append(Circle())
    }
    return array
}

let singularTypeArray = Assembly {
    Square()
    //[Circle(), Circle(), Circle()]
    spewCircles()
    Square()
}

//MARK: Array
print(singularTypeArray)
singularTypeArray._render()

I tried it in playground and I figured out that it works if buildPartialBlock(first returns L instead of some Layer.

@resultBuilder
public enum LayerBuilder {
    
//    public static func buildExpression<L: Layer>(_ expression: L) -> L {
//        expression
//    }
//
    public static func buildPartialBlock<L: Layer>(first: L) -> L {
        first
    }

    public static func buildPartialBlock<L0: Layer, L1: Layer>(accumulated: L0, next: L1) -> some Layer {
        Tuple2Layer(first: accumulated, second: next)
    }
    
    public static func buildOptional<L: Layer>(_ component: L?) -> some Layer {
        ArrayLayer(from: component.map { [$0] } ?? [])
    }
    
    public static func buildEither(first component: some Layer) -> some Layer {
        ArrayLayer(from: [component])
    }
    
    public static func buildEither(second component: some Layer) -> some Layer {
        ArrayLayer(from: [component])
    }
    
    public static func buildLimitedAvailability(_ component: some Layer) -> some Layer {
        component
    }
    
    public static func buildArray<L: Layer>(_ components: [L]) -> some Layer {
        ArrayLayer(from: components)
    }
}

I think it's a compiler bug.

YAY! Confirmed! For Loop Works!

Hilariously, that code works for a for loop, which is my main concern, but doesn't actually work for an explicit array. And when adding the buildExpressions back in, it breaks the for loop. (the 'τ_1_0' error returns. )

I've added the tag "compiler" to the post. Was that the right thing to do? Do you recommend also opening a Feedback or a Github Issue?

Correct. It only calls buildArray for for loops; array literals are considered expressions and passed to a relevant overload of buildExpression.

To be clear, what exactly did you change? I'd think (without having tried it myself) you need two overloads for this to work:

public static func buildExpression<L: Layer>(_ expression: L) -> L {
  expression
}

public static func buildExpression<L: Layer>(_ expression: [L]) -> ArrayLayer<L> {
  ArrayLayer(from: expression)
}

I'm not sure which (if either) of these can be marked as returning some Layer.

Yes, of course (...I'll supply the change). I was referring back to the code I had posted before to get arrays to work. I'll admit to finding the names "buildArray" and "buildOptional" initially confusing since they are for implementing for loops and ifs, not Arrays and Optionals. I get it now, but that took me a minute.

Noted that I should use the parameter name "expression" not "components." Fixing in gist.

Yes, there is an opinion it's not a bug, and hmm I should check all my result builders with Swift 5.8 :frowning:.

FWIW: Thank you folks, as I now have almost parity with my Concrete-Type-Array based code!!! A refactor win, even if the road forward looks a bit tricky.

1 Like

On that note, I found your buildEither examples didn't work in my code, I had to keep the _EitherLayer type (seen in gist). But the buildOptional did and it saved me a _WrappedLayer type! Thanks!

I don't really know how to write a DSL that will work for my purposes without first class for-loops? I hope I'm just missing an implementation error and can get buildExpressions for Arrays to not break them again. I'd rather live without Arrays.

As I understand both buildEither methods must return the same type

   public static func buildEither<F: Layer, S: Layer>(first component: F) -> EitherLayer<F, S> {
        .first(component)
    }
    
    public static func buildEither<F: Layer, S: Layer>(second component: S) -> EitherLayer<F, S> {
        .second(component)
    }

maybe it will help you

Yup! That's what I did! Thank you for putting it in the record. (Also in the gist. Stole it from Apple)

Maybe it's ok for you to erase types to any Layer, I think this should work fine

@resultBuilder
public enum LayerBuilder {
    
    public static func buildExpression<L: Layer>(_ expression: L) -> any Layer {
        expression
    }
    
    public static func buildExpression(_ expression: any Layer) -> any Layer {
        expression
    }
    
    public static func buildBlock(_ components: any Layer...) -> any Layer {
        ArrayLayer(from: components)
    }
    
    public static func buildOptional(_ component: (any Layer)?) -> some Layer {
        ArrayLayer(from: component.map { [$0] } ?? [])
    }
    
    public static func buildEither(first component: any Layer) -> any Layer {
        component
    }
    
    public static func buildEither(second component: any Layer) -> any Layer {
        component
    }
    
    public static func buildLimitedAvailability(_ component: any Layer) -> any Layer {
        component
    }
    
    public static func buildArray(_ components: [any Layer]) -> any Layer {
        ArrayLayer(from: components)
    }
}

Mayham. Love it. :rofl:

I'll kick the wheels on that approach. My other route was actually entirely the other direction, locking down the allowed primitives in an enum instead of allowing anything that conforms to a protocol.

If the idea would eventually be able to do a very large Stage, I'm worried about all the boxing/unboxing I'd have to do, but it's possible.

I have added a heterogeneous "for-loop" proof of concept.


//Heterogeneous Loop

struct Repeating<Content:Layer>:Layer, RenderableLayer  {
    var id: String { "Repeating" }
    //var count:Int

    var elements: [Content]

    public init(count:Int, @LayerBuilder content: (Int) -> Content) {
        self.elements = []
        for index in 0..<count {
            self.elements.append(content(index))
        }
    }

    func render() {
        for element in elements {
            element._render()
        }
    }
}

let multiType = Assembly {
    Square()
    Repeating(count: 3) { index in
        let adjusted = index+2
        CircleWithParam(radius: adjusted*3)
        SquareWithParam(side: adjusted*2)
    }
    Circle()
}

//MARK: Loop Attempt
print(multiType)
multiType._render()
1 Like

Found it!

adding

  static func buildExpression<L:Layer>(_ expression: L) -> some Layer {
       expression
  }

is what broke the for loop.

Removing it will turn EVERYTHING into an ArrayLayer/WrappedOptional if I want to allow optionals in the result builder.

Better choice change -> some Layer to L