Why does String's "init<T>(_ value: T) where T : LosslessStringConvertible" use a generic constraint?

String source code:

/// Creates an instance from the description of a given
/// `LosslessStringConvertible` instance.
@inlinable @inline(__always)
public init<T: LosslessStringConvertible>(_ value: T) {
	self = value.description
}

I'm wondering why the implementation isn't just

public init(_ value: LosslessStringConvertible) {
	self = value.description
}

Because if I want to pass a LosslessStringConvertible from a function parameter, I have to write this

func pointlessFunction<T: LosslessStringConvertible>(_ value: T) -> String {
    String(value)
}

Instead of this, which is more concise and allows me to avoid using generic constraints if I refer to it within another function

func pointlessFunction(_ value: LosslessStringConvertible) -> String {
    String(value)
}

Many thanks!

I do this a few times. The non-generic version will be using a box that wraps T when generating the binary, while the generic version will directly use T. The non-generic version doesn’t seem to unbox it, so you’re stuck with extra indirection. If you expect the implementation to be inlined, the former can be faster, providing you can pay the extra code size from specialization.

I don’t know about that one in particular, though.


Also, you can just use value.description.

2 Likes

Thanks Lantua, that makes sense given that in my experience a concrete type is passed into this initialiser 99% of the time.

The ExistentialSpecializer pass can specialize functions that take existentials as arguments.

:thinking: Has it always been there? I might need to get back to my old code base to see how this changes things.

Yes, that pass has always been there.

More broadly, as a matter of API design, a generic function and a function taking an existential are not the same. A generic function may be dispatched on multiple types of objects, but at any call site in the code it may be dispatched only on one type. That is, it maintains type identity of the specific type being passed to it.

A function taking an existential is dynamic both at runtime and compile time: it may take an object of any type conforming to the protocol at any call site. The call site therefore lacks type information, and that type information can only be recovered via runtime type checking.

Put shortly: generic functions preserve type information, functions taking existentials discard it. In my view, unless there is a compelling reason to discard the type information (e.g. because you are storing a heterogeneous collection, for example), you should preserve it wherever possible, and this means favouring generic functions over those taking existentials.

5 Likes

Yea, I've been under the same impression.

It might be that the specializer fails to turn existential into generic in some case for me, that I'm under the impression that it couldn't inline pass the witness table. It was years back so it might still be buggy then.

If the ExistentialSpecializer is there then, wouldn't it be better to prefer existential over generic? IIUC it just transforms existential parameters into generic ones.

I think existential specializer only works in specific circumstances that are pretty limited in scope.

No. Generics are easier to specialise exactly because they preserve their type information. They specialise more consistently. Additionally, that extra type information is valuable. Requiring a compiler pass to transform existential code to generic code is strictly less good than having generic code to begin with.

OK, I agree in scenarios where the extra type information is valuable. In scenarios where the type information doesn't matter much, though, existential seems to be a relatively safe choice given that it is much easier to use, and may be lifted into generic via ExistentialSpecializer if the compiler sees some benefit (and the compiler is usually better than me at that).

Leaving aside style opinions, ExistentialSpecializer isn’t formally guaranteed to exist and always work, and it can’t work across dynamic linking boundaries (i.e. for non-inlined methods in frameworks).

1 Like

ExistentialSpecializer (in its earliest form) was merged on September 26, 2018 (so after Swift 4.2, which was released on September 17, 2018). It's actually a relatively recent addition.

But yes, the 2 function signatures are semantically equivalent.

1 Like

One more difference not mentioned yet: you can store the function taking an existential as a closure, but you cannot do the same with the generic one.

let x: (LosslessStringConvertible) -> Void // OK
let x: <T: LosslessStringConvertible>(T) -> Void // pseudoswift 

Does anybody know if this is on the roadmap already?

1 Like