Pitch: allow optional assignment in failable init

a lot of times i have types that wrap an implementation with failable inits, and dispatching to them is kind of awkward:

extension SpecializedType:LosslessStringConvertible
{
    public
    init?(_ description:__shared String)
    {
        if  let value:GenericImpl<T> = .init(description)
        {
            self.init(value: value)
        }
        else
        {
            return nil
        }
    }
}

could we instead allow assigning optional values to self, which would desugar to returning nil if the optional is nil?

extension SpecializedType:LosslessStringConvertible
{
    public
    init?(_ description:__shared String)
    {
        self = GenericImpl<T>.init(description).map(Self.init(value:))
    }
}
6 Likes

Reading this, my concern is that it's much more difficult to reason about the scenarios in which the initializer would fail.

Indeed, in this specific snippet, without knowing that GenericImpl<T>.init(_:) is failable (as opposed to, say, an initializer for a collection type that implements a custom map), it is impossible for me to reason that this initializer will fail, and if I miss the ? in the method signature I wouldn't even know to think about it.

By contrast, though return nil in failable initializers is a "fake" return statement co-opted to indicate failure, it does have the virtue of being a big and glaringly obvious sigil. So to me, the mantra that code is read more often than it is written is actually apt here: it might feel redundant to the writer, but I'm much more comfortable reading the "awkward" version.

16 Likes

I think this is a compelling case for control flow statements as expressions. Being able to use ?? return nil for this behavior would clearly indicate the initializer’s failability without declaring a variable that is immediately returned.

1 Like

I think this is a matter of style and familiarity. This is immediately familiar to coders who are used to the style of coding. It has less clutter and reads quickly with little noise to me. But I do recognize that some (maybe even many) disagrees. For that reason, we avoided Optional.map in my previous teams style guide lines. However, this can be said about many stylistic choices.

This is kinda an argument against Optional.map in general. A lot of people think in terms of monads.

There are other cases where the suggested syntax would produce mer terse, less-cluttered code, even without any use of map, eg:

public extension LosslessStringConvertible where Self: CaseIterable & RawRepresentable<String> {
    init?(_ description: String) {
        self = Self.allCases.first { $0.rawValue == description }
    }
}

I think almost all would realize that first returns an Optional and immediately understand the semantics of the above code.

6 Likes

Am I understanding the idea correctly that self = nil would immediately exit the init? If so my main concern is that this syntax would obscure control flow in more complex cases—a seemingly benign assignment, even if you're fully aware of the types involved, would behave quite differently from a similar assignment outside of an init?.

6 Likes

But it still has to be read and thought about not glanced at. I think that's something I've noticed about Swift over the past year or two. It's certainly gotten more ergonomic to write shorter code snippets. But the code that's out there is getting much harder to read.

I don't mean understand. Nothing particularly fancy is being done. I mean get the gist of the authors intent by just looking at it. A lot of programming for a lot of people is reviewing other peoples code. If you have to actually slow down and actually read every character it takes forever. I think the Swift code that's out there, while fine, is much much slower to parse visually.

Re-formatting the original examples to make them feel a little more "from the same code base"

extension SpecializedType:LosslessStringConvertible {
    public init?(_ description:__shared String) {
        if  let value:GenericImpl<T> = .init(description) {   
        	self.init(value: value) 
        }
        else  {  return nil  }
    }
}
extension SpecializedType:LosslessStringConvertible {
    public init?(_ description:__shared String) {
        self = GenericImpl<T>
        			.init(description)
        			.map(Self.init(value:))
    }
}

That makes the second one easier to chunck, certainly, but first one is still MUCH easier for someone unfamiliar with the code base to understand what the author thinks can fail. There is an if jutting out right there on the left margin to catch the eye.

public extension LosslessStringConvertible where Self: CaseIterable & RawRepresentable<String> {
    init?(_ description: String) {
        self = Self
                   .allCases
                   .first { $0.rawValue == description }
    }
}

That formatting makes the fail-able line (.first) easier to catch if the reader has the same gut relationship with .first that they do with if. I think it isn't likely that "I can fail" is the first thing a reader thinks when they see first like it is with if. but I won't quibble on that point. (I take that back. That's important.) Eyeballs still have to physically move around the code to find it a lot more.

In something other than a fail-able init, I don't think I'd make a big deal about the difference, really. But the ONE thing a reader of the code needs to know when looking at a fail-able init is what can fail.

This isn't one of the places I'd pick to streamline.

2 Likes

No surprise exits. One approach that I think could work here is if the body of init? just defined self to have the optional type of Self? in the code following the above initial assignment. That trailing part of the body would then be not unlike a closure body with a [weak self] capture.

This feels like a more acceptable direction to me in a vacuum (self is of type Self? within failable inits), though actually pursuing such an approach is rife with source compatibility concerns.

1 Like

what if instead of using assignment, we were to go the opposite direction and say that we can return an instance of Self? from a failable init, and that would be the same as conditionally assigning to self?

extension SpecializedType:LosslessStringConvertible
{
    public
    init?(_ description:__shared String)
    {
        return GenericImpl<T>.init(description).map(Self.init(value:))
    }
}
4 Likes

That also seems preferable to 'hidden' control flow, though I'm apprehensive as to whether it's desirable to lean further into the weird double-treatment of init? as (internally) both a Void-returning function and an optional-returning function. It seems more consistent with the rest of the language to say that an init? is 'just' an initializer where self has type Self?. And to be clear, I'm not necessarily advocating for such an approach—I think the status quo where we are biased towards the 'happy path' is perfectly reasonable and I'm not sure that a design which puts the success and failure cases on more equal footing would actually be better overall, even if it addressed this one pattern.

1 Like

I can see the value of something like this combined with if/switch expressions to simplify failable conversions:

init?(value: RawValue) {
  // implicit return
  switch value {
  case 0: .zero
  case 1: .one
  default: nil
  }
}

Just to explore ideas in this space a bit, what if we just said that any initializer could return a value (of type Self for init and Self? for init?), and an initializer body consisting of more than one statement or a single Void-typed expression has an implicit return self at the end to preserve source compatibility*?

Then we even get benefits for non-failable inits, because you can write:

init(...) {
  guard someSpecialCase else {
    return specialCaseValue
    // instead of:
    //     self = specialCaseValue
    //     return
  }
  self.blah = blah
}

I haven't convinced myself 100% that I like this yet, though.

* I'm sure there are still some rough edges that I haven't considered here. For example, this code would subtly change behavior, because right now we discard the result of f() and now we would be returning it/assigning it to self:

struct S {
  init() {
    f()
  }
  func f() -> S { ... }
}

But code like this would be weird, and it's probably unlikely when S would have actual members.

5 Likes

I also like this [Pitch: allow optional assignment in failable init - #9 by taylorswift] approach better, FWIW. The fact that it makes the syntax of init? functions more similar to -> Self? functions rather than a whole new style to parse DOES feel more like a potential worthwhile streamline.

1 Like

I think it would also be reasonable to just say that if you want to use the 'return-from-init behavior' then you must do so explicitly (i.e., no implicit return for single-expression bodies). This still made a bit weird by the fact that we'd need to continue to support early return (with no value) from init, which is one of the things that makes me wary of a change like this.

Another thing we could do is say that returning a self value is only permitted when self is not already fully initialized, so any circumstances like the S example you have would retain their current meaning. Of course, we'd then still need an exception for init? since it's perfectly valid to return nil even once you have a complete self value.

1 Like

For reference the initial announcement blog post: Failable Initializers - Swift Blog - Apple Developer

I had been hoping for a more detailed explanation on why it hadn't been implemented with self = nil for the unhappy path. It may be the lack of symmetry that feels awkward to work with now (?) But it was too early for the pitch process. Is it the ability to fast exit fail?

Does anyone recall? I don't think I'd totally object to a self = Self? either if its framed as a move towards symmetry in the two paths rather than as an excuse to get rid of ifs :wink:

I've sometimes wanted a failure propagation mechanism similar to try but for optionals. Just some keyword that unwraps or returns nil in situations where returning nil is possible. It would be similarly terse, but without obfuscating the possibility of failure or special-casing init?:

self = keyword GenericImpl<T>.init(description).map(Self.init(value:))

If failable initializers allowed assigning nil to self, the compiler wouldn’t be able to reason about unsafe use of self:

struct S {
    private name: String
    public let default = S(name: "Default")
    private init(name: String) { self.name = name }
    public init?() {
        let none: S? = nil
        self = Bool.random() ? none : .default
        print(name) // Access through potentially-nil self
    }
}

Possible fixes:

  • Prohibit access to self after assignment to self. That would be… unpopular. :slight_smile:
  • Prohibit access to self after potential assignment of nil to self. Requires the compiler perform data-flow analysis.
  • Prohibit access to self after potential assignment of nil to self, and require that assignments of nil use the explicit nil literal. Still requires analysis of every assignment to self.
  • Treat self as Self? after potential assignment of nil to self. Then self must become optional after any control flow where one branch might assign nil to self.
  • Add a rule that assignment of nil must only appear as the last statement in the execution trace of a function. That’s exactly what return does.

And there we have it. return nil prevents a whole mess of issues related to “what is the type of self?” and “what happens if I try to use self after assigning nil to it?”.

4 Likes

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

At first blush, that does seem quite a reasonable expansion of return nil that addresses the concerns about hidden control flow while answering the motivation you state.

6 Likes

I agree, as long as you take into account @allevato’s source compatibility concerns.

2 Likes

i think there are at least two ways to address that issue:

  1. require explicit return in an init, which basically turns off SE-0255 in an initializer context.

    pros:

    • consistent with the idea that you have to write return to exit an initializer

    cons:

    • very slightly harder to refactor a static func into an init
    • could get annoying after a while and induce a sort of SE-0380-like movement to remove the explicit return, which would collide head-first with the original source-compatibility concern.
  2. require the initializer’s written signature to be spelled with a trailing -> Self?

    pros:

    • doesn’t collide with any existing syntax
    • opt-in, so all existing inits are unaffected
    • slightly easier to refactor a static func into an init
    • generalizes nicely to -> Self on a non-failable init, if we ever want to pursue that direction
    • might have some educational value for understanding that a return in an init now has a type context and can use leading dot syntax, etc?

    cons:

    • more typing
    • if people like the feature a lot, they might migrate all of their initializers to the new spelling, which means the average initializer signature will become slightly more verbose.
    • might have some educational cost, since we will now have three (potentially four) spellings of init instead of the two we have currently.
1 Like