fbpx

Blogs from the Ranch

< Back to Our Blog

Why Associated Type Requirements Become Generic Constraints

Avatar

Jeremy W. Sherman

Objective-C Protocols: Just Messages

Objective-C had protocols. They name a set of messages. For example, the UITableViewDataSource protocol has messages for asking the number of sections and the number of rows in a section.

Swift Protocols: Messages + Associated Types

Swift has protocols. They too name a set of messages.

But Swift protocols can also have associated types. Those types play a role in the protocol. They are placeholders for types. When you implement a protocol, you get to fill in those placeholders.

Associated Types Make Implementing Protocols Easier

Associated types are a powerful tool. They make protocols easier to implement.

Example: Swift’s Equatable Protocol

For example, Swift’s Equatable protocol has a function to ask if a value is equal to another value:

static func ==(lhs: Self, rhs: Self) -> Bool

This function uses the Self type. The Self type is an associated type. It is always filled in with the name of the type that implements a protocol. (Not convinced Self is an associated type? Jump to the end of the article, then come back.) So if you have a type struct Name { let value: String }, and you add an extension Name: Equatable {}, then Equatable.Self in that case is Name, and you will write a function:

static func ==(lhs: Name, rhs: Name) -> Bool

Self is written as Name here, because you are implementing Equatable for the type Name.

Equatable uses the associated Self type to limit the == function to only values of the same type.

Contrast: Objective-C’s NSObjectProtocol

NSObjectProtocol also has a method isEqual(_:). But because it is an Objective-C protocol, it cannot use a Self type. Instead, its equality test is declared as:

func isEqual(_ object: Any?) -> Bool

Because an Objective-C protocol cannot restrict the argument to an associated type, every implementation of the protocol suffers. It is common to begin an implementation by checking that the argument is the same type as the receiver:

func isEqual(_ object: Any?) -> Bool {
    guard let other = object as? Name
    else { return false }

    // Now you can actually check equality.

Every implementation of isEqual(_:) has to check this. It must check each and every time it is called.

Implementers of Equatable never have to check this. It is guaranteed once and for all, for every implementation, through the Self associated type.

Power Has a Price

Protocol ‘SomeProtocol’ can only be used as a generic constraint because it has Self or associated type requirements.

Associated types are a powerful tool. That power comes at a cost:

error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements

Code that uses a protocol that relies on associated types pays the price. Such code must be written using generic types.

Generic types are also placeholders. When you call a function that uses generic types, you get to fill in those placeholders.

When you look at generic types versus associated types, the relationship between caller and implementer flips:

  • Associated Types: Writer Knows, Caller Doesn’t:
    When you write a function that uses associated types, you get to fill in the placeholders, so you know the concrete types. The caller does not know what types you picked.
  • Generic Types: Caller Knows, Writer Doesn’t:
    When you write a function that uses generic types, you do not know what type the caller will pick. You can limit the types with constraints. But you must handle any type that meets the constraints. The caller gets to pick that type, and your code needs to work with whatever they pick.

Example: Calling Equatable’s == Forces Use of Generics

Consider a function checkEquals(left:right:). This does nothing but defer to Equatable’s ==:

func checkEquals(
  left: Equatable,
  right: Equatable
) -> Bool {
  return left == right
}

The Swift compiler rejects this:

error: repl.swift:2:7: error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
left: Equatable,
      ^

error: repl.swift:3:8: error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
right: Equatable
       ^

Why? Without Generics, checkEquals Is Nonsense

What if Swift allowed this? Let us do an experiment.

Pretend you have two different Equatable types, Name and Age.
Then you could write code like this:

let name = Name(value: "")
let age = Age(value: 0)
let isEquals = checkEquals(name, age)

This is nonsense! There are two ways to see this:

  • Doing: How do you run this code? What implementation of == would checkEquals call in the last line? Name’s? Age’s? Neither applies. These are only ==(Name, Name) and ==(Age, Age), because Equatable declares only ==(Self, Self). To call either Name’s or Age’s == would break type safety.
  • Meaning: What does this mean? An Equatable type is not a type alone. It has a relationship to another type, Self. If you write checkEquals(left: Equatable, right: Equatable), you only talk about Equatable. Its associated Self type is ignored. You cannot talk about “Equatable” alone. You must talk about “Equatable where Self is (some type)”.

This is subtle but important. checkEquals looks like it will work. It wants to compare an Equatable with an Equatable. But Equatable is an incomplete type. It is “equatable for some type”.

checkEquals(left: Equatable, right: Equatable) says that left is “equatable for some type” and right is “equatable for some type”. Nothing stops left from being “equatable for some type” and right from being “equatable for some other type”. Nothing makes left and right both be “equatable for the same type”.

Equatable.== needs its left and right to be the same type. This makes checkEquals not work.

Discover why a code audit is essential to your application’s success!

Teaching checkEquals to Handle All Equatable+Self Groups

checkEquals cannot know what “some type” should be in “Equatable where Self is (some type)”. Instead, it must handle every group of “Equatable and Self type”: It must be “checkEquals for all types T, where T is ‘Equatable and its associated types’”.

You write this in code like so:

func checkEquals<T: Equatable>(
  left: T,
  right: T
) -> Bool {
  return left == right
}

Now, every type T that is an Equatable type – this includes its associated Self type – has its own checkEquals function. Instead of having to write checkEquals(left: Name, right: Name) and checkEquals(left: Age, right: Age), you use Swift’s generic types to write a “recipe” for making those types. You have walked backwards into the “Extract Generic Function” refactoring.

Example: Calling NSObjectProtocol’s isEqual(_:) Does Not Require Generics

Writing checkEquals using NSObjectProtocol instead of Equatable does not need generics:

import Foundation

func checkEquals(
  left: NSObjectProtocol,
  right: NSObjectProtocol
) -> Bool {
  return left.isEqual(right)
}

This is simple to write. It also allows us to ask stupid questions:

let isEqual = checkEquals(name, age)

Is a name even comparable with an age? No. So isEqual evaluates to false. Name.isEqual(_:) will see obj is not a kind of Name. Name.isEqual(_:) will return false then. But unlike Equatable.==, every single implementation of isEqual(_:) must be written to handle such silly questions.

Trade-Offs

Associated types make Swift’s protocols more powerful than Objective-C’s.

An Objective-C’s protocol captures the relationship between an object and its callers. The callers can send it messages in the protocol; the implementer promises to implement those messages.

A Swift protocol can also capture the relationship between one type and several associated types. The Equatable protocol relates a type to itself through Self. The SetAlgebra protocol relates its implementer to an associated Element type.

This power can simplify implementations of the protocol. To see this, you contrasted implementing Equatable’s == and NSObjectProtocol’s isEqual(_:).

This power can complicate code using the protocol. To see this, you contrasted calling Equatable’s == and NSObjectProtocol’s isEqual(_:).

Expressive power can complicate. When you write a protocol, you must trade the value of what you can say using associated types against the cost of dealing with them.

I hope this article helps you evaluate the protocols and APIs you create and consume. If you found this helpful, you should check out our Advanced Swift bootcamp.

For the More Curious: Is Self an Associated Type?

Self acts like an associated type. Unlike other associated types, you do not get to choose the type associated with Self. Self is automatically associated with the type implementing the protocol.

But the error message talks about a protocol that “has Self or associated type requirements”. This makes it sound like they are different things.

This is hair-splitting. But a hair in the wrong place distracts. I went to find an answer. I have found it for you in the source code for the abstract syntax tree used by the Swift compiler. A doc comment on AssociatedTypeDecl says:

Every protocol has an implicitly-created associated type ‘Self’ that
describes a type that conforms to the protocol.

Case closed: Self is an associated type.

Avatar

Jeremy W. Sherman

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project