Eliminating Impossible States with Never
Swift has a very useful type called Never
in the standard library. It’s defined very simply:
public enum Never {}
That might seem strange. What kind of enum
has no case
s?
This is an example of an uninhabited type—a type that doesn’t have any valid values. There’s no way to create a Never
value. That’s a useful property for proving things in the type system. Uninhabited types let us make things impossible.
When it was introduced in SE-0102, Never
was framed as a useful return value for functions. Before then, Swift functions that never returned, like fatalError
, were marked with a @noreturn
attribute. By declaring fatalError
to return Never
instead, you’ve proven that it never returns. Since there’s no way to create a Never
, there’s no way to return one—so you’ve proven that the function can never return.
public func fatalError(
_ message: @autoclosure () -> String = String(),
file: StaticString = #file, line: UInt = #line
) -> Never {
…
}
This isn’t the only place that uninhabited types like Never
are useful.
Types help us by restricting the number of possible values. The clearest example of this may be the Result
type:
enum Result<Value, Error> {
case success(Value)
case failure(Error)
}
Without Result
, asynchronous, failable APIs look like this:
func run(completion: @escaping (Value?, Error?) -> ()) { … }
This API is susceptible to bugs because two impossible states can be represented by the types. There should never be (1) both a value and an error or (2) neither a value nor an error. But since it’s possible to represent those in the types, the run
function and its caller can both make that mistake.
run { value, error in
switch (value, error) {
case let (value?, nil):
handleSuccess(value)
case let (nil, error?):
handleError(error)
case (_?, _?):
fatalError("Can't have both a value and an error")
case (nil, nil):
fatalError("Can't have neither a value nor an error")
}
}
Result
eliminates those impossible states. By making the impossible states impossible, potential bugs are eliminated.
run { result in
// With `Result`, no `fatalError`s are necessary.
// We've made those impossible states impossible.
switch result {
case let .success(value):
handleSuccess(value)
case let .failure(error):
handleError(error)
}
}
Uninhabited types let us go a step further. We can mark case
s of an enum
as impossible, essentially erasing them from the type:
// This can't fail because you can't create a `Never` to use
// for a `.failure`
// This is basically equivalent to declaring `value` as an `Int`
let value: Result<Int, Never> = …
switch value {
case let .success(value):
…
// A failure case isn't required because it's impossible
// This is actual working Swift code
}
// This always fails because you can't create a `Never` to use
// for a `.success`
// This is basically equivalent to declaring `error` as an `Error`
let error: Result<Never, Error> = …
switch error {
// A success case isn't required because it's impossible
case let .failure(error):
…
}
That doesn’t make much sense if you’re just declaring local variables with Result
: you could just declare your Int
or Error
directly. But it becomes important once you start using associatedtype
s, generics, or a type that adds effects (e.g. an FRP observable).
Take this Elm-inspired description of a UI, e.g.:
// Something approximating The Elm Architecture
// `View`s have `Message`s that can be sent back to the `Component`
protocol Component {
associatedtype Message
mutating func update(_: Message) -> Command?
nonmutating func render() -> View<Message>
}
// You can create a `Button<String>`,
// but you can't create a `Button<Never>`
// because you couldn't set its action
struct Button<Message> {
var action: Message
var title: String
}
// You can create a `View<String>.button`,
// but you can't create a `View<Never>.button`
// because you can't create a `Button<Never>`
enum View<Message> {
case button(Button<Message>)
case text(String)
}
Using Never
, we can prove that some views aren’t interactive, i.e. that they contain no buttons. A View<Never>
can’t contain a Button
, or any other type of view with an action, because those views can’t be created. That means we can restrict portions of our view hierarchy to non-interactive views.
Let’s say, for instance, that we’ve created a document viewer. We want to render documents to views, but those views should always be static—users shouldn’t be allowed to create buttons. We can enforce that by rendering documents to a View<Never>
:
// Our spec says that documents should never contain interactive
// elements. Returning `View<Never>` here proves this in the type
// system. It's impossible to accidentally return a `Button` now.
func render(_ document: Document) -> View<Never> { … }
We may also want to export documents to HTML. We decide that writing a second renderer would be difficult, but it’s not so bad to convert each View
to an HTML
type. Ordinarily, you might write this generically over the message type:
func export<Message>(_ view: View<Message>) -> HTML {
switch view {
case .button:
// `fatalError`, like `!`, means you haven't
// proven something in the type system
fatalError("Documents can’t contain buttons.")
case let .text(text):
return HTML.text(text)
}
}
Unfortunately, while we know that we’re exporting documents and that documents never contain buttons, we haven’t proven that, so the compiler doesn’t know. We have an impossible state that can be represented in the types, so we have to resort to fatalError
.
We can prove that we’ll never try to export a button by using Never
again:
func export(_ view: View<Never>) -> HTML {
switch view {
case let .text(text):
return HTML.text(text)
}
}
By using Never
, we can completely get rid of the case .button
! The Swift compiler knows that you can never create a View<Never>.button
, so you’re not required to include that case
in your switch
. Now someone can’t accidentally call export
on an interactive view.
One problem remains: we’ll want to use our rendered document, a View<Never>
, inside a View<Message>
. This can be done by using another switch
statement like the one above above:
extension View where Message == Never {
func promote<M>(to _: M.self) -> View<M> {
switch self {
case let .text(text)
return .text(text)
}
}
}
We only need to unwrap and rewrap our elements to be in the right View
type. And we don’t need to include a case button
.
Now we can fully render our UI:
func render(_ document: Document) -> View<Never> { … }
extension View where Message == Never {
func promote<M>(to _: M.self) -> View<M> { … }
}
func render(_ state: State) -> View<Message> {
return View.stack([
render(state.document).promote(to: Message.self),
.button("Export", Message.export),
])
}
Never
let us eliminate impossible states:
render(_: Document)
can’t return interactive elements.export(_: View<Never>)
doesn’t need to handle interactive documents.- You can’t accidentally call
export
with an interactive view
This same approach can also be used with a protocol’s associatedtype
—fulfilling the protocol, but eliminating impossibilities within specific types that conform to it.
Types are useful because they restrict possible values. Never
can help us by eliminating some of those values, restricting the possible values even further. Proving that they’re impossible in the type system gives us confidence in the code that we write.