Improved Protocol-Oriented Programming with Untyped Type Aliases (part 2)

Michi Kono
Mar 30, 2016 · 8 min read

Protocol-Oriented Programming combined with generics introduces new strategies for writing great iOS software. They eliminate an entire class of bugs by allowing your team to catch bugs during compile-time instead of runtime.

In the last article, we learned how a typealias can serve as a generic when not explicitly set to a specific type. We will now review the new problems that are introduced when inheritance and classes are added to the mix. These problems are crucial to overcome if you plan to build on top of existing data structures that leverage generics. By the end, you should understand how to use the strategies covered to achieve common design patterns.

To follow along, it is expected that you know the basics around generics and type aliases. I recommend you copy and paste the sample code into Xcode as you read along, or you can use the accompanying Playground file.

You should really use the Playground file for this one. The examples and concepts are much more complex than the first article.

Inheritance and Generics

When attempting to perform inheritance with a generic class, things can get tricky. This might seem like something you wouldn’t do often, but you might see this in action when trying to extend a collection such as Dictionary or Array.

To illustrate, let’s create a Pet and Inspector concept.

class Pet {} 
class Inspector<P> {}

Inspector is a generic class that has an unused generic type called P. To be clear, the above Inspector<P> syntax is analogous to an Array definition:

class Array<P> {
// Returns a value of type P
func removeLast() -> P {
// ...
}
}

The <P> is a generic type that is specified during the class initialization. Just like an Array, you would use Inspector like this:

let inspector = Inspector<Pet>()

In this case, within Inspector, any instance of P would adhere to Pet’s protocol. Things feel a little strange when you extend a generic because you have to provide a second generic definition (the angled brackets):

class FurnitureInspector<C: Chair>: Inspector<C> { 
func getMaterials(thing: C) -> Wood {
return thing.mainMaterial()
}
}

It may not be immediately obvious that C is actually extending Chair in this example because Chair is concrete. This means Chair cannot be defined as a struct and must be a class. You may also be surprised to know that the generic definition provided to Inspector can be unrelated to FurnitureInspector’s:

class FurnitureInspector<C: Chair>: Inspector<Pet> {

Swift forces you to declare descendant classes as generics even if it means an unused generic. In this case, it is entirely possible you would not use C at all inside FurnitureInspector. When initializing the class, note the lack of <Pet> reference because it is handled within the previous class declaration:

let inspector = FurnitureInspector<Chair>() inspector.getMaterials(Chair())

An interesting thing happens when dealing with concrete types as a generic. You are able to omit their inclusion in the implementation. The previous two lines are equivalent to the following:

let inspector = FurnitureInspector() 
inspector.getMaterials(Chair())

Because Chair in <C: Chair> is a concrete class, it is inferred that it is being provided during the initializer. There are times where this scenario does not always involve a concrete class, which we will explore next.

A Type Alias as a Return Type

The return type of inspector.mainMaterial() is Wood because Chair was used, but what happens if it was Lamp instead? As in:

let inspector = FurnitureInspector() 
inspector.getMaterials(Lamp()) // <<< Error

This code errors because FurnitureInspector expects C = Chair. We need to alter the generic declaration so that it supports Lamp OR Chair. But it is more difficult than that; changing C’s type from Chair to Furniture is not enough. Pay special attention to this line in getMaterials():

func getMaterials(thing: C) -> Wood {

Do you see the problem? It is expecting Wood to always return, but we cannot guarantee that as soon as we change C to be something more generic than Chair (for example, Lamp is made of Glass). To get around this, we have to access the type alias directly (pay special attention to the C.M line):

protocol Furniture { // snippet from past example
typealias M: Material
func mainMaterial() -> M
}
class FurnitureInspector<C: Furniture>: Inspector<Pet> {
func getMaterials(thing: C) -> C.M {
return thing.mainMaterial()
}
}
let inspector1 = FurnitureInspector<Chair>()
inspector1.getMaterials(Chair())
let inspector2 = FurnitureInspector<Lamp>() inspector2.getMaterials(Lamp())

The change to <C: Furniture> forces us to provide a concrete class (as mentioned earlier). We also use the type alias name directly, which you can see as C.M. This correlates to this line in Furniture:

typealias M: Material

The C is defined in the FurnitureInspector and could have been any other name. In fact, Xcode should have autosuggested M as you typed in C:

Demonstrating that Xcode can understand properties of a generic type

Protocols, combined with inferred type aliases, can make keeping track of types confusing. Don’t fret. The compiler will almost certainly know exactly what is going on.

Constraining Generics using WHERE

So far, we’ve learned how to constrain a typealias. When implementing an actual generic, you are able to constrain it using the where keyword in a much more powerful way than what a typealias can do. For example, you can constrain the generic type based on the value of a typealias.

Let’s say we add a new typealias to Furniture called A:

protocol Furniture {
typealias A
func label() -> A
// rest of protocol ...
}

And then we implement label():

class Chair: Furniture {
func label() -> Int {
return 0
}
// rest of Chair class ...
}
class Lamp: Furniture {
func label() -> String {
return ""
}
// rest of Lamp class ...
}

Again, we are relying on Swift to infer the type of A. In this case, we have elected for different signatures between Chair and Lamp. Now for changes to the generic constraint:

class FurnitureInspector<C: Furniture where C.A == Int> {
func calculateLabel(thing: C) -> C.A {
return thing.label()
}
}
let inspector1 = FurnitureInspector<Chair>()
let inspector2 = FurnitureInspector<Lamp>() // <<< Error

In the above code, FurnitureInspector requires that C (instance of Furniture) implements a method label() that returns an Int. The second inspector fails because Lamp implements a label() method that returns something that is not an Int. Much like before, we could even constrain the protocol’s definition of A. For example, we can constrain it to Any just to show it can be done:

protocol Furniture {
typealias A: Any
func label() -> A
}

Protocols, combined with inferred type aliases, can make keeping track of types confusing. Don’t fret. The compiler will almost certainly know exactly what is going on. But it is a good practice to be careful when casting objects involving generics. The obvious example of this in the wild is when dealing with dictionaries and arrays of mixed types (e.g., an Array of Any).

Implementing Patterns

In the previous article, we looked at objects in charge of initializing themselves in a static method (for a Factory pattern). What about in the case of a separate class or service being delegated this responsibility? For example, if you were building an ORM, you might have a ModelFactory that spits out objects implementing a Model protocol (e.g., UserModel). How well does Swift handle things then?

Here is an example of a service responsible for building furniture:

////// Error
class FurnitureMaker<C: Furniture> {
func make() -> C {
return C()
}
func material(furniture: C) -> C.M {
return furniture.mainMaterial()
}
}

The above code does NOT work, but we’ll fix that. It’s actually very close to working as is. This class will do two things:

  1. Build and return Furniture objects. This is a simple example, but you can imagine the make() method accessing a service registry, for example, to return instances appropriate to the environment, such as during testing.
  2. material() acts as a delegator for calling mainMaterial(). Again, we can think about how material() might encapsulate other logic such as logging or aggregating more complex behavior.
Illustrating the error shown when attempting to use a default constructor for C (an implementation of Furniture)

The above code breaks because C does not have any initializers (init() method) according to Xcode.

In order to fix the above code, we must add it into the protocol:

protocol Furniture {
init()
// rest of protocol ...
typealias M: Material
func mainMaterial() -> M
}

Then we need to add init() declarations to each Furniture definition:

class Chair: Furniture { 
required init() {}
func mainMaterial() -> Wood {
return Wood()
}
}
class Lamp: Furniture {
required init() {}
func mainMaterial() -> Glass {
return Glass()
}
}

Now we can use our factories:

let chairMaker = FurnitureMaker<Chair>()
let chair1 = chairMaker.make()
let chair2 = chairMaker.make()
chairMaker.material(chair2) // returns Wood
let lampMaker = FurnitureMaker<Lamp>()
let lamp = lampMaker.make()
lampMaker.material(lamp) // returns Glass

Earlier, we discovered that if a concrete class is used in a generic class declaration, we can omit it during initialization. We can actually clean up the implementation at the expense of the class declaration:

class ChairMaker: FurnitureMaker<Chair> {}let betterChairMaker = ChairMaker()
let chair3 = betterChairMaker.make()
let chair4 = betterChairMaker.make()
betterChairMaker.material(chair4) // returns Wood

The caller does not need to worry about passing in Chair each time they create a FurnitureMaker because ChairMaker implicitly handles it for them. You might write something like this if you were creating a model service, for example.

(Note that in Swift 1.x, you need <C:Chair> added to ChairMaker’s declaration)

Wrapping Up

We covered how inheritance showcases a few quirks in generic classes in Swift. We also learned that the compiler keeps track of much of this complexity for you. Finally, we learned about how to restrict a generic or typealias to a subset of types. Using this knowledge, my hope is that you can achieve better abstraction in your code. Many of these concepts are universal across typed languages.

At Capital One, we are excited at the new constructs that Swift enables. We strive to leverage the latest and greatest technologies and are working hard to give back to the community in the form of insights, knowledge, and open source. I hope this article demystifies for you some of the darker corners of Swift, but if you have any questions, please reach out!


For more on APIs, open source, community events, and developer culture at Capital One, visit DevExchange, our one-stop developer portal. https://developer.capitalone.com/

Capital One Tech

The low down on our high tech from the engineering experts at Capital One. Learn about the solutions, ideas and stories driving our tech transformation.

Michi Kono

Written by

Techie. Engineering @CapitalOne.

Capital One Tech

The low down on our high tech from the engineering experts at Capital One. Learn about the solutions, ideas and stories driving our tech transformation.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade