Pitch: allow optional assignment in failable init

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(_:))
    }
3 Likes