Swift — Protocol-oriented programming

Jullian Mercier
7 min readJun 23, 2019

Protocol-oriented programming is a design paradigm providing some guarantee that an object conforming to a given protocol will implement its underlying methods and properties. These are called protocol requirements.

Protocols are a convenient way to explicitly tell your classes or structs to follow a set of rules while avoiding the inheritance conundrum.

The standard library is a valuable ressource for protocols.

Sequence is one of these.

— The Sequence case

« A sequence is a list of values that you can step through one at a time. The most common way to iterate over the elements of a sequence is to use a for-in loop »

for n in 0…4 {
print(n)
}
// Prints: 0
// Prints: 1
// Prints: 2
// Prints: 3
// Prints: 4

A for-in loop uses a next() method from an IteratorProtocol (a protocol that Sequence must conform to) in order to be able to iterate through each element.

Let’s dive into the actual implementation of Sequence protocol from the standard library.

public protocol Sequence {   /// A type representing the sequence’s elements.
associatedtype Element
/// A type that provides the sequence’s iteration interface and
/// encapsulates its iteration state.
associatedtype Iterator : IteratorProtocol
where Iterator.Element == Element
/// Returns an iterator over the elements of this sequence. __consuming func makeIterator() -> Iterator}

Protocols are very powerful as they support generics.

Unlike classes or structs, the generic type is marked with the ‘associated type‘ keyword.

The Sequence protocol must provide an Iterator that is an object which will encapsulate the iteration. The Iterator generic type must adopt an IteratorProtocol with some generic constraint.

This is some powerful capability when it comes to specializing our generics as it guarantees that a concrete type will match the protocol type constraints requirements.

makeIterator() must return an Iterator.

— Usage in code

struct CustomSequence: Sequence {    typealias Iterator = CustomIterator
typealias Element = Int
__consuming func makeIterator() -> Iterator {
return CustomIterator(index: 0)
}
}
struct CustomIterator: IteratorProtocol { var index: Int = 0 mutating func next() -> Int? {
defer {
index += 1
}
return index < 5 ? index: nil
}
}

First, conforming to the Sequence protocol obliges CustomSequence to implement makeIterator() and return a given Iterator.
Typealiases represent our concrete types that is a CustomIterator object and an Int for our Sequence elements.

CustomIterator, through its IteratorProtocol conformance, must implement a next() method that will return a given element in the Sequence or nil if the iteration ended.

The iteration must end at some point (return nil) otherwise an infinite loop might be run.

for n in CustomSequence() {
print(n)
}
// Prints: 0
// Prints: 1
// Prints: 2
// Prints: 3
// Prints: 4

Some improvements could be made.

Typealiases don’t need to explicitly specify concrete types as it will be inferred by the compiler thanks to the return type from makeIterator() as well as the return type from next().

CustomSequence could also directly adopt IteratorProtocol so that it doesn’t need to implement makeIterator() as it will, itself, be the iterator.

struct CustomSequence: Sequence, IteratorProtocol {
var index: Int = 0
mutating func next() -> Int? {
defer {
index += 1
}
return index < 5 ? index: nil
}
}

It now becomes easy to treat a struct as a Sequence using its related functions and properties.

let customSequence = CustomSequence().map { $0 * 2 }for n in customSequence {
print(n)
}
// Prints: 0
// Prints: 2
// Prints: 4
// Prints: 6
// Prints: 8

Creating our own sequence turns out to be very convenient when it comes to dealing with complex objects on which we need to iterate over quite frequently. Customizing our next method will provide some flexibility on the iteration ‘style‘ we want to apply.

Protocols are a set of rules to follow. They’re a blueprint.

They offer a wide range of possibility such as defining a default implementation for our methods.

protocol Instrument {
func play()
}
extension Instrument {
func play() {
print(« Playing with my brand new instrument »)
}
}
struct Guitar: Instrument { }

Now the play() method from the extension will be available as is, providing a default implementation for any object conforming to the Instrument protocol.

let guitar = Guitar()
guitar.play()
// Prints: Playing with my brand new instrument

Providing a new play() method in the object will simply override the default implementation.

struct Guitar: Instrument {
func play() {
print(« Playing with my brand new guitar »)
}
}
let guitar = Guitar()
guitar.play()
// Prints: Playing with my brand new guitar

However, removing the play() method from the protocol requirements while keeping its default implementation might end up in some confusion regarding the Guitar behavior.

protocol Instrument { }extension Instrument {
func play() {
print(« Playing with my brand new instrument »)
}
}
let guitar = Guitar()
guitar.play()
// Prints: Playing with my brand new guitar

So far so good however let’s now try to explicitly specify the type.

let guitar: Instrument = Guitar()
guitar.play()
// Prints: Playing with my brand new instrument

There’s no customization point in the protocol definition as play() doesn’t exit so the compiler will statically dispatch that method using its default implementation from the extension.

Reimplementing play() in the protocol requirements will remove this undesired behavior, the method will then be dynamically dispatched using the Guitar play() implementation.

— Static vs. Dynamic dispatch

Methods declared in the protocol itself will be dynamically dispatched whereas methods in a protocol extension — which are not protocol requirements — will use static dispatch.

A static dispatch implies that the compiler will know which method will be called at runtime which leads to performance boost.

A dynamic dispatch induces the usage of protocol witness tables to resolve, at runtime, which method to call. This is why, in our last example, the Guitar play() method is called instead of the extension default implementation.

— Type erasure

Let’s try to create an array of Instruments.

var instruments: [Instruments] = []

Works fine. Now let’s do the same with Sequences.

var sequences: [Sequence] = []// Error: Protocol ‘Sequence’ can only be used as a generic constraint because it has Self or associated type requirements

Any time a protocol uses generic types in its requirements, the compiler will raise an error.

It makes sense since two objects conforming to the same protocol might be working with different types which would make them actually different.

While we can’t really use these generics protocols as fully useable types, we could use wrappers around them.

This is where type-erasure comes in.

The Swift standard library uses this kind of wrapper for its Sequence protocol.

« A type-erased sequence.

An instance of `AnySequence` forwards its operations to an underlying base sequence having the same `Element` type, hiding the specifics of the underlying sequence »

public struct AnySequence<Element> {    /// Creates a sequence whose `makeIterator()` method forwards to
/// `makeUnderlyingIterator`.
@inlinable public init<I>(
_ makeUnderlyingIterator: @escaping () -> I
) where Element == I.Element, I : IteratorProtocol
}

The secret of the AnySequence wrapper struct lies in the generic type constraints of the init() method. It will guarantee that its generic type Element will match the Iterator wrapper struct Element (both AnySequence and AnyIterator are type-erasures).

let anySequence = AnySequence { () -> AnyIterator<Int> in
var x = 0
return AnyIterator { () -> Int? in
defer { x += 1 }
return x < 5 ? x : nil
}
}
for n in anySequence {
print(n)
}
// Prints: 0
// Prints: 1
// Prints: 2
// Prints: 3
// Prints: 4

It looks quite similar to our CustomSequence() (in a shorter way though) however we’re now able to create a proper array of AnySequences.

var sequences: [AnySequence<Int>] = []
sequences.append(anySequence)

Let’s type erase our Instrument protocol.

First we need to define an associated type.

protocol Instrument {
associatedtype NewInstrument where NewInstrument: Instrument
func play()
func swap() -> NewInstrument
}
var instruments: [Instrument] // Raises an error as expected.

The swap() method will return a new object that must conform to the Instrument protocol.

struct Guitar: Instrument {
func play() {
print(“Playing with my brand new guitar”)
}
func swap() -> Banjo {
return Banjo()
}
}
struct Banjo: Instrument {
func play() {
print(“Playing with my brand new banjo”)
}
func swap() -> Guitar {
return Guitar()
}
}
struct Ukulele: Instrument {
func play() {
print(“Playing with my brand new ukulele”)
}
func swap() -> Guitar {
return Guitar()
}
}
struct Piano: Instrument { func play() {
print(“Playing with my brand new piano”)
}
func swap() -> Guitar {
return Guitar()
}
}

Now, let’s wrap our protocol around a struct.

struct AnyInstrument<T: Instrument>: Instrument {    private let _play: ()
private let _swap: T
init<U: Instrument>(
_ instrument: U
) where U.NewInstrument == T {
_play = instrument.play()
_swap = instrument.swap()
}
func play() {
_play
}

func swap() -> T {
return _swap
}
}

Let’s now try to create an array of AnyInstruments.

var anyInstruments = [
AnyInstrument(Banjo()),
AnyInstrument(Ukulele()),
AnyInstrument(Piano())
]

The compiler now knows the concrete type for our associated type which is a Guitar.

let guitars = instruments.map { $0.swap() }
guitars.map { $0.play() }
// Prints: Playing with my brand new guitar
// Prints: Playing with my brand new guitar
// Prints: Playing with my brand new guitar

AnyInstrument is really just a wrapper around our Instrument protocol.

Conclusion

Protocols are a powerful way to harmonise your objects behavior while avoiding the inheritance issues. In protocol-oriented programming, objects remain independent from one another which remove any kind of relationship complexities.

They’re widely used in the Swift standard library (Sequence, Collection..) as well as in many frameworks (UIKit’s UITableViewDataSource, UITableViewDelegate..).

They support generics which brings in some flexibility however using them as as fully useable types is not yet possible.

Using type-erasures help bypass these protocol limitations.

--

--

Jullian Mercier

Senior iOS engineer. jullianmercier.com. @jullian_mercier. Currently looking for new job opportunities.