and when written this way, it would call the matching version and do not have the "Value of protocol type 'CustomStringConvertible' cannot conform to 'CustomStringConvertible'" problem.
So, why are the String.init(describing:) written with generic? Why not just protocol type as parameter type like above? So passing protocol parameter calls the specific version.
With the way String.init(describing:)'s are written now, when you only have a protocol type value, it will call the most general and cannot call the specific one.
My playground:
//: [Previous](@previous)
extension String {
// declaring this way with generic, protocol parameter match to the 4th call
init<Subject: TextOutputStreamable>(xxx instance: Subject) {
print("xxx11111")
self.init(describing: instance)
}
init<Subject: CustomStringConvertible>(xxx instance: Subject) {
print("xxx2222")
self.init(describing: instance)
}
init<Subject: CustomStringConvertible & TextOutputStreamable>(xxx instance: Subject) {
print("xxx3333")
self.init(describing: instance)
}
init<Subject>(xxx instance: Subject) {
print("xxx4444")
self.init(describing: instance)
}
// but declaring this way, protocol is match to exact protocol param type call!
init(yyy instance: TextOutputStreamable) {
print("yyy11111")
self.init(describing: instance)
}
init(yyy instance: CustomStringConvertible) {
print("yyy2222")
self.init(describing: instance)
}
init(yyy instance: CustomStringConvertible & TextOutputStreamable) {
print("yyy3333")
self.init(describing: instance)
}
init<Subject>(yyy instance: Subject) {
print("yyy4444")
self.init(describing: instance)
}
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ instance: TextOutputStreamable?, default defaultValue: @autoclosure () -> String) {
print("-a1-")
// this only match to 4th generic one
appendLiteral(instance.map(String.init(xxx:)) ?? defaultValue())
// but this match to specific one
appendLiteral(instance.map(String.init(yyy:)) ?? defaultValue())
}
mutating func appendInterpolation(_ instance: CustomStringConvertible?, default defaultValue: @autoclosure () -> String) {
print("-a2-")
appendLiteral(instance.map(String.init(xxx:)) ?? defaultValue())
appendLiteral(instance.map(String.init(yyy:)) ?? defaultValue())
}
mutating func appendInterpolation(_ instance: (CustomStringConvertible & TextOutputStreamable)?, default defaultValue: @autoclosure () -> String) {
print("-a3-")
appendLiteral(instance.map(String.init(xxx:)) ?? defaultValue())
appendLiteral(instance.map(String.init(yyy:)) ?? defaultValue())
}
mutating func appendInterpolation<Subject>(_ instance: Subject?, default defaultValue: @autoclosure () -> String) {
print("-a4-")
appendLiteral(instance.map(String.init(xxx:)) ?? defaultValue())
appendLiteral(instance.map(String.init(yyy:)) ?? defaultValue())
}
}
struct Foo: CustomStringConvertible, TextOutputStreamable {
func write<Target>(to target: inout Target) where Target : TextOutputStream {
target.write("Text Output Streamable")
}
var description: String { "Custom String Convertible" }
}
print("Interpolation", "let s1 = Foo() as CustomStringConvertible")
let s1 = Foo() as CustomStringConvertible
print("\(s1, default: "s1 is nil")")
// print out:
//-a2-
//xxx4444
//yyy2222
//Text Output StreamableText Output Streamable
print("Interpolation", "let s2 = Foo()")
let s2 = Foo()
print("\(s2, default: "s2 is nil")")
//print out:
//Interpolation let s2 = Foo()
//-a3-
//xxx4444
//yyy3333
//Text Output StreamableText Output Streamable
//: [Next](@next)
"box", "existential": is there somewhere I can read up on these to get some understanding how these work? Is "existential" short for "existential container"?
So struct when referred to by the protocol it implements, it turns into an "existential"?
but 1 call to the instance param's methods/properties are faster because direct call vs. 2 might be indirect but ExistentialSpecializer can make the calls direct, too?
But with the generic version, this:
String.init(describing: Foo() as CustomStringConvertible)
end up calling the most generic overloaded version with the slowest internals.
Instead of letting the compiler resolve the call, is there anyway to "specify" the version I want, something like:
String.init< CustomStringConvertible>(describing: Foo() as CustomStringConvertible)
(Edit: maybe since the argument is an existential, it just cannot call this)
I think Rust uses impl T for an existential that implements/conforms to T. Swift uses T for both. I saw it somewhere on this forum that the point is to make it easier to grasp for the beginner, but ends up causing some other unpleasant confusion instead.
Basically, when you want to write generic code, use the language’s generics feature.
The only time you should really care about existentials is when you want to erase a type (e.g. because it’s a return type and you might return different types along different paths, or you want to keep the type secret, or you need a type-flexible stored property/local variable).
@young After digging for the post I mentioned in the previous comment, I stumble upon an old post that you might be interested in–Improving the UI of generics.
I read around here that Rust used to do it the way we do. But the confusion was too much. I guess we should do it too, like a "any MyProtocol," and as soon as possible (like Swift 6).
That's a very interesting question, and I'm actually not clear as to the answer. What's more interesting, this behavior isn't the same for a custom protocol:
protocol P { }
struct S: P { }
extension String {
init<T: P>(xxx t: T) { fatalError("C") }
init(xxx p: P) { fatalError("D") }
}
String(xxx: S()) // error: Ambiguous use of 'init(xxx:)'
I wonder if the behavior observed is unintentional. I actually don't recall this particular question coming up before, at least in the context of initializers. @dabrahams@jrose@Douglas_Gregor?
Since these two ( and also my two init's) compile fine without ambiguity, it means the compiler definitely know the parameter types are different and the code is treated as correct. The fact the compiler is not able to call the correct overload or "ambiguous" compile error indicate something is wrong with the compiler.
So back to my original wondering, String now has these four init's:
init<Subject>(describing instance: Subject) where Subject : TextOutputStreamable
init<Subject>(describing instance: Subject) where Subject : CustomStringConvertible
init<Subject>(describing instance: Subject) where Subject : CustomStringConvertible, Subject : TextOutputStreamable
@_disfavoredOverload // need this to favor the "existential" overload proposed below
init<Subject>(describing instance: Subject)
the first three are fast but only called with exact generic type match, when call with "existential" (is this the correct term?), the last one is called which is slow because it use reflection inside.
I think they should add these three overloads for efficiency to avoid calling the last generic version when called with "existential":