Cocoaphony

The Littlest Type

Sometimes there’s code so commonplace that we forget how strange it actually is. I mean, Swift is a strongly typed language right? Types types types! We say what things are, and the compiler enforces it for us. But then you see some piece of code like this:

func addOne(_ x: Int) -> Int {
    fatalError("Haha! No Int for you!")
}

That’s legal Swift. I don’t think many would find that surprising. Of course that’s legal, right? But it is surprising. addOne claims to be a function that accepts an Int and returns an Int. It does accept an Int, but…it doesn’t return an Int.

“Don’t be silly, Rob. It crashes. It can’t return an Int if it crashes.”

Well, yeah. But it promised to return an Int. It didn’t promise to “return an Int or crash,” did it? The whole point of strong types was that the compiler would enforce our promises, and the compiler doesn’t bat an eye at this code.

“The compiler can’t possibly know everything that might crash.”

I’m not ready to concede that, but even so, the compiler clearly can see that this function doesn’t return an Int. There’s no return intValue anywhere. This should prick our ears a bit. Something is strange here. Is it just compiler magic, or is there something deeper?

Hint: There’s something deeper.

Staring into the Void

Let’s take a step back and think about another thing that should surprise us.

let printSquid: () -> Void = { print("🦑") }

So, printSquid is a closure that takes no parameters and returns…what?

You might be tempted to say “nothing.” But that’s not what the code says. It says it returns Void. What’s Void?

public typealias Void = ()

Void is a real thing. It’s the type of the empty tuple. It’s not “nothing.” In Swift, the type () and the value () happen to have the same spelling. When we mean () as a return type, we traditionally write it using the typealias Void. But () -> () is exactly the same thing as () -> Void. Why “Void?” Because Swift has ObjC roots, and ObjC is C-like, and in C there are void functions that literally return nothing.

In Swift, every function returns something. There are functions that return (), and there’s some syntactic sugar that inserts -> Void and return () for you in some cases. But you’re free to include them if you like (it’s not particularly idiomatic Swift, but it’s legal).

func f() -> Void {}
func g() { return () }

And functions like print that seem to have no return value, really do return Void:

let printResult: Void = print("🦑") // 🦑
print(printResult)                  // ()

If we don’t add the : Void here, the compiler will emit a warning because you usually don’t want Void values, but they’re totally legal.

The measure of a type

Each type has a set of values that are part of that type. Some types have an enormous number of values. Int has around 18 quintillion values. String has even more. And some types have very few values. Most enums have just a handful of possible values. Bool has just 2. A lot of the power of strong types is that we can create small types; types that can only hold exactly the values that are legal for our program. That’s why we prefer enums to String and Int unless we mean “arbitrary text” or “an arbitrary number,” and we prefer structs to Dictionary unless we mean “an arbitrary mapping of keys to values.” Smaller, more constrained types help the compiler help us.

So how small can a type get?

Void has just one value. So that’s pretty small. In most languages that have this type, it’s called Unit. The one-value type.

Can they get smaller?

Not a value in sight

It’s time to get back to the original example:

func addOne(_ x: Int) -> Int {
    fatalError("Haha! No Int for you!")
}

Looking at fatalError, it feels a lot like print. The syntax is similar. But we can’t just throw print into places fatalError goes:

func addOne(_ x: Int) -> Int {
    print("Haha! No Int for you!") // Cannot convert return expression of type '()' to return type 'Int'
    ^^^^^
}

How does the compiler know we’re allowed to use fatalError here, but not print? Maybe the compiler just knows that fatalError is special. That’s what was done prior to Swift 3:

@noreturn public func fatalError(...)
^^^^^^^^^

The @noreturn attribute told the compiler that this function doesn’t return, and then the compiler had logic to handle that case and let us skip returning the Int we promised. But…bleh. I hate that solution. It opens up weird corner cases. For example, what happens if we add @noreturn to a function that we claim does return something, or something that throws:

@noreturn func addOne(_ x: Int) -> Int { ... }
@noreturn func runForever() throws { ... }

The first one is probably an error, and maybe the compiler should forbid it. But what about the second one? Is throwing a “return?” At the implementation level, it actually is. But should this attribute allow it? There’s not a really obvious answer. Is it possible for a function to require a @noreturn parameter? How does this impact function overloading?

In Swift 3 they got rid of the attribute hack, and solved the problem with a type: Never. The signature of fatalError is now:

public func fatalError(...) -> Never
                               ^^^^^

So what’s Never? Is it some new compiler trick? Nope. It’s just a type, an enum with no cases:

public enum Never {}

How many Never values are there? Well, none. You can’t construct one. That has all kinds of interesting implications.

func f() -> Never { ... }   // f never returns
func g(_: Never) { ... }    // g can never be called

struct S {                  // S can never be constructed
    let nope: Never
    ...
} 

enum E {
    case ok(Int)       // E.ok can be constructed
    case nope(Never)   // E.nope can never be constructed
}

// But also interesting:

struct G<Element> {}
let ok = G<Never>()     // This is fine. Never can be a phantom type.

Another interesting implication is that [Never] is an empty array.

Never is the smallest possible type. We call it an “uninhabited type.” There’s nothing special about the name “Never.” You can create your own no-case enum, and it’ll work the same.

// Our own custom uninhabited type
enum NeverReturn {}

func neverReturn() -> NeverReturn {
    // We could call fatalError() here, since generating any uninhabited type
    // is sufficient to create any other. But we can also use an infinite loop.
    // The compiler can prove this will never return. Never isn't just for crashing!
    while true {}
}

func addOne(_ x: Int) -> Int {
    neverReturn()   // It's fine not to return Int, because the compiler knows this doesn't return
}

// While Never can be used to create a NeverReturn, they're not the same type
let never: Never = neverReturn()           // Cannot convert value of type 'NeverReturn' to specified type 'Never'
let neverEver: NeverReturn = fatalError()  // Cannot convert value of type 'Never' to specified type 'NeverReturn'

While it’s possible to create your own uninhabited types, I don’t really recommend it. The Swift team considered having different types for things like “exit” vs “abort” and intentionally chose not to. One uninhabited type is probably plenty. But it’s nice that it’s not some magical name.

You never hit bottom

In type theory, an uninhabited type is often called a bottom type, and written as ⊥. A bottom type is a subtype of every other type. So Never would be an Int and a String and a UIViewController and every other type. The opposite is the top type (⊤), the supertype of every other type. In Swift, that’s Any.

But in Swift, Never isn’t actually a bottom type. If it were, you could write this, and you can’t:

let x: Int = fatalError()   // Cannot convert value of type 'Never' to specified type 'Int'

It’s easy to fix this with a little extra syntax if you need it:

let x: Int = { fatalError() }() 

So Never acts like a bottom type when it’s being returned from a function, but not when it’s being passed to a function or assigned to a variable. It would be more consistent for Never to be a true bottom type, and for it to conform to every non-static protocol (i.e. protocols without static or init requirements). Whether that’s worth the weird corner cases it might create, I’m not sure. But maybe.

Some things just don’t happen

Never is my favorite type in stdlib. It’s been my favorite type since it was introduced in Swift 3, and the Combine framework has completely justified my love of it by applying it to generics.

Publishers generate a series of values or a typed failure:

public protocol Publisher {
    associatedtype Output
    associatedtype Failure : Error
    func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

But what should Failure be if the Publisher never generates errors? Never, of course. No magic needed. It just works. What if it only can generate a Failure? Well, then Output is Never (see IgnoreOutput). When there’s a proper type, the special cases disappear, and it just works.

So Never has gone from a little-known type solving a little-known problem around fatalError to something Swift developers will probably use every day without even thinking about it. And that makes me very happy.