Over a million developers have joined DZone.

Swift: Protocols and Associated Types

Explaining protocols with associated types in Swift.

· Mobile Zone

Associated types in protocols are an interesting feature in swift. They are a way you can ensure type correctness of a group of protocols used as policies for generic types or functions. They’re very useful for defining abstract protocol relationships that need to be adhered to by concrete types, but they’re kind of confusing too. A protocol with associated types is like an abstract class in languages like C++ or Java — you can’t create one, you can only create it’s children.

So, for example, let’s say we have a protocol defining a car:

public protocol Automobile {
  typealias FuelType
  typealias ExhaustType
  func drive(fuel: FuelType) -> ExhaustType
}

This is an interesting protocol as it doesn’t define what the exhaust types or fuel types are. It just defines a relationship between an arbitrary fuel type and some equally arbitrary exhaust type. Furthermore, as this protocol has two abstract associated types, it can’t be instantiated as an actual, concrete type either, by which I mean this:

var car: Automobile?

...is going to get the compiler mad at you. The associated types FuelType and ExhaustType aren’t defined, so the type isn’t defined either. So how do you use this? Well, you need to create a type using the protocol, like this:

public struct Electricity: Fuel {
  public init() {}
  public func use() { print("i hug trees in my spare time...") }
}

public struct Water: Exhaust {
  public init() {}
  public func content() { print("i am mostly water...") }
}

public struct ModelS: Automobile {
  public init() {}
  public func drive(fuel: Electricity) -> Water {
    fuel.use()
    return Water()
  }
}

So here, we’ve created an Automobile type based on the protocol, and the compiler is smart enough to infer the FuelType and ExhaustType from the signature of the drive:fuel: method. Now here, the ModelS type is a concrete type that conforms to the Automobile protocol, but you can’t cast a ModelS to an Automobile as Automobile is abstract, so don’t try this:

var auto: Automobile = sClass

And expect good things to happen. They won’t.

Abstract Protocol Adapters (aka Thunks)

To get around this, we can use Thunks and type erasure. This allows us to create concrete types that reflect a specific abstract protocol like Automobile. Why is this called type erasure, you might be thinking? well, because this approach essentially erases the two associated types, replacing them with concrete types. A thunk, basically, is a helper class that performs some kind of additional computation or supports subroutine calls outside of the usual calling convention. This is an example of the former. I think of them as bridges or adapters, personally.

public struct RealAutomobile<F, P>: Automobile {

  let _drive: (F) -> P

  public init<A: Automobile where A.FuelType == F, A.ExhaustType == P>(auto: A) {
    _drive = { return auto.drive($0) }
  }

  public func drive(fuel: F) -> P {
  return _drive(fuel)
  }

}

This is an Automobile adapter. It’s interesting, in that it uses Swift’s functional capabilities to close over the method calls to a submitted object. It also uses a where clause to ensure that the submitted object is of a type that supports Fuel and Exhaust types that correspond with the class generic parameters. The compiler uses this bi directionally. Here,

var modelSCar = RealAutomobile<Electricity, Water>(auto: modelS)

The compiler will check the types assocaited with the modelS object and ensure that they correspond to Electricity as a FuelType and Water as an ExhaustType. Conversely, here

var modelSCar = RealAutomobile(auto: modelS)

The compiler infers the correct FuelType an ExhaustType from the auto object.

So now, we could do something like this:

var modelSCar = RealAutomobile(auto: modelS)
var explorerCar = RealAutomobile(auto: explorer)
var sClassCar = RealAutomobile(auto: sClass)

modelSCar.drive(electricity)
sClassCar.drive(premium)
explorerCar.drive(regular)

Where we have essentially created types that reflect the Automobile interface but have replaced the associated types with concrete types.

So how useful is this? Well, we have created concrete Automobile types, but we can’t do things like:

var cars = [modelSCar, explorerCar, sClassCar]

as the three objects are in fact of different types as the fuel and exhaust types differ. You can, however, associate any type RealAutomobile<T,P> with any object that corresponds to that type, where the FuelType is T and the ExhaustType is P:

var test1: RealAutomobile<Premium,COExhaust>?
test1 = sClassCar

But if the car used doesn't use Premium fuel, or doesn't exhaust a COExhaust, like this:

test1 = modelSCar

Expect the compiler to complain.

Overall, associated types are interesting from a type safety perspective. They allow you to design complex, interrelated type systems with provable attributes, and this can lead to increased code stability. Abstract Protocol Adapters and type erasure also give you another design technique you can use to create types that conform to abstract protocols in a type-safe way.

Topics:
associated types ,protocols ,generics ,swift ,ios ,macosx

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}