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))"
}
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...
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.