Make Never the bottom type

Hi all,

Since it's come up again in discussion recently, I wanted to share my proposal for making Never a true bottom type in Swift. This is partially implemented here, though there remains work to be done before it's ready for review. In the meantime, I'd be interested in hearing everyone's feedback on the design so far!


Make Never the Bottom Type

Introduction

This proposal makes Never the bottom type, or universal subtype in Swift.

Swift-evolution thread: Link
Relevant past discussions of Never:

Motivation

SE-0102 introduced the Never type, an enum with no cases, as a way of conveying to both the compiler and user that a function cannot return by normal means. Instead, a Never-returning function might throw an error, trap, or simply never terminate. Over time, the type's uses have expanded to model other "impossible" situations. For example, thanks to SE-0215, which conformed Never to Error, one can construct a Result<String, Never> which is guaranteed to represent a success state. Furthermore, the acceptance of SE-0215 concluded that:

Never should become a blessed bottom type in the language. This matches with semantics in other languages and its intended role in Swift.

Making Never a bottom type has a number of concrete benefits:

  • Constructs like TODO in Kotlin, todo! in Rust, and ??? in Scala would become easier to define and use.

    func TODO(_ message: String) -> Never { fatalError("TODO: \(message)") }
    
    func fetchAndResizeImage(at url: URL, size: Size) -> Image {
        if let cached = fetchCachedImage(at: URL, size: size) {
            return cached
        } else {
            let fullSizeImage = TODO("Implement image fetching")
            let resizedImage = resizeImage(fullSizeImage, size: size)
            cacheImage(resizedImage, from: url, size: size)
            return resizedImage
        }
    }
    

    With this proposal, the example above would typecheck and compile successfully, making it easier to iterate on work-in-progress programs without struggling to satisfy type checking and definite initialization errors. This type of construct can be approximated today using an unconstrained generic return type, but requires additional type annotations in many contexts like the example above.

  • Expressions like let x = y ?? fatalError("y shouldn't be nil because ...") would be allowed to compile. This is essentially a long-form version of the force-unwrap operation, which lets the programmer document the reason the unwrap is safe. This type of construct was originally discussed during the review of SE-0217. While there are some concerns as to its teachability, it does allow programmers to significantly reduce boilerplate in areas where a guard let x = y else { fatalError("...") } would otherwise be needed. Also, unlike SE-0217, it doesn't require introducing a new standard library operator.

  • main functions returning Never would be able to satisfy the @main attribute requirement. This would allow the authors of frameworks providing default main implementations to more clearly specify whether they terminate to clients who call them directly. Similarly, overriden members could use Never as a contravariant parameter type or covariant return type.

  • As part of a future proposal, control flow statements like throw could become expressions of type Never, allowing their use in new contexts.

Proposed solution

Make the standard library type Never (but no other uninhabited types) the bottom type in Swift. This means that Never will be convertible to any other type.

To accomplish this, a new subtyping relation is introduced: For all types T, Never <: T.

Detailed design

Under this proposal, Never is convertible to any type:

let w: Int = fatalError()
let x: (Int)->String = fatalError()
let y: (Int, String) = fatalError()
let z: AnyObject = fatalError()

Covariance and contravariance work as expected throughout the language with the introduction of this new subtyping relation:

let f1: (Int) -> Never = { x in fatalError("\(x)") }
let f2: (Never) -> String = f1

let arr1: [Never] = []
var arr2: [Int] = arr1

class SuperClass {
  func f(x: Never) -> Int { 42 }
  var x: Int { 42 }
}

class SubClass: SuperClass {
  override func f(x: String) -> Never { fatalError() }
  override var x: Never { fatalError() }
}

It's important to note that although Never is a subtype of all types, it does not inherit the members of all types. This is consistent with the approach taken by languages like Scala. This decoupling of inheritance and subtyping can be surprising at first, but in practice it improves the ergonomics of working with Never. Additionally, explicit coercion can be used to access members if desired:

let plusOne = (fatalError() as [Int]).map {$0 + 1}

Also, while with this new subtyping rule Never becomes a subtype of all metatypes, Never.Type does not become a subtype of all metatypes because unlike Never, it is inhabited.

Additionally, this proposal intentionally does not automatically conform Never to additional protocols. The core team's acceptance notes from SE-0215 do a good job of expressing why this isn't yet being addressed:

However, being a bottom type does not imply that Never should implicitly conform to all protocols. Instead, convenient protocol conformances for Never should be added as deemed useful or necessary.

...

With respect to protocol conformances, the Core Team felt the language has clearly moved in a direction where explicit protocol conformance is fundamental to the language model. The compiler has grown affordances to make the implementation of protocol conformances easy in many cases (e.g., synthesized implementations of Hashable) but that the explicit protocol conformance is quite important. Adding rules for implicit protocol conformances — something that has been considered in Swift’s history — ends up adding real complexity to the language and its implementation that can be hard to reason about by a user as well as by the language implementation.

What this means in practice is that making Never the bottom type is not sufficient to allow the following code to compile:

func foo<T: Identifiable>(bar: T) {}
foo(bar: fatalError()) // error: global function 'foo(bar:)' requires that 'Never' conform to 'Identifiable'

var foo: some Bar {
    return fatalError() // error: return type of var 'foo' requires that 'Never' conform to 'Bar'
}

However, Never is the subtype of all existential types. For example, the following is valid:

let x: CustomStringConvertible = fatalError()

The following uses are also valid due to the coercions to a conforming type:

foo(bar: fatalError() as MyIdentifiableType)

var foo: some Bar {
    return fatalError() as TypeThatConformsToBar
}

Source compatibility

A few special cases are required to ensure making Never the universal subtype maintains source compatibility. For example, consider the following macOS program:

import Darwin
atexit { fatalError() }

The trailing closure parameter has type @convention(c) ()->Void, while the supplied closure appears to have the incompatible type ()->Never. This example compiles today because ()->Uninhabited single-expression closures may be rewritten into Void returning closures, where Uninhabited is any trivially uninhabited type. In most cases, this rewrite rule is no longer necesary for Never returning closures because ()->Never is a subtype of ()->Void. However, ()->Never is not compatible with @convention(c) ()->Void because covariant returns are not allowed when working with C function pointers. As a result, this proposal retains the single-expression closure rewrite rule to maintain source compatibility in the case where a single-expression closure literal is converted to an @convention(c) function. The behavior of trivially uninhabited types which are not Never remains unchanged. In practice, this behavior isn't noticeable except for the fact that @convetion(c) functions may mislead users by appearing to support covariant returns in limited circumstances.

Overload resolution is another potential area of concern with regard to source compatibility. Consider the following overload set:

func foo(bar: @autoclosure ()->Int) { print("one") }
func foo(bar: @autoclosure ()->Never) { print("two") }

Before this proposal, foo(bar: fatalError()) would print "two" because the second overload was the only valid candidate. Under this proposal, it will continue to print "two" because the second overload is more specific; it is an exact match rather than a subtype match. The following case is trickier:

func foo(bar: @autoclosure ()->Int) { print("one") }
func foo(bar: @autoclosure ()->Never?) { print("two") }

Once again, before this proposal, foo(bar: fatalError()) would print "two" because the second overload was the only valid candidate. Under this proposal, neither candidate is an exact match. In order to maintain source compatibility, promotion to Optional must be ranked above Never subtyping when selecting overloads. To maintain compatibility, overload candidates relying on the Never-to-anything conversion are ranked lower than all overloads except those which are unavailable or marked with the private @_disfavoredOverload attribute.

Despite these complications, this proposal ultimately maintains source compatibility.

Effect on ABI stability

Because a value of type Never cannot exist at runtime, this change does not meaningfully impact the ABI. For example, because it is impossible to construct a value of type Never it cannot interact with dynamic casting, etc.

Effect on API resilience

This change has no impact on API resilience.

Alternatives considered

Make all trivially and/or structurally uninhabited types equivalent to the bottom type

One option which has been considered in the past is treating every uninhabited type as equivalent to the bottom type. This makes sense from a logical perspective, but it has the potential to significantly complicate typechecking and has few practical benefits. Additionally, enums with no cases are widely used as a namespacing mechanism, and are not intended to be used as the bottom type. Structurally uninhabited types (tuples with uninhabited element types, structs with uninhabited stored property types, etc.) introduce additional complications, like determining whether a type is equivalent to the bottom type across a resilience boundary. This proposal recommends that Never and any typealiases thereof remain the sole representation of the bottom type in Swift.

Address use cases one-by-one with tailored standard library APIs

Instead of making Never the bottom type, we could introduce new standard library API on a case by case basis to support some of the same use cases. For example, a new func ??<T>(lhs: T?, rhs: @autoclosure () -> Never) -> T overload could support the let x = y ?? fatalError() use case without changing the type system. However, as noted in the TODO() example, this approach has its limitations. It also unnecesarily increases the surface area of the standard library and introduces potentially confusing new operator overloads.

Future Directions

Expanded protocol conformances for Never

SE-0215 introduced Never conformances to Hashable, Equatable, Comparable, and Error. These conformances were intended to address the use of Never in many common contexts, like Result<..., Never>. If more conformances are desired in the future, there are a few potential directions we could take:

  1. Automatically conform Never to any protocol which doesn't have static or initializer requirements. Implementations of these requirements would be uncallable, so the implementations would be "uninhabited". There's some debate as to whether this makes sense from a semantic perspective. Additionally, introducing the concept of fully implicit conformances would significantly complicate the language and might not be worth the benefit.
  2. Automatically synthesize non-static, non-initializer requirements for any protocols Never explicitly conforms to, similar to the existing requirement synthesis for Hashable, Equatable, etc. This fits Swift's existing conformance model better, but requires some manual effort.
  3. Continue to introduce new Never conformances in the standard library on a case-by-case basis as needed.

Because this is a complicated design space which deserves careful consideration on its own, it's considered out-of-scope for this proposal.

Make control flow statements like throw, return, break, continue, and fallthrough expressions of type Never

Once Never is the bottom type, it's worth considering making control flow statements like throws expressions of type Never. Among other things, this would allow writing code like let x = y ?? throw SomeError(). This is another large design space which is out-of-scope for this proposal, but it presents a number of interesting possibilities.

54 Likes

It's wonderful to see this finally happening! I agree with the choices you made in the proposal. Thank you so much for working on this! :clap::clap:

1 Like

I hope this isn't too tangential, but to me this supports my belief that Swift would benefit from an explicit namespacing mechanism, instead of relying on caseless enums which serve that purpose more or less by coincidence, while their intent can be a bit ill-defined because an enum is a type and a namespace isn't.

18 Likes

The rest of the motivations notwithstanding, I do not see a benefit to have the long-form !. Discussion in other threads seem to just want to get rid of ! in this regard.

Providing the "reason" force-unwrap is safe is just as useful as a code comment, which doesn't require special syntax. The only real benefit would be that it can capture the context that causes the nil, but given that no one mention it, I'm led to believe that that's not what they have in mind.

3 Likes

The benefits do seem appealing, but I'll only support this if we agree to never deprecate !, no pun intended. :wink:

3 Likes

How does this interact with metatypes? Could you specify what new subtyping relations are being introduced by this proposal? For example, if we have T <: S, then do we also have T.Type <: S.Type? (One could ask this broadly but I suppose we could limit ourselves to T = Never here.). Do we have a rule that Never <: T.Type for all T?

As a result, this proposal retains the single-expression closure rewrite rule where necesary to maintain source compatibility.

Is the list of "where necessary" small? Could you list the places where this is necessary?

There is no exception for metatypes. Never should also be the bottom type of all metatypes.

1 Like

But foo(bar: fatalError() as MyIdentifiableType) should work, yes?

1 Like

Yes

That's not implementable. Metatypes of classes are specially laid out so that class objects can act as subtypes of their parent class's class object, in order to support class dynamic dispatch, but metatypes are not otherwise related to each other. Never.Type is also an inhabited type with Never.self as its single value; it doesn't make sense for it to have any other values. There should be no special case for it.

4 Likes

That's right, for all types T, Never <: T, but it should be spelled out more clearly in the draft. That includes metatypes in that Never <: Int.Type, Never <: MyClass.Type, Never <: Never.Type, etc. Like @Joe_Groff mentioned, however, Never.Type is not impacted by any of these subtyping relations because it is inhabited; Never.Type isn't really different from any other enum meta type.

6 Likes

As far as I'm aware, this is only a source compatibility concern when converting a single-expression closure literal to an @convention(c) function. This part of the proposal still needs more investigation though.

2 Likes

I‘m not sure I fully follow, can you maybe explain it in a way that would be easier to understand? Also did you meant Never.Protocol or Never.Type?

Never isn't a protocol. There's only Never.Type.

2 Likes

It's happening!

Expressions like let x = y ?? fatalError("y shouldn't be nil because ...") would be allowed to compile. This is essentially a long-form version of the force-unwrap operation,

Shouldn't this be preconditionFailure() instead of fatalError() to match the behavior of ! in all builds?

fatalError traps in release builds—it’s assertionFailure that is debug-only.

Ah sorry, brain fade, I guess I was already tired when I replied.

Thanks for the excellent proposal draft! I love to see changes the make the language more regular.

It might be useful to add a couple of sentences to the section on protocols addressing opaque return types. It would be good to clarify whether this is allowed:

var body: some View {
    TODO(“I can see my house from here”)
}
2 Likes

This sounds like an excellent addition, +1!

Good idea, I added an example to the proposal. Your example would compile because SwiftUI includes an explicit conformance of Never to View. In general this proposal doesn't allow that kind of construction though. For example:

var foo: some Bar {
    return fatalError() // error: return type of var 'foo' requires that 'Never' conform to 'Bar'
}

Similar to the generics example from the proposal, the following is allowed:

var foo: some Bar {
    return fatalError() as MyTypeThatConformsToBar
}

Edit: As a general rule, I think it's useful to compare this to the fact that while Never is a subtype of all types, it does not inherit their members. Similarly in this case, it does not inherit conformances either.

Terms of Service

Privacy Policy

Cookie Policy