Using LosslessStringConvertible initializer with a Substring parameter

Hello,

I have a function I've written in Swift to parse a list of numbers from a Substring:

func parseNumbers<T> (_ line : Substring) -> [T] where T : LosslessStringConvertible, T : Numeric {

    return line.split(separator: " ").dropFirst().map() { T(String($0))! }

}

If I remove the String() call in the map closure, I get a compiler error (Xcode 12.2) saying Cannot convert value of type 'Substring.SubSequence' (aka 'Substring') to expected argument type 'String' even though, AFAICT, the requirement for LosslessStringConvertible's initializer is to take a StringProtocol, which Substring implements. However, if I change the above to be specific to Double instead of taking a type parameter, it works fine without the conversion to String. Just wondering what I might be misunderstanding about the protocols and/or generics. Thank you!

Neal

Where do you see that? LosslessStringConvertible has only one requirement, which is a failable initializer that takes a String:

Ah, thanks. It was my mistake, I was misreading this source code as relevant: swift/stdlib/public/core/FloatingPointParsing.swift.gyb

It seems to make sense to me have LosslessStringConvertible's functions take a StringProtocol, however, to avoid copies if callers can provide a Substring. Since String & Substring both implement the same interface it could be an easy change (I think).

In fact, it is impossible to make such a change, because it would break the ABI.

Thanks! I didn't realize it was ABI breaking to change an argument type to a protocol that the type implements.

Perhaps another initializer that takes StringProtocol would work, then. Being able to save a String construction & copy if the user who is calling into LosslessStringConvertible functions only has Substring (and, in fact, has taken care to use Substring to avoid String copies, such as what I did here) seems in the spirit of StringProtocol/String/Substring, and enough to justifying the process of deprecating the old one and providing a new one for callers to recompile with. Definitely not an urgent change by any means, though.

1 Like

That, too, would break the ABI (unless it is given a default implementation that calls the current initializer, which would defeat the purpose).

I was just about to make a thread about this :slight_smile: I think it's worth considering.

My understanding is that StringProtocol is closed to new conformances, so the only types that could be passed in to the initialiser are String and Substring, and that it is possible for the standard library to make a String from a Substring without copying -- in other words, it would be possible for the standard library to implement the generic version in a protocol extension by adjusting the representation and calling in to init?(_ description: String). Perhaps over time, the requirement could be changed to the generic version, in a similar way to how Hashable's requirement was changed.

Also worth noting: if you implement the generic version for your type, it will satisfy the requirement for LosslessStringConvertible. Users will see both of them in autocomplete, which is a bit messy and perhaps also worth looking in to. (Update: filed as SR-14220).

There are potential downsides to making a function generic if it is not also @inlinable. Then again, generics are an important feature of Swift and we shouldn't be afraid to use them wherever we can.