okay, let’s take a step back and talk about why we need this kind of thing in the first place. at least 8 times out of 10 for me, i run into this problem with LosslessStringConvertible, hence why the example i provided uses LosslessStringConvertible.
it’s really common to have some sort of generic parsing logic in a frozen, transparent, @usableFromInline type that has an init?(_ description:String):
// @frozen @usableFromInline internal
extension InlineBuffer:LosslessStringConvertible
{
/// Parses a hex string.
@inlinable internal
init?<String>(_ description:__shared String) where String:StringProtocol
{
// lots of code that should be emitted into the client as
// few times as possible!
}
}
a naïve approach (which is unfortunately, i see written all too often) is to write the public interface of the wrapper type to also be generic over StringProtocol:
extension SHA256:LosslessStringConvertible
{
@inlinable public
init?(_ description:__shared some StringProtocol)
{
if let buffer:InlineBuffer<Storage> = .init(description)
{
self.init(buffer: buffer)
}
else
{
return nil
}
}
}
but this is a terrible approach because we don’t want to make the SHA256.init(_:) witness @inlinable, it is a huge function and we might also want to preserve resilience.
a better API would preserve the resilience barrier by manually specializing for String and Substring:
extension SHA256:LosslessStringConvertible
{
public
init?(_ description:__shared String)
{
if let buffer:InlineBuffer<Storage> = .init(description)
{
self.init(buffer: buffer)
}
else
{
return nil
}
}
public
init?(_ description:__shared Substring)
{
if let buffer:InlineBuffer<Storage> = .init(description)
{
self.init(buffer: buffer)
}
else
{
return nil
}
}
}
if InlineBuffer also lives in the same module, this also allows us to remove the @frozen, @usableFromInline, @inlinables from that type as well.
typing it out isn’t a problem - copilot gets the idea pretty quickly - but it looks heavyweight and it’s hard to visually scan it and understand that they are essentially the same function copied-and-pasted multiple times.
interestingly, Self?-returning static functions don’t have this problem, they collapse nicely into:
extension SHA256 //:LosslessStringConvertible
{
public static
func _init(_ description:String) -> Self?
{
InlineBuffer<Storage>.init(description).map(Self.init(_:))
}
public static
func _init(_ description:Substring) -> Self?
{
InlineBuffer<Storage>.init(description).map(Self.init(_:))
}
}
but Self?-returning static funcs are not idiomatic, and specifically in this example, they cannot witness LosslessStringConvertible’s requirement anyway.
you and @Jumhyn have convinced me that hidden control-flow in self = is not the way to go, but i think that returning Self? can solve a lot of problems here, and also make it easier to refactor things between init and static func ... -> Self?.
extension SHA256 //:LosslessStringConvertible
{
public
init?(_ description:__shared String)
{
InlineBuffer<Storage>.init(description).map(Self.init(_:))
}
public
init?(_ description:__shared Substring)
{
InlineBuffer<Storage>.init(description).map(Self.init(_:))
}
}
to address this concern:
we could require an explicit -> Self? on the written signature of the init:
public
init?(_ description:__shared Substring) -> Self?
{
InlineBuffer<Storage>.init(description).map(Self.init(_:))
}