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
- Proposal: SE-NNNN
- Authors: Owen Voorhees
- Review Manager:
- Status:
- Implementation: (WIP)apple/swift#31327
Introduction
This proposal makes Never
the bottom type, or universal subtype in Swift.
Swift-evolution thread: Link
Relevant past discussions of Never
:
- Deprecate ! and make Never the bottom type
- [Pitch] Never as a bottom type
- [Review] SE-0102: Remove @noreturn attribute and introduce an empty NoReturn type
- [Pitch] Introducing the "Unwrap or Die" operator to the standard library
- SE-0215 - Conform Never to Equatable and Hashable
- Uninhabited Type (Never) Conversions
- Combining ? with throwing when there's no reasonable default
- Scala like placeholder for types
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 aguard 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 returningNever
would be able to satisfy the@main
attribute requirement. This would allow the authors of frameworks providing defaultmain
implementations to more clearly specify whether they terminate to clients who call them directly. Similarly, overriden members could useNever
as a contravariant parameter type or covariant return type. -
As part of a future proposal, control flow statements like
throw
could become expressions of typeNever
, 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:
- Automatically conform
Never
to any protocol which doesn't havestatic
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. - Automatically synthesize non-
static
, non-initializer requirements for any protocolsNever
explicitly conforms to, similar to the existing requirement synthesis forHashable
,Equatable
, etc. This fits Swift's existing conformance model better, but requires some manual effort. - 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.