Allow returning `.none` in failable inits


(Adrian Zubarev) #1

Update: I messed up my original post by a wrong assumption that .none is not allowed in some other places (other than in == and != functions) because of the missing conformance to Equatable protocol, which was obviously wrong. Sorry about that.



As part of a different topic I thought that we could and maybe should allow returning .none from a failable initializer. There was a similar thread before, but it seems that it went nowhere.

I would like to revive that pitch because I think it would be a reasonable addition.

struct S {
  init?() {
    // before: 'nil' is the only return value permitted in an initializer
    return .none
  }
}

struct S {
  init?() {
    // after: okay no more error
    return .none
  }
}

Let us re-write this init in a different way to showcase the issue from a different perspective:

struct S {
  static func `init`() -> S? {
    return .none // okay here
  }
}

_ = S.init() as S? // works

I would appreciate if someone with compiler insights would think if this patch can be considered as starter bug. In that case I'd like to take the opportunity and tackle this patch myself in the next couple month as I dive in C++ and the Swift compiler, otherwise I would start with other starter bugs and pass the implementation of this pitch to a more experienced compiler developer. Any guidance on how to implement this patch would be very helpful.

Thank you in advance for your feedback.


(Happy Human Pointer) #2

I am wondering the use cases for this. Would it be so difficult to just write:

return nil

rather than

return .none

Correct me if I am wrong, but the point of Optional is not to interact with it like an enum but simply as something that exists. Some may not even know that Optional is an enum. While this proposal may be worthwhile, I do not think it is worthwhile enough.

You can fork the swift compiler and mess around with it yourself. However, I'm not a compiler engineer so if you really want to implement this for yourself you can ask one of the mods to help you find one. If you do attempt, good luck :smiley:!


(Alejandro Alonso) #3

I would think this is a starter. I would look around TypeCheckStmt in Sema where we visit returns and diagnose anything other than nil in an initializer.


(Adrian Zubarev) #4

It‘s not about dificulty, but rather about consistency and also a bit of personal preference I would argue. I do like .none more than nil. With this patch the only place where you‘d be left with nil as the only solution is the use of equality functions when your type does not conform to Equatable and you‘d like to know if its value is .none, you have to use nil for that matters.


(Happy Human Pointer) #5

In what way is this consistent? The only uses of .none in the Swift stdlib only uses .none in tests of Optional.


(Happy Human Pointer) #6

Are there any others who agree with this? Have you kept in mind beginners?


(Adrian Zubarev) #7

I don‘t see how this is related, because it won‘t change anything for you. If you always write nil, this patch won‘t prevent you from doing it, so it's not related to Swift beginners by any means. I view this pitch more like a bug fix rather then really something new that requires a full proposal, but I wanted to go this route.

Optional is not a magic type, and should no be seen like this. Optional only has the exclusivity of more compiler support for more convenience because it comes from the stdlib. That makes .none part of the type and not a consequence of nil.

Some Swift developers also don‘t know that this is statically false (the compiler will emit a warning) but true at runtime (only for some exlusive stdlib types, not for custom generic types):

class Animal {}
class Cat: Animal {}
class Dog: Animal {}

print(Cat?.none is Dog) // prints true at runtime

(Happy Human Pointer) #8

That's interesting. Why is that? Is it because we don't know the true type of Cat so it can be Dog, and since it is nil it can be anything? Maybe that is a bug that can be fixed.

I'm decided against this one, but I can't hold you back if you do want to make this change.


(Adrian Zubarev) #9

It's not a bug but actually intended behavior. You can convert any empty array/set/dictionary of any Element type do a different empty array/set/dictionary of a different Element type. Similar you can convert an optional if it's value is none to a different unrelated Optional. However this is a totally different topic already.

Can you elaborate the reasoning of your decision not supporting the usage of none where it actually should already work as its part of Optional type? (See the example above where I fake an init using a static function, where it already works as intended.)


(Adrian Zubarev) #10

cc @Joe_Groff do you know if there were any technical reasons that prevented this syntax or was there any other reasoning for that decision?


#11

I am not sure this really fits the language.

class C {
    init?() { return nil }
}

Nothing in the syntax for fail-able initializers says enum to me. Why would a case from Optional be allowed when no other case from any other enum is allowed to be returned by an initializer? Not even .some(...) can be returned, and it's from the same enum.

I realize that exposing the implementation of Optional to the language is an irreversible decision, but I don't see value in expanding that exposure in this case. It doesn't seem to buy us anything, and it opens up questions that don't need to be answered when returning nil.


(Adrian Zubarev) #12

That is indeed a very good question. I personally have not though about this in that way. If that turns out to be the main reasoning why we do not allow .none in first place, I can easily withdraw this pitch. That's the beauty of the evolution process. We pitch something and share and evaluate feedback so it fits mostly everyones needs.

Other than that, this would be a good proposal to jumpstart compiler contribution, which I aimed here for.


(Adrian Zubarev) #13

However nil is only literal and Optional is the only type in the stdlib that conforms to the ExpressibleByNilLiteral that allows the usage of nil in first place. What would you say if I argued that the enclosing type does not conform to ExpressibleByNilLiteral and therefore it does not make sense that you can return nil from the initializer?!

That said an initializer in Swift is very similar to a static function, the signature is slightly different and its behavior is extended. That makes a failable initializer return an Optional type where Wrapped is the enclosing type. If we were to allow .none then we can also consider returning .some(...) as well, even though everyone will still prefer the implicit return from the initialization. Interestingly enough you write this which also compiles just fine:

struct R {
  var value: Int
  init(_ value: Int) {
    self.value = value
    return
  }
}

In functions this evaluates to returning Void or an empty tuple (), but in case of an initializer it will return the enclosing (Self) type. (Please feel free to correct me if I'm mistaken here.)


#14

I would argue that ExpressibleByNilLiteral is an implementation detail. The compiler could have easily hard-coded support for nil as a keyword, but chose to make Optional fit into the rest of the language, as if it weren't special. The conflict here is that Optional is special, with all the syntax which exists explicitly to support it.

My issue here is that initializers in Swift don't return values. Returning nil for fail-able initializers is already an exception, because there's no other way to express failure in this situation. (Not only did exceptions not yet exist, but even now they are arguably the wrong measure when no actual error occurs in initialization.) I don't see value in adding an exception to the exception. Or broadening the exception to account for an implementation detail of Optional.

But all this is purely my opinion. If the core team states that Optional as enum is more than an implementation detail, then your proposal makes sense, semantically.


(Adrian Zubarev) #15

Thank you @Avi that is very valuable feedback.


(Adrian Zubarev) #16

In fact I think my last example from above should allow returning self as well.

struct R {
  var value: Int
  init(_ value: Int) {
    self.value = value
    return self // what's wrong with this?
  }
}

Maybe we should simply generalizing return in init to something more predictable:

  • The initialization order in super-type relationship is already well understood.
  • An init always returns a value (mostly implicitly at this moment)
  • Explicit return is only permitted to nil when you need to opt out and wrap the value into an optional with current rules.
  • The last two points could be generalized into permitting explicit returns in any init.
    • return self should be allowed in a non-optional init.
    • return .none and possibly even return .some(self) should be allowed in failable init's (latter is already implicit anyway). I would also think that return .some(other) can be possible where other can be guaranteed to be of type Self.
    • return nil already exists today.

(Mathew Polzin) #17

I tend to agree with the idea of generalizing return from initializers. Initializers certainly read better at the call site than static constructors in many situations (again a matter of opinion, but one held by the community at large it seems like) but I am not sure why they can’t ultimately be no more than sugar.

I think I am missing some details when I describe it this way, though. There are some seemingly large semantic differences between a static context and a (pre-initialized) instance context.


(Christian Schnorr) #18

That said an initializer in Swift is very similar to a static function, the signature is slightly different and its behavior is extended.

I would argue that this is false. In an initializer, the (uninitialized) instance has already been created and it's the initializer's job to configure it, while in a static function the instance would need to be created first.

I would also think that return .some(other) can be possible where other can be guaranteed to be of type Self .

That would currently be equivalent to write self = other in the initializer, which only works for value types as it would change an object's identity.

Also, in my mental model, an initializer does not return a value. return nil merely is a way to signal that the instance could not be initialized (note that nil is not a value but a compile-time literal), and that is very different from the return .none you propose, which would be returning a value.


(Adrian Zubarev) #19

Well I don't try to say that I'm technically or semantically correct. I do admit that I might talk 'a fair bit' of non-sense, but I at least hope that my ideas go into the right direction and people do understand what I was trying to say. :)

That is the factory pattern that many of us still want to see happen in Swift. Of course it must guarantee that other is of type Self. However I understand your point and why the idea of return .some(other) should be dropped here.


(Bernd Ohr) #20

If we are talking about generalising initialisers returning a value wrapped in a container (in the moment only Optional is allowed), then I have a half-serious suggestion:

struct S {
  [init]() {
    return []
  }
}

Sorry, could not resist...