Express Impossible Code in Swift with Never
Tuesday May 7, 2019
Some time ago, while digging through the RxSwift source code, I first encountered Never. At the time, I filed it away in my brain under the “weird — look into that” compartment. It wasn’t until recently that I had a chance to revisit it. It’s a neat little concept, but more importantly, useful! We’re now using it in production code. This article will help you understand Swift’s Never type and it’s usefulness via two real-world examples.
What is Never?
The quick and dirty: Never expresses impossibility via the type system. There are two main uses:
- Express when code cannot be run or called
- Express when a function cannot return
This article will dive into the first point — which is what I refer to as impossible code. I’ve found the second point regarding non-returning functions to be less practical and not as interesting to discuss here and will instead defer to Swift’s fatalError as an example.
What makes Never interesting is that it’s an uninhabited type —it cannot be instantiated. Just look at it’s implementation:
It’s an enum with no cases. There’s no way to instantiate it — but the type exists and it’s available for use. In fact, we can easily create our own uninhabited type in the same way. This becomes valuable as the Swift compiler takes advantage of this fact to help us be more expressive within the type system.
Void and NSNull
Void and NSNull instances can represent the absence of a value. And the absence of a value is possible, it’s a real thing. It can be essentially thought of as representing an occurrence or something happening — an event. For instance, I’d like to know when the rain stops but I don’t care about how much rain fell. Void would work well for this.
Never, on the other hand, can’t do this because it can’t be instantiated. When it’s used to define a function or variable, or more practically, to name a generic type, it can make code impossible to run. But why would anyone write code that would never run? It might seem strange at first, but it comes up when working with generics and it can help us avoid resorting to fatalError or developer comments like // No need to do this.
Let’s look at some examples of impossible code.
Networking with Never
This example will use Never in a networking implementation that uses generics. We’ll start with Swift’s Result and build on that in the subsequent networking code.
Swift’s Result (now built into Swift 5) is a generic enum that can help showcase some impossible code. It is defined as:
Here it is being used for a name fetching routine:
What would an instance of Result<Never, Error> mean? It would mean it must have failed. Since it is impossible to create an instance of Never, it would be impossible to create the .success case here. Here’s a slightly different example:
*In case you’re wondering, in Swift 5, Never already conforms to Error.
The .failure case in the switch isn’t required as the compiler knows it’s impossible to create it. Thus, there is no need to use fatalError or an instructive // This should never happen comment. In fact, it would be helpful for the compiler to issue an “Impossible to execute” warning if the .failure case was ever written.
Handling Impossible Responses
Using the previous example as a building block, let’s describe networking requests using an abstract and generic Request type:
Request has a generic associatedtype, Response, which is used to describe the type of object that will eventually be provided back to the caller. When the network call finishes, completion is called with a Result<Response, Error>.
Suppose we want to implement a user analytics network call. For this call we don’t care about knowing when it succeeds however, when it fails, we’d like to log it so that we can fix it later.
TrackScreen1 implements this without Never:
Did you notice how comments are required to help others understand the requirement and protect against developer error? This is because the code alone doesn’t express the requirement of not caring about the success case.
With Never, we can directly encode the requirement as shown here in the improved TrackScreen2:
Here, in send, there’s no need to worry about developers calling completion because it’s impossible to create the .success case. Further, when the caller handles the result, the .success case can be omitted from the switch.
Never has provided a type-safe way to express the requirement. It gives some flexibility to describe other scenarios like fire-and-forget calls — calls that don’t need any completion handling whatsoever. Not to mention, it facilitates cleaner code.
Coordinators with Never
If you’ve ever used a “coordinator” pattern for describing navigation flows in an iOS app, this example might hit home a bit more. Here’s a quick overview of how we’ll interpret the pattern.
A coordinator can be thought of as being responsible for a particular user flow. For example, there might be a SettingsCoordinator which shows the user’s settings. In that particular flow, tapping the “Add Payment” button might start a child flow, AddPaymentCoordinator, to add a payment method.
Parent coordinators generally need to know when a child finishes so it can react accordingly. The child might also need to pass data back to its parent when it finishes. Given these requirements, here is the coordinator protocol:
The settings and payment example described above might look like:
It turns out that some coordinators never finish. The best example of this is the very first, or root, coordinator. It introduces the app and spawns child coordinators, but it’s impossible for the user to “finish” it.
Let’s look at how this AppCoordinator1 looks like without using Never:
Just like our earlier networking example, helper comments are required to express the requirement of this coordinator never finishing.
Here is the improved AppCoordinator2 using Never:
The implementation is cleaner without the helper comments while still being expressive. Even though it’s impossible to execute the completion closure, it’s still required when calling start. Ideally, the compiler would warn us if code was ever written inside the closure.
This post explores Swift’s Never via two real-world examples. It illustrates how we can write impossible to execute code — expressed via types. This reduces our need to use fatalError, and // This should never happen comments. Writing expressive, clean code is important for building quality products that are maintainable. Moreover — it feels good!
I’d like to thank Mark Collette for helping craft the examples, Lisa Tran for editing, and Katie Luke for the artwork.