Artisanal Objective-C Sum Types
A sum type combines many possible differently-typed values into a single value, expressed in Swift as an enum
.
In C, we can achieve the power of Swift’s enum
with a combination of a C union
and a C enum:
First, I’m sure you notice Swift’s enum
is more concise, but it’s also safer. In C, the compiler doesn’t prevent us from either creating an example with a mismatched type or from consuming a union as a mismatched type:
Trying to mismatch our enum
types in Swift won’t compile. Yay!
Unfortunately, in Objective-C, we can’t use Objective-C objects in structs or unions in ARC, so we can’t use our C sum type with Objective-C objects. However, we can build a similar construct by hand.
First, we need a class to capture all of the distinct types, and expose them through a single block-based callback interface:
We must declare forward references for our distinct types, which we’ll define later. We mark new
and init
as NS_UNAVAILABLE
so that consumers won’t be able to instantiate our base class directly. If they use -Wobjc-designated-initializers
, they won’t be able to subclass our base class either (without importing ExamplePrivate.h
, which we won’t distribute to them).
Next, we’ll declare our distinct types:
They’re simply plain-old-objective-c-objects that extend our base class. Our base class and our distinct types all have switchFoo:bar:
. The distinct types will simply call the respective callback block with self
. The base class will have an empty implementation. We use NS_REQUIRES_SUPER
on switchFoo:bar: because if we add another distinct type, we want all of our implementations of switchFoo:bar: on our distinct types to be warned (thanks to -Wobjc-missing-super-calls
). If we change the base class switchFoo:bar: to switchFoo:bar:quz
, but don’t update the distinct type implementations, none of our callbacks will be called. This use of NS_REQUIRES_SUPER is basically a hack to give us something close to Swift’s override
keyword.
We need to declare our base class’ NS_DESIGNATED_INITIALIZER
so we can implement our distinct types’ initializers. As I mentioned above, we’ll put these declarations in Example`Private`.h
, so our consumers won’t be able to directly instantiate or subclass Example. They can’t simply be declared in a .m
file because the compiler only warns about nullability completeness if we declare a method in a .h file.
Implementing the base class is pretty trivial:
Implementing the distinct types are also pretty trivial. Again, notice that we must call [super switchFoo:bar:]
so we’ll get a warning if we change the base class’ -switchFoo:bar:
.
Finally, we’ll declare a Switcher
object, which captures a generic type at a single call site for a type-safe return value. Objective-C allows us to assign values to local variables within a block, but it’s easy to accidentally miss an assignment:
This is contrived, but if you imagine four or five distinct types, you can see how it would be easier to miss an assignment. Using a proper return value is safer.
Unfortunately, Objective-C generics only capture types at a class declaration (unlike Swift generics, which can capture a type at any call site). Thus, we need to create a separate class to capture the return types for arbitrary call sites:
We still have the same risk about missing an assignment, but it’s only in one place in our entire codebase:
Now, the compiler will give us an error if we forget to return:
Now, we finally have the tools to build our Objective-C sum type. We can act on it directly:
Or we can transform its value and be guaranteed that every distinct type was properly processed with the correct type:
And we have the same type checking support Objective-C gives all of its method calls, so we don’t have to worry about passing the wrong values when we create a distinct type:
And we don’t have to worry about unpacking the wrong values from a sum type:
I realize this is a LOT of boilerplate code. The next step is to create a generator for these types since it’s all extremely mechanical, so stay tuned!
I had to put the code in separate gists in order to break it into readable chunks. The code is available as a proper Xcode project on GitHub.
Twitch is hiring! Learn about life at Twitch and check out our open roles here.