Constructing SwiftUI-like conditional return with parameter packs

I'm trying to construct a recursive, strongly-typed-as-in-SwiftUI type from a parameter pack, without resorting to type erasers.

I'm working with a resultBuilder that has the following functions supported:

static func buildBlock<T>(_ components: T) -> T where T: Foo
static func buildEither<T, U>(first component: T) -> EitherFoo<T, U> where T: Foo, U: Foo
static func buildEither<T, U>(second component: U) -> EitherFoo<T, U>  where T: Foo, U: Foo

It does not support a parameter pack buildBlock by design, the intention is that you can return one instance from it, no more, no less.

There is no AnyFoo, and a good solution to this problem won't involve adding it.

I have a struct FooCollection declared as follows.

struct FooCollection<each T>: Foo where T: Foo {

    /* 
      initializer elided, but this is intended to be initialized with like the following: 

      FooCollection {
            MyFooProducer()
            MyBarProducer()
      }
      
      and the "producers" create a dependency for an associated `Foo` later. There is never more than one created at once.
      */
    
     var members: (repeat Optional<each T>)
    
     // note; `content` is a requirement of the `Foo` protocol, and `FooCollection` is itself a subtype of `Foo`.
     var content: some Foo {
         // ??
     }
}

My problem is I cannot figure out how to fill in body. Pack Iteration doesn't seem like the answer because I can't guarantee that there are 1 or more members in each T, and recursion doesn't work for the same reason; I can't find a way to destructure the pack in a way that moves me closer to the goal either (I thought there was a way to do this in the past, but can't figure out how it would ever have worked).

I'm leaning toward this not being possible b/c I can't even figure out how I would spell such a type if opaque types didn't exist, but thought I'd ask in case anyone has any ideas.

1 Like

What should the magic impl of content do (assuming its possible)? I think this might be too abstract to follow, at least for me :sweat_smile:

What would content want to return in this case? The content of all the present members? Are members the producer?

What would content want to return in this case

Probably easier to explain this by showing what it would look like with a concrete set of types:

struct FooCollection: Foo {
         
     var members: (Foo?, Bar?, Baz?)
    
     @FooBuilder      
     var content: some Foo {
         if let foo = members.0 {
              foo
         } else if let bar = members.1 {
              bar
         } else if let baz = members.2 {
               baz
         } else {
               fatalError() // unreachable, for reasons
         }
     }
}

So reframing the question: how can I write this, but using a parameter pack so the arity of the tuple isn't hard coded?

This passes the front-end but crashes during codegen:

protocol Foo {
    associatedtype Content: Foo
    var content: Content { get }
}

struct FooCollection<each T: Foo>: Foo {
         
     var members: (repeat (each T)?)
    
     var content: some Foo {
        for member in repeat each members {
            if let member {
                return member.content
            }
        }
        fatalError()
     }
}

I think it shouldn't compile, because what would the some Foo return type be inferred as? Each member has a different type to witness that, and you're going to have to deal with that manually.

What should happen? You said you don't want AnyFoo; do you want to constrain FooCollection to cases where each member has the same content type?

1 Like

Do you even want packs in that case? I guess for compile time counts...

SwiftUI deals with this by having a hidden requirement which doesn't involve View.

For example, you could have

enum Primitive {
    case basic
}

protocol Foo {
    associatedtype Content: Foo
    var content: Content { get }
    func _pushPrimitives(into: inout [Primitive])
}

extension Foo {
    func _pushPrimitives(into list: inout [Primitive]) {
        content._pushPrimitives(into: &list)
    }
}

extension Never: Foo {
    var content: Never { fatalError() }
    func _pushPrimitives(into: inout [Primitive]) {
        fatalError()
    }
}

struct Basic: Foo {
    var content: Never { fatalError() }
    func _pushPrimitives(into list: inout [Primitive]) {
        list.append(.basic)
    }
}

struct UserDefined: Foo {
    /* @FooBuilder */
    var content: some Foo {
        Basic()
    }
}

struct FooCollection<each T: Foo>: Foo {
         
    var members: (repeat (each T)?)

    var content: Never {
        fatalError()
    }

    func _pushPrimitives(into list: inout [Primitive]) {
        for member in repeat each members {
            if let member {
                member._pushPrimitives(into: &list)
            }
        }
    }

}
1 Like

I guess for compile time counts...

Yeah this was the main motivation.

I realized after I asked this that we can solve this without packs, by having our ViewBuilder output a recursive struct instead of packs. A FooPair<T, U> maps really well to the end result EitherFoo. This is only viable b/c the API we're writing is similar to SwiftUI in that nothing but the framework interacts with the output of content afterwards and the FooCollection isn't stored, so we don't have to write out the potentially extremely long type that gets produced. So I'm still somewhat interested in seeing a parameter pack solution for this, though it's more academic at this point.

Yeah I'm aware of GraphOutputs, but I don't see how it's applicable in this case. I want to transform a flat list of types (a parameter pack) into a nested, recursive type, and this only seems relevant to building a SwiftUI-like system from scratch.

Yeah this is a different type every time. Solutions I've tried for this in the past start from the knowledge that you can call SwiftUI ViewBuilder directly, so instead of the for-repeat-each I wrote ViewBuilder.buildBlock(repeat each member) . And from that I was anticipating that I would be calling my own DSL's members directly as well, it was just a question of how to accumulate them in a var in a way that the compiler would understand.

I think if you want better suggestions from us, you're going to have to provide a bunch more context :)