This introduction to generic types, protocols, and enums in Swift is part 2 of a series on Swift for Objective-C Developers. Check out part one about Optional Values here.
This post assumes you are familiar with the idea of generics, but either haven't used them in practice or have been burnt by other languages' (looking at you, C++) implementations and are reluctant to embrace them in Swift.
In Objective-C we have the almightly `id`, the basic representation of all object types in the language. We often use it as a catch-all when we don't necessarily know the type of an object we're going to be working with. Most often, it is used as the type for objects managed by a collection. Because we do not want to have a different class for every object type we want to contain in an array we use an array type that accepts objects of type `id`, so we can use any type we like. This is certainly better than having multiple classes such as NSStringArray, NSNumberArray, or NSIndexPathArray, etc. for each type we want to place in a container.
But, we can do even better!
There are very obvious flaws to this approach. It is important to remember that the Foundation collection classes (NSArray, NSDictionary, NSSet, etc) don't do this because it's the correct way, but rather because it is the best compromise given the limitations of the language.
There are three basic problems to this approach.
- Your code is incapable of being self-documenting. It is impossible to know what is being held inside of an NSArray instance simply by knowing that is an NSArray. If we had an NSStringArray class we know just by looking at it that there are instances of NSString being contained by the array. This is the type of self-documenting behavior we want.
- Type errors are introduced at runtime instead of compile time. Because methods such as `insertObject:` take an `id` instead of an object of a specific type, the compiler will never complain about the type of object you pass it, even if you pass an object of a type you did not intend. This means you may not catch the error until the code is running. When writing large apps with complicated code paths it is very common for these sorts of errors not be caught right away. And it can be particularly hairy when refactoring your code, especially when there are some infrequent code paths that you will inevitably forget about.
- It increases the amount of state in your application. When the compiler cannot dictate the correctness of the types in your code it becomes your responsibility. You must write code to modify the behavior of your logic based on the type of a particular object. Your application's flow becomes dependent on you maintaining the correctness of this logic without the help of the compiler to tell you when you screw up. Like runtime errors, this becomes a bigger issue while refactoring.
It isn't very difficult to understand why lacking type information is problematic. But how can we fix this? And how do generics fit into the equation?
Well, we fix these problems by giving our objects types. We simply have to compose types in order to create ones that are appropriate for our objects' requirements.
We can do this in Swift by utilizing generics and abstracting out a common interface for the objects we want to handle similarly.
As a concrete example, let's consider an online store app that displays a list of available products.
We could create a type for a Product itself.
But this can quickly become out of hand as you introduce different types of pruducts. Perhaps your store sells bikes and you'd like to have your user interface reflect the type of bike it is: a road bike, a mountain bike, a hybrid commuting bike, etc.
Now we have:
Your store also sells skateboards and you'd like to display the width of the deck, so you'll need to store that information as well.
You can see that this Product type is quickly becoming out of hand and becoming responsible for more information than the type was originally meant to represent.
The next obvious step is to create types for each of different products.
But now we're back where we started and we're duplicating information. Both of these types have a `name` and `price` property. Additionally, we can no longer store objects that may be of either type in one collection.
Both of these problems can be solved by parameterization of the common properties between these two products.
Doing this now gives us a new type, `Purchasable`, which we can use to refer to any object whose type implements the `Purchasable` protocol.
Using generics, we can now create an array whose type only allows objects that are `Purchasable`, in our application they can be either `Bike`s or `Skateboard`s.
As you can see, using generics we were able to create a whole new type, `[Purchasable]` (or with the alternate syntax: `Array<Purchasable>`), to represent an array of objects that are Purchasable. We immediately know looking at the type what it is and what we're holding inside of it. The Swift Array class abstracted out its logic using generics and now we get the perks of having specific array classes for a specific type (like the NSStringArray mentioned earlier) without having to write a new class manually for each type.
Additionally, this allows us to write code that restricts its own exposure to type information, limiting its responsibility to subset of the information that it actually cares about. For example, if we were to write a function that calculates the tax for a particular set of products, the function does not need to know anything about the product other than its price and therefore that is the only information the code using it should be exposed to. Because we have separated the `price` property into a protocol we can do so without our function caring whether the product is a Bike or a Skateboard.
Another type of problem we often use `id` to solve in Objective-C is the case where an object can be of multiple types and the instances aren't known at compile time. For example, consider a JSON object. Let's say we working with JSON objects that can be strings, integers, or booleans. We need a way to encapsulate all of these types so that we can write code that works generically with "JSON objects".
In Swift we can do this by creating a `JSONObject` type that is the composition of it's possible subtypes (String, Int, Bool) combined using an Enum.
Note that in Swift, the name of an Enum case cannot be the same as the type of its associated values, hence the more verbose names in this example.
This allows us to create objects whose type is `JSONObject`, but can represent specific values of other types without needing to explicity test which kind of value it is. More concretely, we are able to replace Objective-C code such as:
with Swift code like this:
This is better because `object` is adhering to a contract that can be checked by the compiler. It has a strict set of value types it can represent and the code reflects that exactly.
There are other implicit benefits to using method as well. For example, if you were to add or remove cases from the Enum, the compiler would alert you that you need to update the behavior of the switch statement to reflect the change. In the Objective-C code above, if you modify the behavior of the application such that the object can never be a number, you now have dead code in your application increasing its technical debt.
Swift's generics, in combination with protocols, allow a new level of abstract that was previously unavailable to Objective-C developers. The most obvious benefit is the introduction of typed collections as a first class citizen in the language but as we've seen above they play in an important role in making your own code safer and more correct. Similarly, Enums are a powerful feature when working with objects that can be of multiple types while maintaining an acceptable level of type safety.
Overall, these are very welcome additions to our programming repertoire.
If you enjoyed this post, follow me on Twitter.