Swift Generic Protocols

Published on

Protocols in Swift can be generic via abstract type members rather than parameterisation. Consequently, the protocol itself can no longer be used as a type but only as a generic constraint.

The Problem

If you tried to use a generic protocol as a type as follows:

protocol GenericProtocol {
  typealias AbstractType
  func magic() -> AbstractType
}

let list : [GenericProtocol] = []

We would see the following error message:

Protocol 'GenericProtocol' can only be used as a generic constraint because it has Self or associated type requirements.

If you’re not familiar with generics, you might be wondering why generic protocols cannot be used as types. The explanation is quite simple — the name of the generic protocol, e.g. GenericProtocol, represents a set of types rather than a single type. If you were to have an arbitrary array of GenericProtocol, you would not be able to say anything about the return type of the magic() method, as it might be different for the various contained elements.

This is nicely illustrated on StackOverflow.

Type Members vs Parameterisation

In Swift, protocols can be made generic by using abstract type members ( typealias keyword) while classes, structs, methods and functions become generic by type parameterisation (see below).

func genericFunc(ts : [T]) -> Bool {
  return true; // not very exciting
}

Turns out that, in general, type members and parameterisation have equivalent expressivity — as long as any language 1 in question fully supports both ways. We can imagine Swift supporting generic parameterised protocols in a future iteration which would make the following code possible:

protocol GenericProtocolWithParam {
  func magic() -> T
}

let list : [GenericProtocolWithParam] = []

If you’re interested in the tradeoffs between type parameterisation and abstract type members, check out the discussion on the dev forums and the article on how the issue affects Scala.

Workaround

We can workaround the lack of type parameterisation in protocols by using a thunk. We do this by defining a struct which implements the protocol by forwarding all methods to an abstract type which implements said protocol, provided as part of the initialisation of the struct (a form of dependency injection). The actual forwarding is done with closures.


protocol GenericProtocol {
  typealias AbstractType
  func magic() -> AbstractType
}

struct GenericProtocolThunk : GenericProtocol {
  // closure which will be used to implement `magic()` as declared in the protocol
  private let _magic : () -> T

  // `T` is effectively a handle for `AbstractType` in the protocol
  init(_ dep : P) {
    // requires Swift 2, otherwise create explicit closure
    _magic = dep.magic
  }

  func magic() -> T {
    // any protocol methods are implemented by forwarding
    return _magic()
  }
}

Once we have a thunk, we can proceed to use it as a type by providing type values.


struct StringMagic : GenericProtocol {
  typealias AbstractType = String
  func magic() -> String {
    return "Magic!"
  }
}

// we can now create arrays of thunks if we specify the type param
let magicians : [GenericProtocolThunk] = [GenericProtocolThunk(StringMagic())]
magicians.first!.magic() // returns "Magic!"

Self Requirement

In the context of a protocol declaration, Self refers to the type adopting the protocol. This makes the protocol generic, as Self can be thought of as syntactic sugar for a convenience type member. For example:

protocol EquatableSelf {
 func equals(other : Self) -> Bool
}

// By adopting `EquatableSelf`, every occurrence of `Self` in the protocol gets
// semantically replaced with `ImplicitStruct`
struct ImplicitStruct : EquatableSelf {
  var val : Int64
  func equals(other: ImplicitStruct) -> Bool {
    return self.val == other.val;
  }
}

If we did not have the ability to use Self, we could have achieved the same in the following way:


protocol EquatableTypealias {
  typealias EquatableType
  func equals(other : EquatableType) -> Bool
}

struct ExplicitStruct : EquatableTypealias {
  typealias EquatableType = ExplicitStruct
  var val : Int64
  func equals(other: ExplicitStruct) -> Bool {
    return self.val == other.val;
  }
}

Protocol-Oriented Programming

Protocols in Swift play a central role, as Dave Abrahams says in Session 408 (PDF) at WWDC 2015:

… When we made Swift, we made the first protocol-oriented programming language.

Watching the full video is highly recommended for any developers interested in Swift and the protocol-oriented approach. In particular, there’s a very good slide illustrating the consequence of adding a Self requirement to a protocol.

Generics and Type Variance

Another issue that pops up with the introduction of generics is the variance of generic types. In simple terms, it deals with the following problem:

class Animal {
  // Animal-specific ivars + methods
}

class Cat : Animal {
  // Cat-specific ivars + methods
}

var cats : [Cat] = [Cat()]

// Here, we're typecasting from [Cat] to [Animal].
// Such typecasts could be unsafe depending on the details.
var animals : [Animal] = cats

I can suggest starting off with the Wikipedia page on type variance to further explore the issue at hand.

References


  1. Scala is such a language. ↩︎

← Back to Writings