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

(This is a post I made on Capital One’s tech blog.)

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.

This is the second of a two-part series that explores the topic of using a typealias as a generic inside protocols. I recommend you copy and paste the sample code into Xcode as you read along, or you can use the accompanying Playground file.

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!

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

(This is a post I made on Capital One’s tech blog.)

Protocol-Oriented Programming is crucial to writing great Swift code. But the reusability of a protocol is greatly restricted because Swift explicitly disallows adding generics to one. This can be overcome by using an untyped (or implicitly typed) typealias as a generic — something most Swift developers have never seen.

This is the first of a two-part series that explores the topic of using a typealias as a generic inside protocols. I recommend you copy and paste the sample code into Xcode as you read along, or you can use the accompanying Playground file.

A Generic Crash Course

Before we can explore the purpose of an untyped typealias, we will briefly discuss generics to ensure all readers can follow along.

A generic lets you reuse code across different types. It accomplishes this by limiting a variable to a specific protocol at compile time. This means avoiding run time checks (let … as). For example, imagine two different types that needed to “add one” that were structurally similar:

extension Int {
  func addOne() -> Int {
    return self + 1
  }
}
extension String {
  func addOne() -> String {
    return self + “1”
  }
}

Then, we wrap these in an increment method that works with both interfaces:

func increment(foo: Any) -> Any {
  if let fooStr = foo as? String {
    return fooStr.addOne()
  }
  else if let fooInt = foo as? Int {
    return fooInt.addOne()
  }
  fatalError(“unknown type can’t addOne”)
}
increment(“foo”)
increment(1)
increment(1.0) // <== this causes a runtime error

This particular code is dangerous because you wouldn’t catch the issue while compiling (i.e., this could become a production bug). Another approach might leverage a generic. First, you would add a protocol to the previous extensions:

protocol CanAddOne {
  func addOne() -> Self
}
extension Int: CanAddOne {
  func addOne() -> Int {
    return self + 1
  }
}
extension String: CanAddOne {
  func addOne() -> String {
    return self + “1”
  }
}

And now you could rewrite the increment method to check if the argument conforms to CanAddOne:

func incrementBetter<T: CanAddOne>(foo: T) -> T {
  return foo.addOne()
}
incrementBetter(“foo”)
incrementBetter(1)
incrementBetter(1.0) // <== compile error

The T represents any type that conforms to CanAddOne. We now gain compile-time checking for code that previously required runtime checks. This concept is leveraged heavily in situations where we only need a subset of functionality from an object or objects. This is most readily seen when working with Arrays or Dictionaries, but is also extremely common in Cocoa.

The problem is that generics and protocols do not play well together in Swift 2. To use them together, you must use a typealias instead. From this point forward, you should understand the basics around generics.

Inferring a typealias

Did you know you could write a typealias in a protocol without specifying what it is an alias for? For example, in the following code, M is inferred in the implementation. Notice how Chair and Lamp have different return types for the methods in question.

protocol Furniture {
  typealias M
  func mainMaterial() -> M
  func secondaryMaterial() -> M
}
struct Chair: Furniture {
  func mainMaterial() -> String {
    return “Wood”
  }
  func secondaryMaterial() -> String {
    return “More wood”
  }
}
struct Lamp: Furniture {
  func mainMaterial() -> Bool {
    return true
  }

  func secondaryMaterial() -> Bool {
    return true
  }
}

The above code assumes M (which could have been any unused identifier) is unknown within the protocol but should be consistent once inside any implementation. So in this example, changing only one return type would cause an error:

struct Stool: Furniture { // <<< does not conform to Furniture
  func mainMaterial() -> String {
    return “Wood”
  }
  func secondaryMaterial() -> Bool {
  return false
  }
}

In the above examples, M can be anything, but we can actually constrain it. For example, let’s create some structures representing materials.

protocol Material {}
struct Wood: Material {}
struct Glass: Material {}
struct Metal: Material {}
struct Cotton: Material {}

Now we modify our Furniture protocol for M to conform to Material:

protocol Furniture {
  typealias M: Material
  func mainMaterial() -> M
  func secondaryMaterial() -> M
}

Our Chair and Lamp structures immediately complain about not conforming to the protocol. Here’s an example of a fixed Chair structure:

struct Chair: Furniture {
  func mainMaterial() -> Wood {
    return Wood()
  }
  func secondaryMaterial() -> Wood {
    return Wood()
  }
}

Swift correctly recognizes that the intended value of M is Wood, an implementation of Material. If you want mainMaterial() and secondaryMaterial() to return different types, you would need to change the Furniture protocol accordingly:

protocol Furniture {
  typealias M: Material
  typealias M2: Material
  func mainMaterial() -> M
  func secondaryMaterial() -> M2
}

The other slightly unintuitive thing is that neither mainMaterial() nor secondaryMaterial() can be declared to return Material. For example, the following code will not work:

struct Chair: Furniture { // <<< does not conform to Furniture
  func mainMaterial() -> Material {
    return Wood()
  }
  func secondaryMaterial() -> Material {
    return Wood()
  }
}

Type Alias for Self

Swift 2.0 prevents protocols from containing aliases that reference themselves; however, in Swift 1.2, the mentioned pattern is valid. This type of constraint in a generic is useful in patterns such as Factory. Let’s examine how we might add a static factory method to Furniture’s protocol in Swift 1.x+:

protocol Furniture {
  typealias M: Material
  typealias M2: Material
  typealias T: Furniture
  func mainMaterial() -> M
  func secondaryMaterial() -> M2
  static func factory() -> T
}

In Swift 2+, there are two approaches to self-referencing protocols. The first is to create a separate protocol entirely.

protocol HouseholdThing { }
protocol Furniture: HouseholdThing {
  typealias M: Material
  typealias M2: Material
  typealias T: HouseholdThing
  func mainMaterial() -> M
  func secondaryMaterial() -> M2
  static func factory() -> T
}

In this protocol, we are now expecting factory() to return a HouseholdThing. Here’s how this might look on Chair:

struct Chair: Furniture {
  func mainMaterial() -> Wood {
    return Wood()
  }
  func secondaryMaterial() -> Cotton {
    return Cotton()
  }
  static func factory() -> Chair {
    return Chair()
  }
}

While this code models a very popular design pattern (Factory), it doesn’t quite work as you would expect. There is a major drawback in Swift regarding how typealias (as opposed to a true generic) seems to work: it is not possible to force a method to return an instance of itself. To better illustrate, look carefully at the same method in Lamp:

struct Lamp: Furniture {
  func mainMaterial() -> Glass {
  return Glass()
  }
  func secondaryMaterial() -> Glass {
    return Glass()
  }
  static func factory() -> Chair {
    return Chair()
  }
}

Notice: factory() in Lamp is returning Chair and still conforms to the protocol!

The second way to have a protocol reference itself in Swift 2+, is to use Self:

protocol Furniture {
  typealias M: Material
  typealias M2: Material
  func mainMaterial() -> M
  func secondaryMaterial() -> M2
  static func factory() -> Self
}

For structures, this will solve the issue and the compiler will now recognize that factory() does not return the intended return type. For a class, Swift 2.0 lets you mark it as final to properly take advantage of Self:

final class Lamp: Furniture {
  func mainMaterial() -> Glass {
    return Glass()
  }
  func secondaryMaterial() -> Glass {
    return Glass()
  }
  static func factory() -> Lamp {
    return Lamp()
  }
}

Lamp.factory() will no longer compile if it written to returns anything but Lamp.

Wrapping Up

This concludes the first part of the series on this topic. We covered how the type of a typealias can be inferred by the compiler. We also learned how to leverage this feature to create new types of constraints when creating protocols. Finally, we learned how Self has special caveats in these contexts.

In the next part of the series, available here, we will explore how class inheritance introduces new problems. These problems are crucial to overcome if you plan to build on top of existing data structures that leverage generics.

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 that this article demystifies some of the darker corners of Swift for you, but if you have any questions, please reach out!