.init<Subject: CustomStringConvertible>(xxx instance: Subject) vs. .init(xxx instance: CustomStringConvertible)

From my previous post asked why there are four versions of String.init(describing:) when the most general one can suffice, learned a revelation to me:

Value of protocol type 'CustomStringConvertible' cannot conform to 'CustomStringConvertible'; only struct/enum/class types can conform to protocols

so because of above, this:

String.init(describing: Foo() as CustomStringConvertible)

does not match to the <Subject: CustomStringConvertible> version, it match to the most general <Subject> version.

but I find that it can be written this way instead:

String.init(describing instance: CustomStringConvertible) { ... }

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)

You may want to checkout this thread–(Why does String's "init<T>(_ value: T) where T : LosslessStringConvertible" use a generic constraint?)

2 Likes

"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"?

And so both of these work:

// 1
String.init<Subject: CustomStringConvertible>(describing instance: Subject) { ... } 
// 2
String.init(describing instance: CustomStringConvertible) { ... }

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)

These are existential types. SO–What is an existential type?.

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.

2 Likes

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

2 Likes

@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.

1 Like

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

1 Like

So I wonder: why not overload for both generic type and "existential" type parameter? and let the compiler choose whichever is best:

extension String {
    // 1
    init<Subject>(xxx instance: Subject) where Subject : CustomStringConvertible { ... }
    // 2
    init(xxx instance: CustomStringConvertible)  { ... }
}

but unfortunately the compiler do not choose 1:

struct Foo: CustomStringConvertible {
    var description: String { "Custom String Convertible" }
}

// these both call 2
// case 1
_ = String.init(xxx: Foo())      // why the compiler not choose 1 here?
// case 2
_ = String.init(xxx: Foo() as CustomStringConvertible)

If the compiler can choose 1 if a Foo is passed, then we can have both.

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.

I tried calling this in my playground:

String(xxx: S() as P)

It compiled without ambiguity! and ran

Terms of Service

Privacy Policy

Cookie Policy