Generic function specialization for recursive protocols trying to chose wrong function

I'm trying to build a view builder abstraction to match types of view builders and nested data structures (or tree like structures where each level of tree can have different type of value). I've defined two protocols for view builders where one has no children and the other one is extending the first protocol with adding a child builder associated type where child type is constrained by view builder protocol (recursion), and child builder's data type is constrained to parent builder's item's child type. But when I create a generic build function where there are two specializations for the one with child and the regular one, it tries to chose the wrong one and doesn't compile.

UPDATE: Typo fixed and it builds, but the build function still choses build<VB: ViewBuilderType> over build<VB: ViewBuilderParentType>

Here is the sample minimal code:

protocol TreeType {
    associatedtype Child
    var children: [Child] { get }
}

protocol ViewBuilderType {
    associatedtype Item
    func build(_ item: Item) -> String
}

protocol ViewBuilderParentType: ViewBuilderType where Item: TreeType {
    associatedtype ChildBuilder: ViewBuilderType where ChildBuilder.Item == Item.Child
    var childBuilder: ChildBuilder { get }
}

func build<VB: ViewBuilderParentType>(_ builder: VB, _ data: VB.Item, indentation: Int = 0) -> String {
    let indentationPad = String(repeating: " ", count: indentation * 4)
    var output = "\(indentationPad)\(builder.build(data))\n"
    for child in data.children {
        output += build(builder.childBuilder, child, indentation: indentation + 1) + "\n"
    }
    
    return output
}

func build<VB: ViewBuilderType>(_ builder: VB, _ data: VB.Item, indentation: Int = 0) -> String {
    let indentationPad = String(repeating: " ", count: indentation * 4)
    return "\(indentationPad)\(builder.build(data))"
}

Example Usage:

struct ViewBuilderWithChild<Parent: ViewBuilderType, Child: ViewBuilderType>: ViewBuilderParentType where
    Parent.Item: TreeType,
    Child.Item == Parent.Item.Child
{
    let parent: Parent
    let childBuilder: Child
    
    func build(_ item: Parent.Item) -> String {
        parent.build(item)
    }
}

extension ViewBuilderType {
    func withChild<ChildBuilder: ViewBuilderType>(_ childBuilder: ChildBuilder) -> ViewBuilderWithChild<Self, ChildBuilder> {
        ViewBuilderWithChild(parent: self, childBuilder: childBuilder)
    }
}

let builder =
SectionViewBuilder().withChild(
    SectionViewBuilder().withChild(
        IntegersViewBuilder()
    )
)

let data = Section(
    title: "Parent Section",
    children: [
        Section(title: "Int Section 1", children: [1, 2, 3]),
        Section(title: "Int Section 2", children: [3, 4, 5])
    ])

let output = build(builder, data)
print(output)

In this example function specialization doesn't detect second level of ViewBuilderParentType

You have a typo, and because of that they are two unrelated functions.
In one function you have indentation, and indendation in the other. You're calling the first one.

I've fixed the typo, but my problem continues. It still choses most generic type over more specialized one for the inner types. I think this is because the ViewBuilderParentType doesn't enforce the childViewBuilder to be ViewBuilderParentType also (because there is no way to exit that type recursion) but at compile time child types known to implement ViewBuilderParentType

As a workaround, if I keep adding copies of the build function with more constrains on the generic type it choses the right implementation, but then I have to keep copy pasting the same code again and again. Here is an example to support 3 levels:

func build<VB: ViewBuilderType>(_ builder: VB, _ data: VB.Item, indentation: Int = 0) -> String {
    let indentationPad = String(repeating: " ", count: indentation * 4)
    return "\(indentationPad)\(builder.build(data))"
}

func build<VB: ViewBuilderParentType>(_ builder: VB, _ data: VB.Item, indentation: Int = 0) -> String {
    let indentationPad = String(repeating: " ", count: indentation * 4)
    var output = "\(indentationPad)\(builder.build(data))\n"
    for child in data.children {
        output += build(builder.childBuilder, child, indentation: indentation + 1) + "\n"
    }
    
    return output
}

func build<VB: ViewBuilderParentType>(_ builder: VB, _ data: VB.Item, indentation: Int = 0) -> String
where
    VB.ChildBuilder: ViewBuilderParentType
{
    let indentationPad = String(repeating: " ", count: indentation * 4)
    var output = "\(indentationPad)\(builder.build(data))\n"
    for child in data.children {
        output += build(builder.childBuilder, child, indentation: indentation + 1) + "\n"
    }
    
    return output
}

func build<VB: ViewBuilderParentType>(_ builder: VB, _ data: VB.Item, indentation: Int = 0) -> String
where
    VB.ChildBuilder: ViewBuilderParentType,
    VB.ChildBuilder.ChildBuilder: ViewBuilderParentType
{
    let indentationPad = String(repeating: " ", count: indentation * 4)
    var output = "\(indentationPad)\(builder.build(data))\n"
    for child in data.children {
        output += build(builder.childBuilder, child, indentation: indentation + 1) + "\n"
    }
    
    return output
}

That's a lot of code, and your example usage doesn't contain all the things needed for it to compile. I think I understand what you want though

protocol GeneralProtocol {
    associatedtype Item
    var item: Item { get }
}
protocol SpecificProtocol: GeneralProtocol where Item: GeneralProtocol { }
func f<T: GeneralProtocol>(_ t: T) {
    print("A")
}
func f<T: SpecificProtocol>(_ t: T) {
    print("B")
    f(t.item)
}
struct Foo: SpecificProtocol {
    var item: Foo { self }
}
f(Foo()) // prints B A

You would like for it to print an endless stream of "B", instead of "B A", is that the case?

If so, the compiler would have to choose at runtime which of the f functions it needs to execute. It's called dynamic dispatch.
You can get dynamic dispatch in a few ways in swift:

  • literally writing if let x = t.item as? SpecificProtocol { f(x) } else { f(t.item) }
    This doesn't work in your case, because of associated types
  • methods with overrides
  • simply having a closure as a variable you can assign different values
  • protocol requirements

Here's how I would solve that using protocol requirements

protocol GeneralProtocol {
    associatedtype Item
    var item: Item { get }
    func executeF()
}
extension GeneralProtocol {
    func executeF() {
        f(self)
    }
}
protocol SpecificProtocol: GeneralProtocol where Item: GeneralProtocol { }
extension SpecificProtocol {
    func executeF() {
        f(self)
    }
}
func f<T: GeneralProtocol>(_ t: T) {
    print("A")
}
func f<T: SpecificProtocol>(_ t: T) {
    print("B")
    t.item.executeF()
}
struct Foo: SpecificProtocol {
    var item: Foo { self }
}
f(Foo()) // prints B B B B...
2 Likes

Oh, I see. I think I got it, because the f is called from the SpecialProtocol itself, compiler choses the right specialization again, because the self guaranteed to conform SpecificProtocol. Although this requires me to recurse through the protocol itself I think I can get my head around this. Thank you very much this was very eye opening.

1 Like