Question re: String.init(describing:) overloads: why there are four?

String Doc

I don't understand why there are four overloads:

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
init<Subject>(describing instance: Subject)

the last the generic param Subject is totally unqualified. So why first three? Why just the last one not suffice?

I'm coming from this tweet on using custom string interpolation to print out optional value. His appendInterpolation(...) only handle CustomStringConvertible type. I look up String.init(describing:) and saw there are four overloads. I thought maybe there needs to be four overload of appendInteerpolation(...) to handle all the types that String.init(describing:) can? But then I saw the last String.init(describing:) is just <Subject> which prompted me to wonder why the other three overloads exist at all? Why not just this last one with <Subject> only?

Anyway, back to the custom String.StringInterpolation, I tried just this:

extension String.StringInterpolation {
//    mutating func appendInterpolation<Subject: TextOutputStreamable>(_ value: Subject?, default defaultValue: @autoclosure () -> String) {
////        appendLiteral(value != nil ? value!.description : defaultValue())
////        appendLiteral(value.map { $0.description } ?? defaultValue())
//        appendLiteral(value.map(String.init(describing:)) ?? defaultValue())
//    }
//
//    mutating func appendInterpolation<Subject: CustomStringConvertible>(_ value: Subject?, default defaultValue: @autoclosure () -> String) {
//        appendLiteral(value.map(String.init(describing:)) ?? defaultValue())
//    }
//
//    mutating func appendInterpolation<Subject>(_ value: Subject?, default defaultValue: @autoclosure () -> String) where Subject : CustomStringConvertible, Subject : TextOutputStreamable {
//        appendLiteral(value.map(String.init(describing:)) ?? defaultValue())
//    }
//
    mutating func appendInterpolation<Subject>(_ value: Subject?, default defaultValue: @autoclosure () -> String) {
        appendLiteral(value.map(String.init(describing:)) ?? defaultValue())
    }
}

-- just one appendInterpolation<Subject> seems to work just fine. No need for those other overloads. Is this enough here? Is this sufficient? Or I need all four?

my playground code:

//: [Previous](@previous)

// https://twitter.com/natpanferova/status/1315914663582822400/photo/1

import Foundation

infix operator ???: NilCoalescingPrecedence
public func ???<T>(value: T?, defaultValue: @autoclosure () -> String) -> String {
    value.map(String.init(describing:)) ?? defaultValue()
}

var url: URL?
print("The url is \(url ??? "url is nil")")


extension String.StringInterpolation {
//    mutating func appendInterpolation<Subject: TextOutputStreamable>(_ value: Subject?, default defaultValue: @autoclosure () -> String) {
////        appendLiteral(value != nil ? value!.description : defaultValue())
////        appendLiteral(value.map { $0.description } ?? defaultValue())
//        appendLiteral(value.map(String.init(describing:)) ?? defaultValue())
//    }
//
//    mutating func appendInterpolation<Subject: CustomStringConvertible>(_ value: Subject?, default defaultValue: @autoclosure () -> String) {
//        appendLiteral(value.map(String.init(describing:)) ?? defaultValue())
//    }
//
//    mutating func appendInterpolation<Subject>(_ value: Subject?, default defaultValue: @autoclosure () -> String) where Subject : CustomStringConvertible, Subject : TextOutputStreamable {
//        appendLiteral(value.map(String.init(describing:)) ?? defaultValue())
//    }
//
    mutating func appendInterpolation<Subject>(_ value: Subject?, default defaultValue: @autoclosure () -> String) {
        appendLiteral(value.map(String.init(describing:)) ?? defaultValue())
    }
}

let v1: Int? = nil

print("v1 \(v1, default: "v1 is nil")")

let v2 = 3

print("v2 \(v2, default: "v2 is nil")")

print("url is \(url, default: "url is nil")")


//: [Next](@next)

Have you tried to force the last one when one of the other three suffice (by casting to Any)? Does it still use description?

Do you mean this:

let any: Any = "Help"

let str = String(describing: any)

print(str)      // print out "Help"

var anyOptional: Any? = nil

print("anyOptional is \(anyOptional, default: "url is nil")")   // print out "anyOptional is url is nil"

all worked

I mean a custom type that uses CustomStringConvertible.description in a “non-traditional” way. I wonder if the initializer will use it. Something like this:

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

// Custom String Convertible
print(String(describing: Foo()))

// Text Output Streamable
print(String(describing: Foo() as Any))
print(String(describing: Foo() as CustomStringConvertible))
print(String(describing: Foo() as TextOutputStreamable))
print(String(describing: Foo() as CustomStringConvertible & TextOutputStreamable))

:thinking::thinking::thinking: Something seems to be up with iOS Playground.

I think it all comes down to efficiency and correctness. The source code for the initializers show that they have different implementations for different Subject.

Let me label the 4 initializers, so they're more wieldy:

// initializer 1
init<Subject>(describing instance: Subject) where Subject: TextOutputStreamable
// initializer 2
init<Subject>(describing instance: Subject) where Subject: CustomStringConvertible
// initializer 3
init<Subject>(describing instance: Subject) where Subject: CustomStringConvertible, Subject: TextOutputStreamable
// initializer 4
init<Subject>(describing instance: Subject)

TextOutputStreamable and CustomStringConvertible are 2 protocols that each allows the user to provide a description for an instance of a type. Initializers 1 and 2 take advantage of their existence, and use the user-defined value as the instance's description. 1 and 2 are implemented differently, because the "description" part of TextOutputStreamable and CustomStringConvertible are implemented differently: TextOutputStreamable uses write(to:) and CustomStringConvertible uses description.

Because initializers 1 and 2 are implemented differently, 3 becomes necessary for the compiler to know what to do when Subject conforms to both CustomStringConvertible and TextOutputStreamable.

4 (the most general case?) uses reflection to create a description of an instance, regardless of its type.

Initializers 1, 2. and 3 exist when 4 already works for all Subjects, because it's more efficient (and correct?) to use what the user has provided, instead of finding out an instance's description through reflection.

For the same reason, there are multiple overloads of appendInterpolation in DefaultStringInterpolation.

4 Likes

So I should also have four appendInterpolation<Subject>(...) where ... (the ones I commented out) so it cal call the matching the String.init(describing:) then, yes? Even though the most generic one should work.

I'm not sure if String.init(describing:) in your appendInterpolation in the top of this thread receives dynamic dispatch. If it does, then your current appendInterpolation already takes care of handling different String.init(describing:). If it doesn't, then you'll need to overload your appendInterpolation to take more refined implementations of String.init(describing:).

deleted. See my next post ... sorry

For performance. If you look at the version that’s generic over CustomStringConvertible you can see the implementation looks like this:

  @inlinable
  public init<Subject: CustomStringConvertible>(describing instance: Subject) {
    self = instance.description
  }

That is, it’s identical to fetching the .description property of the value, and is likely to get inlined to just that.

Whereas the the unconstrained version does a whole bunch of stuff. First it creates an empty string. Then it calls another function to write the value to that string. That speculatively tries to cast the value to various types (generating metadata and other detritus along the way) until it finds the right type. If it doesn’t, it falls back to reflection.

2 Likes

Ok, so it better to call the more specific one and only fall back to most generic one as last choice.

Never mind what I said in my last post, don't know what's going on, even with four generic methods

appendInterpolation<Subject>(...) where ...

only the most generic String.init() is called:

import Foundation

extension String {
    init<Subject>(xxx instance: Subject) where Subject : TextOutputStreamable {
        print("11111")
        self.init(describing: instance)
    }
    init<Subject>(xxx instance: Subject) where Subject : CustomStringConvertible {
        print("2222")
        self.init(describing: instance)
    }
    init<Subject>(xxx instance: Subject) where Subject : CustomStringConvertible, Subject : TextOutputStreamable {
        print("3333")
        self.init(describing: instance)
    }
    init<Subject>(xxx instance: Subject) {
        print("4444")
        self.init(describing: instance)
    }
}

extension String.StringInterpolation {
    mutating func appendInterpolation<Subject>(_ value: Subject?, default defaultValue: @autoclosure () -> String) where Subject : TextOutputStreamable  {
        print("-a1-")
        appendLiteral(value.map(String.init(xxx:)) ?? defaultValue())
    }

    mutating func appendInterpolation<Subject>(_ value: Subject?, default defaultValue: @autoclosure () -> String) where Subject : CustomStringConvertible  {
        print("-a2-")
        appendLiteral(value.map(String.init(xxx:)) ?? defaultValue())
    }

    mutating func appendInterpolation<Subject>(_ value: Subject?, default defaultValue: @autoclosure () -> String) where Subject : CustomStringConvertible, Subject : TextOutputStreamable {
        print("-a3-")
        appendLiteral(value.map(String.init(xxx:)) ?? defaultValue())
    }

    mutating func appendInterpolation<Subject>(_ value: Subject?, default defaultValue: @autoclosure () -> String) {
        print("-a4-")
        appendLiteral(value.map(String.init(xxx:)) ?? 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" }
}


let s1 = Foo() as CustomStringConvertible?
print("\(s1, default: "s1 is nil")")
// only print out:
// -a4-
// 4444
// Text Output Streamable


let s2 = Foo() as Any?
print("\(s2, default: "s2 is nil")")
// only print out:
// -a4-
// 4444
// Text Output Streamable

// but this is calling the specialized one:
let s3: Foo? = Foo()
print("\(s3, default: "s3 is nil")")
// prints:
// -a3-
// 3333
// Custom String Convertible

Am I doing this wrong? How to make interpolation call the specific String.init() when the type is CustomStringConvertible? ?

The compiler knows to use the more specilised version. For example, in your

init<Subject>(xxx instance: Subject) where Subject: CustomStringConvertible {
    print("2222")
    Self.init(describing: instance)
}

the init(describing:) is the one specilaised for Subject: CustomStringConvertible.

Ah right, totally forgot that a protocol doesn't conform to itself. Sorry for a confusion (from my code above). You need to create a new type that only conforms to CSC, or avoid existential. Something like this:

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

func print<X: CustomStringConvertible>(csc x: X) { print(String(describing: x)) }
func print<X: TextOutputStreamable>(tos x: X) { print(String(describing: x)) }
func print<X: CustomStringConvertible & TextOutputStreamable>(both x: X) { print(String(describing: x)) }
func print<X>(neither x: X) { print(String(describing: x)) }

// Matches their *static* type
print(csc: Foo()) // Custom String Convertible
print(tos: Foo()) // Text Output Streamable

// Prefer CSC in static context
print(both: Foo()) // Custom String Convertible

// Prefer TOS in dynamic context
print(neither: Foo()) // Text Output Streamable

Inside each appendInterpolation<Subject>(...) is not the problem. It's this:

s1's type is CustomStringConvertible?, but it's calling the 4th

appendInterpolation<Subject>(...)

why it's not calling

appendInterpolation<Subject: CustomStringConvertible>(...)

the 2nd one? So I must be misunderstand what s1 is here?

Try this:

func print<X: CustomStringConvertible>(csc x: X?) {
    print("\(x, default: "x is nil")")
}
func print<X>(none x: X?) {
    print("\(x, default: "x is nil")")
}
func print<X: CustomStringConvertible & TextOutputStreamable>(both x: X?) {
    print("\(x, default: "x is nil")")
}

print(csc: Foo())
print(none: Foo())
print(both: Foo())

s1 is CustomStringConvertible? (box), which does not conform to CustomStringConvertible (protocol).

Because CustomStringConvertible? is Optional<CustomStringConvertible>, which is a different type from CustomStringConvertible, and does not conform to CustomStringConvertible.

Optional<CustomStringConvertible> doesn't conform to TextOutputStreamable either.

So the only choice left is the 4th appendInterpolation.

It's not just that. Even CustomStringConvertible DOES NOT conform to CustomStringConvertible.

3 Likes

Interesting. I didn't know that. It seems very reasonable that it doesn't conform to itself, to think about it. Since you can't really find a default implementation for description.

Hard to understand, but the compile error message makes this clear for me:

func funkyfunk<T: CustomStringConvertible>(_ v: T) {
    print(v.description)
}

funkyfunk(Foo() as CustomStringConvertible)     // <== compile error here

**error: value of protocol type 'CustomStringConvertible' cannot conform to 'CustomStringConvertible'; only struct/enum/class types can conform to protocols**

**funkyfunk(Foo() as CustomStringConvertible)**

**note: required by global function 'funkyfunk' where 'T' = 'CustomStringConvertible'**

**func funkyfunk<T: CustomStringConvertible>(_ v: T) {**

Rephrasing the first error message: Protocols cannot comform to protocols. Only struct/enum/class types can conform to protocols.

So with just a protocol type, Swift cannot call the specific appendInterpolation<Subject:...>(...), so it just call the unqualified generic one (the last one). There is no way to make string interpolation to call the specific one with just a protocol with common overloading.

But should work with specific call signature:

"\(csc: s1, default: "s1 is nil")"

(never mind, do not work with just protocol, still the same reason: protocol cannot conform to protocol)

I think it's clear now for me. :roll_eyes:

Also, all four appendInterpolation<Subject:...>(...) are needed in order to get the right specialized String.init(describing:)