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
, @inlinable
s 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 func
s 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(_:))
}