Uninhabited Type (Never) Conversions

I've been experimenting with making Never something closer to a true bottom type over the last few days, and wanted to start a discussion around some of the design decisions that have come up. Most of this post is inspired by some of the past discussions of Never:

The general consensus from past discussions seems to be:

  • It's generally useful to be able to treat uninhabited types as a bottom type in expressions, despite the fact that it could confuse beginners
  • It's unclear whether an uninhabited type should be capable of conforming to protocols with static or initializer requirements.
  • Currently, B <: A implies B.Type <: A.Type in Swift(edit: this isn't true in all cases). It is probably not desirable for Never.Type to be a subtype of all meta types. Instead, as has been noted in past threads, Never would be a subtype of all metatypes because it is a subtype of all types, and Never.Type should have no special subtyping behavior.

Based on the above, I'd like to propose the following:

  • All uninhabited types (including Never) should be convertible to any other type. This satisfies expression use cases like let x = optional ?? fatalError("optional was nil!"). It also allows us to remove the special case ()->Never -> ()->T conversion which is allowed in single expression function and closure bodies. I've been hacking on an initial implementation of this which seems promising so far. Notably, conversion is weaker than subtyping. For example, uninhabited types could not be used as covariant return types of method overrides ().
  • All inhabited types should conform to any protocol which does not have static or initializer requirements(maybe except for static/init requirements w/ Self params). I haven't investigated this as thoroughly so far, but with these limitations I believe it can be accomplished by taking advantage of some of the Sema changes to support tuple conformances.
  • Don't introduce any special behavior for metatypes of uninhabited types

Alternatives:

  • Special case only Never instead of all uninhabited types, or add some kind of @bottomType annotation. In my opinion this just adds unnecessary special case behavior.
  • Allow uninhabited types to conform to protocols with static/init requirements and synthesize trapping implementations. This potentially adds a lot of runtime complexity for very little gain. It does mean uninhabited types are not true bottom types, but there are few practical benefits.
  • Try to establish a 'real' subtyping relation instead of just an implicit conversion. This doesn't seem to have many practical benefits, and it makes the metatype situation potentially more complicated.

Future Directions:

  • Allow use of throw as a Never-returning expression, for example, let x = optional ?? throw MyError() this is fairly straightforward to implement, but is of limited usefulness without the changes above.
  • Also allow usage of return, break, and continue as expressions of type Never. These introduce some additional design challenges due to the use of autoclosures in the standard library.
13 Likes

One other major impact Never could have is described by @Joe_Groff in this post:

Personally, I really can't wait for this type of generalization to happen.

3 Likes

Thanks for working on this! @John_McCall had some concerns about the impact on type checker performance this type conversion rule might have. You might want to keep an eye on the impact adding these conversions has on compilation speed.

1 Like

This in particular is something I have often wanted to write.

You can very nearly achieve it today with a generic function:

func throwError<T>(_ error: Error) throws -> T {
  throw error
}

let x = try optional ?? throwError(MyError())

The same can be done by writing generic wrapper versions of fatalError and preconditionFailure.

Of course the proposal at hand would make that unnecessary, but I just wanted to point out that you can achieve essentially the same syntax today, if you don’t want to wait.

Also, more generally, a single generic wrapper function can work with any crashing or throwing failure mode:

func fail<T>(_ crashOrThrow: ()throws->Never) rethrows -> T {
  try crashOrThrow()
}

let x = optional ?? fail{ fatalError("It was nil") }
let y = try optional ?? fail{ throw MyError() }

• • •

(An alternative spelling for the throwError function would be…another overload of fatalError, eg. “fatalError(MyError())”.)

This is true, but personally I find the tradeoff between a custom function like this and just using a guard let statement to be a bit of a toss-up. The only clear winner in the cross-over category of "clear and concise" would be needing neither.

1 Like

I’m not weighing in on the pitch, I’m just saying you can make use of the same pattern today.

Also, I just edited in my previous post that you could equally well spell the function fatalError:

let x = try optional ?? fatalError(MyError())
1 Like

I don't think this is true in general; I think this is only true for subclass relationships. Although there is a similar rule that applies to existential relationships.

1 Like

Yeah, you're right, thanks for pointing that out.