• Skip to main content
  • Skip to footer

Khawer Khaliq

  • Home

A Protocol-Oriented Approach to Associated Types and Self Requirements in Swift

Share
Tweet
Share
Pin

Associated types and Self requirements are important features of Swift and are used extensively in the standard library. But they can be tricky to use and continue to frustrate many Swift programmers. This article shows how, with a protocol-oriented approach, it is possible not only to avoid having to use associated types in many situations but also to use associated types and Self requirements, where appropriate, to leverage their benefits while avoiding the pitfalls.

Contents

What is an associated type
How not to use protocols with associated types
A protocol-oriented way to use associated types
A protocol-oriented alternative to associated types
Use of associated types in the standard library
What is a Self requirement in a protocol
A protocol-oriented approach to using Self requirements
Use of Self requirements in the standard library
Other advantages of protocol-oriented design
1. Dealing with exceptional conditions
2. Extending the domain
Conclusion

What is an associated type

An associated type is a placeholder in a protocol for a type that is used as part of the protocol. Any type that conforms to the protocol must specify an actual type that will replace the placeholder. A protocol can have one or more associated types and these associated types provide flexibility for conforming types to decide which type to use in place of each associated type placeholder. Associated types are specified with the associatedtype keyword. Conforming types can use the typealias keyword to specify the type that will replace each associated type placeholder. In many cases, however, Swift is able to infer that type from the context.

A protocol with an associated type is an incomplete protocol since one or more of the types used as part of the protocol do not get resolved until the protocol is adopted. It is important to note that, although the presence of an associated type makes a protocol generic in the sense that conforming types can substitute any type for the placeholder, it does not mean that every type conforming to such a protocol will be generic. That decision is left to the conforming types.

How not to use protocols with associated types

Protocols with associated types have got something of a bad name in Swift circles. This is due to the very nature of protocols with associated types. As already noted, a protocol with an associated type is an incomplete protocol. It uses a type but that type remains unspecified until a concrete type conforms to the protocol. Therefore, Swift does not consider protocols with associated types as first-class types, i.e., they cannot be used as types of variables, constants, elements of collections, or parameters and return values of functions.

The frustration Swift developers sometimes experience with associated types is because of using protocols with associated types as abstractions for domain entities. Such abstractions then prove unusable as the abstract type cannot be used as the type of a variable, the type of elements of an array, or as a parameter or return type of a function.

To see how an associated type can be problematic when used inappropriately, let’s say we want to automate marking of certain types of exam questions. For a question to be automatically marked, we should be able to compare a given answer with the correct answer for value equality. Since we need to accommodate different types of questions that can be automatically marked, e.g., multiple-choice questions, true-false questions, etc., we need to use a protocol and the protocol should allow us to associate each type of question with a specific type of answer. Sounds like a problem that calls for a protocol with an associated type. Following classic object-oriented thinking, we would use a protocol to model an abstract question, as shown below.

protocol AutoMarkableQuestion {
    associatedtype Answer: Equatable
    var statement: String { get set }
    var correctAnswer: Answer { get set }
    func isCorrect(answer: Answer) -> Bool
}

The protocol AutoMarkableQuestion defines an associated type Answer, which is a placeholder for the type of answer that will be associated with each concrete question type. As we see in this example, Swift allows us to apply constraints to associated types. Here, we constrain the associated type Answer such that any type which replaces this placeholder must conform to Equatable so instances of concrete answer types can be compared for value equality using the == operator. The protocol also defines a variable to store the question statement, a variable to store the correct answer, and a method to check whether a given answer is correct.

Since the isCorrect(answer:) method will just test the given answer for value equality with the stored correct answer, we can implement this method in a protocol extension.

extension AutoMarkableQuestion {
    func isCorrect(answer: Answer) -> Bool {
        answer == correctAnswer
    }
}

Now, we can define concrete question types that conform to the protocol. Let’s start with a true-false question. Note that we don’t need to use the typealias keyword in this case as Swift is able to infer the type we are using in place of the Answer placeholder.

enum TrueFalseAnswer {
    case t, f
}

struct TrueFalseQuestion: AutoMarkableQuestion {
    var statement: String
    var correctAnswer: TrueFalseAnswer
}

Following the same pattern, we can define a type for multiple-choice questions where the correct answer is one of four choices.

enum MultipleChoiceAnswer {
    case a, b, c, d
}

struct MultipleChoiceQuestion: AutoMarkableQuestion {
    var statement: String
    var correctAnswer: MultipleChoiceAnswer
}

Let’s test these concrete types by defining one instance of each question type.

let trueFalseQuestion = TrueFalseQuestion(
    statement: "The sun revolves around the earth.",
    correctAnswer: .f)

let multipleChoiceQuestion = MultipleChoiceQuestion(
    statement: "Which city is the capital of France? a) Rome; b) Paris; c) London; d) Accra",
    correctAnswer: .b)

We can check the output of the isCorrect(answer:) method by providing a correct and incorrect answer for each question.

print(trueFalseQuestion.isCorrect(answer: .t))
// false
print(trueFalseQuestion.isCorrect(answer: .f))
// true
print(multipleChoiceQuestion.isCorrect(answer: .a))
// false
print(multipleChoiceQuestion.isCorrect(answer: .b))
// true

Associated types get the Swift type system working for us and we are greeted with a compile-time error if the answer we provide is an instance of any type other than the answer type associated with the question being answered. We can see this type-safety in action if we try to provide a multiple-choice answer to a true-false question.

print(trueFalseQuestion.isCorrect(answer: MultipleChoiceAnswer.a))
// Error: Cannot convert value of type 'MultipleChoiceAnswer' to expected argument type 'TrueFalseAnswer'

The compiler complains that the type of the value MultipleChoiceAnswer.a is not the expected answer type for a true-false question. We get a different error is we omit the MultipleChoiceAnswer prefix.

print(trueFalseQuestion.isCorrect(answer: .a))
// Error: Type 'TrueFalseAnswer' has no member 'a'

This is all well and good. But the point of using a protocol is to be able to treat questions at an abstract level so we can use questions without having to know the actual type of each concrete question instance. Let's try and do that with one of the questions.

let question: AutoMarkableQuestion = trueFalseQuestion
// Error: Protocol 'AutoMarkableQuestion' can only be used as a generic constraint because it has Self or Associated type requirements

We get the error that has baffled many Swift programmers. The problem is that, since the AutoMarkableQuestion protocol has an associated type, the protocol is not a first-class type and it can be used only as a generic constraint. Therefore, we are getting an error when assigning an instance of TrueFalseQuestion to a variable of type AutoMarkableQuestion even though TrueFalseQuestion conforms to AutoMarkableQuestion. The same thing happens if we try to create an array to store instances of different concrete question types.

let questions: [AutoMarkableQuestion] = [trueFalseQuestion, multipleChoiceQuestion]
// Error: Protocol 'AutoMarkableQuestion' can only be used as a generic constraint because it has Self or Associated type requirements

The fact that we cannot create a variable of type AutoMarkableQuestion or use AutoMarkableQuestion as the type of the elements of an array is not a limitation of Swift. It simply reflects the fact that the AutoMarkableQuestion protocol is incomplete and the protocol type does not provide sufficient information about instances of conforming types. For example, if we could create a variable of type AutoMarkableQuestion, what answer type would we expect a concrete instance stored in that variable to receive. The reality is that we cannot know since the protocol does not provide that information. We would have the same issue if we could create an array with elements of type AutoMarkableQuestion or a function with a parameter or return value of type AutoMarkableQuestion. This is why Swift renders a protocol with an associated type unusable as a first-class type. This makes the AutoMarkableQuestion protocol unusable as an abstraction, defeating the purpose of creating the protocol.

A protocol-oriented way to use associated types

In the previous section, we saw how using an associated type in a protocol renders it unusable as an abstraction of a domain entity since the protocol cannot be used a first-class type. This can sometimes deter developers from using protocols with associated types. But protocols with associated types can enable us to solve otherwise thorny problems in simple and elegant ways. The solution to this seeming conundrum is to look at the domain through a protocol-oriented lens, using protocols to model traits of domain entities rather than the domain entities themselves. This can enable us to leverage the strengths of associated types while avoiding the issues we saw in the previous section.

Let’s revisit the problem of automating marking of certain types of exam questions. This time, rather than using a protocol with an associated type to model a question, which is a domain entity, we use a protocol with an associated type to model the characteristic that we want our questions to have, i.e., they should support automatic marking.

protocol AutoMarkable {
    associatedtype Answer: Equatable
    var correctAnswer: Answer { get set }
    func isCorrect(answer: Answer) -> Bool
}

The AutoMarkable protocol is similar to the AutoMarkableQuestion protocol we had defined in the previous section. It defines an associated type Answer, which is a placeholder for the type of answer that will be associated with each conforming type. As we had done before, we constrain the associated type Answer such that any type which replaces this placeholder must conform to Equatable so instances of answer types as can be compared for value equality using the == operator. We also define a variable correctAnswer to store the correct answer and a method to check whether a given answer is correct. Unlike the AutoMarkableQuestion protocol, we don’t have a variable to store the question statement, since this protocol does not represent a question but rather a specific attribute of questions.

As we had done with the AutoMarkableQuestion protocol, we use a protocol extension to implement the isCorrect(answer:) method to test the given answer for value equality with the stored correct answer.

extension AutoMarkable {
    func isCorrect(answer: Answer) -> Bool {
        return answer == correctAnswer
    }
}

Now, we can define concrete question types to conform to the AutoMarkable protocol. We start with a true-false question, using the TrueFalseAnswer type we had defined in the previous section.

struct TrueFalseQuestion: AutoMarkable {
    var statement: String
    var correctAnswer: TrueFalseAnswer
}

Following the same pattern, we can define a type to model a multiple-choice question, using the MultipleChoiceAnswer type defined in the previous section.

struct MultipleChoiceQuestion: AutoMarkable {
    var statement: String
    var correctAnswer: MultipleChoiceAnswer
}

To test these types, we write a function to check whether an answer given for a question is correct.

func printResultOf<Question: AutoMarkable>(question: Question, forAnswer answerGiven: Question.Answer) {
    print(question.isCorrect(answer: answerGiven) ? "Correct, one point!" : "Sorry, try again!")
}

Remember the error we got in the previous section when we tried to use the AutoMarkableQuestion protocol as a first-class type and we were told that AutoMarkableQuestion can only be used as a generic constraint, which made the abstraction unusable. Now that we have used the protocol AutoMarkable to abstract a trait of the domain entity rather than a domain entity itself, we can do exactly what the error was telling us to do, i.e., use the AutoMarkable protocol as a generic constraint. In the generic function printResultOf(question:forAnswer:), we have used AutoMarkable as a constraint on the type parameter Question, which requires that any type that replaces the Question placeholder must conform to AutoMarkable.

We create concrete question instances in exactly the same way as we had done in the previous section.

let trueFalseQuestion = TrueFalseQuestion(
    statement: "The sun revolves around the earth.",
    correctAnswer: .f)

let multipleChoiceQuestion = MultipleChoiceQuestion(
    statement: "Which city is the capital of France? a) Rome; b) Paris; c) London; d) Accra",
    correctAnswer: .b)

Now we can use the printResultOf(question:forAnswer:) function to test answers to questions, as shown below.

printResultOf(question: trueFalseQuestion, forAnswer: .t)
// Sorry, try again!
printResultOf(question: trueFalseQuestion, forAnswer: .f)
// Correct, one point!
printResultOf(question: multipleChoiceQuestion, forAnswer: .a)
// Sorry, try again!
printResultOf(question: multipleChoiceQuestion, forAnswer: .b)
// Correct, one point!

Note that the forAnswer parameter of printResultOf(question:forAnswer:) has the type Question.Answer, which is the answer type associated with the concrete type that replaces the Question placeholder. This gets the Swift type system working for us to guard against using the incorrect answer type for any question. We see below the compile-time error we get if we try to use a true-false answer for a multiple-choice question.

printResultOf(question: multipleChoiceQuestion, forAnswer: TrueFalseAnswer.t)
// Error: Cannot convert value of type 'TrueFalseAnswer' to expected argument type 'MultipleChoiceAnswer'

This error tells us that the type of the argument we have provided for the forAnswer parameter does not match the Answer type associated with the type we have used to replace the Question placeholder. The compiler catches us even if we omit the TrueFalseAnswer prefix, reminding us that the MultipleChoiceAnswer type has no member t.

printResultOf(question: multipleChoiceQuestion, forAnswer: .t)
// Error: Type 'MultipleChoiceAnswer' has no member 't'

A protocol-oriented alternative to associated types

As we have seen in previous sections, using a protocol with an associated type is not the right way to create an abstraction for a domain entity since the protocol type will no longer be a first-class type, making it useless as an abstraction. We also saw how using a protocol-oriented approach can enable us to use protocols with associated types as they were intended to be used, i.e., as generic constraints rather than as first-class types. In this section, we will explore how using a protocol-oriented approach can often allow us to avoid using protocols with associated types altogether.

The key to protocol-oriented thinking is to start not by looking for abstractions for domain entities but by understanding what the entities will do and what algorithms we need. Since Swift protocols enable retroactive modeling, we can start by writing some concrete code to better understand what our code is supposed to do, use that knowledge to create abstractions based on the attributes and algorithms we need, and retroactively model our concrete types to work with these abstractions.

Let’s consider a case where we want to model different kinds of animals and the food they eat. Thinking from an object-oriented perspective, where we would normally start by looking for abstractions for domain entities, we may use an Animal protocol to model an animal, as shown below, where the associated type Food creates a placeholder that conforming types will use to specify the type of food that a particular type of animal eats.

protocol Animal {
    associatedtype Food
    func eat(_: Food)
}

Having seen the limitations of protocols with associated types, however, we know that this approach will land us in trouble when we try to use Animal as a first-class type, which would be expected of a protocol used to model an abstraction of a domain entity.

var animal: Animal
// Error: Protocol 'Animal' can only be used as a generic constraint because it has Self or Associated type requirements
var animals: [Animal]
// Error: Protocol 'Animal' can only be used as a generic constraint because it has Self or Associated type requirements

As we had noted in a previous section, the fact that we cannot create a variable of type Animal, or use Animal as the type of the elements of an array, is not a limitation of Swift. It simply reflects the fact that the Animal protocol is incomplete and the protocol type does not provide sufficient information about instances of conforming types. For example, if we could create a variable of type Animal, we would not know what food we can expect a concrete animal instance stored in that variable to eat.

With a protocol-oriented approach, however, we don’t need to think about abstractions at the outset. We can write some concrete code and let it guide us to the abstractions we may need. To demonstrate this, we start by creating two types of food that animals commonly eat.

struct Grass {}
struct Meat {}

Now we can create concrete animal types that eat these types of food.

struct Goat {
    func eat(_: Grass) {
        print("Eating Grass")
    }
}

struct Dog {
    func eat(_: Meat) {
        print("Eating Meat")
    }
}

From the above code, we can see that the characteristic we want animals to have is that they eat, and different types of animals only eat specific types of food. The two cases we have are animals that eat grass and those that eat meat. Here is how we can create abstractions to model these eating traits.

protocol GrassEating {
    func eat(_: Grass)
}

protocol MeatEating {
    func eat(_: Meat)
}

We can use retroactive modeling to make our concrete animal types conform to the relevant protocols.

extension Goat: GrassEating {}
extension Dog: MeatEating {}

Since the implementations of the eat(_:) methods follow a standard pattern in the concrete Goat and Dog types, we can use protocol extensions to implement these methods for animals that eat grass and meat respectively.

extension GrassEating {
    func eat(_: Grass) {
        print("Eating Grass")
    }
}

extension MeatEating {
    func eat(_: Meat) {
        print("Eating Meat")
    }
}

With these extensions in place, we don’t need to implement the eat(_:) method in the concrete types and we can rewrite the Goat and Dog types as below.

struct Goat: GrassEating {}
struct Dog: MeatEating {}

We can also define other grass-eating and meat-eating animals in the same way.

struct Sheep: GrassEating {}
struct Wolf: MeatEating {}

This gives the concrete animal types the ability to eat their specific types of food and the type system will stop us if we try to make them eat something that they should not.

let goat = Goat()
let sheep = Sheep()
let wolf = Wolf()

let grass = Grass()
let meat = Meat()

goat.eat(grass)
// Eating Grass
sheep.eat(grass)
// Eating Grass
wolf.eat(meat)
// Eating Meat

goat.eat(meat)
// Error: Cannot convert value of type 'Meat' to expected argument type 'Grass'

What is different between the Animal protocol we had shown earlier and the GrassEating and MeatEating protocols is that the protocols we are using now are complete protocols and the protocols types are first-class types which can be used as types of variables and as types of elements of arrays.

let grazer: GrassEating = goat
grazer.eat(grass)
// Eating Grass

let grazers: [GrassEating] = [goat, sheep]
grazers.forEach({ $0.eat(grass) })
// Eating Grass
// Eating Grass

Use of associated types in the standard library

The Swift standard library makes extensive use of associated types in the protocols used to model sequences and collections. These associated types give significant flexibility to conforming types to customize certain aspects of their implementations while remaining within the overall framework defined by the protocol.

The Collection protocol, for instance, has five associated types:

  • Element: Represents the collection’s elements
  • Index: Represents a position in the collection
  • Indices: Represents the indices valid for subscripting the collection
  • Iterator: Provides the collection’s iteration interface
  • SubSequence: Represents a contiguous subrange of the collection’s elements

These associated types give concrete types that adopt the Collection protocol the flexibility to replace any of the associated type placeholders with appropriate types as required. Array, Set and Dictionary are all collection types in the standard library. Set and Dictionary conform to Collection while Array conforms to RandomAccessCollection and MutableCollection, both of which inherit from Collection. All three are generic collections but there is a difference in the way they use the Element associated type defined by the Collection protocol. Array uses Element as its type parameter, meaning the type that replaces the Element placeholder is decided at the time of instance creation, enabling Array to be used to create an array of any valid type in Swift.

struct Array<Element>

Set also uses Element as the placeholder for the type of elements of each set but, since it needs to test efficiently for membership and ensure that each element appears only once in a set, there is the additional condition that the type of elements should conform to the Hashable protocol.

struct Set<Element> where Element: Hashable

Dictionary is a collection of key-value pairs so it redefines the Element placeholder as a tuple containing a key-value pair, with the condition that only the keys need to conform to the Hashable protocol.

struct Dictionary<Key, Value> where Key: Hashable

String is also a collection as it conforms to BidirectionalCollection and RangeReplaceableCollection, both of which inherit from Collection. There is no need for String to be generic, however, since all strings are collections of characters. Accordingly, the String type replaces the Element placeholder with the Character type and has no type parameter.

If we look further into the Array and String types, we see that Array replaces the SubSequence placeholder with the ArraySlice type, which presents a view onto the storage of a larger array, while String replaces SubSequence with the Substring type, which is a slice of a string that shares its storage with the original string. Since arrays are indexed using integers, Array replaces the Index placeholder with the Int type. String, on the other hand, replaces the Index placeholder with a nested type String.Index, which represents a position of a character or code unit in a string. A string in Swift is actually a collection of extended grapheme clusters, represented by the Swift Character type. Many individual characters can be made up of multiple Unicode scalar values. This is why, unlike Array, which allows random access to its elements using integer indices and whose count property has O(1) complexity, Swift has to actually ‘walk’ a string to be able to get to a certain index in the string or to report the length of the string, so the count property of String has O(n) complexity.

What is a Self requirement in a protocol

A Self requirement is a placeholder in a protocol for a type, used as part of the protocol, which is replaced by the concrete type that conforms to the protocol. A Self requirement can thus be thought of as a special case of an associated type. Like a protocol with an associated type, a protocol with a Self requirement is also an incomplete protocol since the type to be used in place of the Self placeholder gets resolved only when the protocol is adopted. The difference is that, while a conforming type can use any type it wants to replace an associated type, any Self placeholder must be replaced by the confirming type itself.

A protocol-oriented approach to using Self requirements

Thinking back to the animal example we saw in an earlier section, where we modeled the eating characteristic of animals, let’s say we now want to model a different characteristic of animals, i.e., they can mate, but only with other animals of the same type. If we think with an object-oriented mindset, we could use an Animal protocol to model an abstract animal, as shown below, to reflect the mating characteristic of animals.

protocol Animal {
    func mate(with: Self)
}

Note the Self placeholder for the type of the parameter of the mate(with:) method. In concrete types that adopt this protocol, this placeholder must be replaced by the concrete type itself, ensuring that each type of animal can mate only with other animals of the same type. The Animal protocol is thus said to have a Self requirement.

To demonstrate this, we define a Cow type to conform to the Animal protocol, replacing the Self placeholder with Cow.

struct Cow: Animal {
    func mate(with: Cow) {
        print("Mating with another Cow")
    }
}

Since mate(with:) will be implemented similarly in all conforming types, we can use a protocol extension to implement this method.

extension Animal {
    func mate(with other: Self) {
        print("Mating with another \(type(of: other))")
    }
}

Now we can define concrete types conforming to the protocol without having to implement the mate(with:) method.

struct Cow: Animal {}
struct Deer: Animal {}

As with an associated type, use of a Self requirement gets the Swift type system working for us, allowing each animal to mate with another animal of the same type, but not with an animal of a different type.

let cow = Cow()
let deer = Deer()
let anotherCow = Cow()
let anotherDeer = Deer()

cow.mate(with: anotherCow)
// Mating with another Cow
deer.mate(with: anotherDeer)
// Mating with another Deer

cow.mate(with: deer)
// Error: Cannot convert value of type 'Deer' to expected argument type 'Cow'

But using a Self requirement in the Animal protocol would land us in exactly the same problem that we faced when we used the Food associated type in an earlier section. Just like an associated type, a Self requirement in a protocol also renders the protocol unusable as a first-class type.

let animal: Animal = cow
// Error: Protocol 'Animal' can only be used as a generic constraint because it has Self or Associated type requirements

let animals: [Animal] = [cow, deer]
// Error: Protocol 'Animal' can only be used as a generic constraint because it has Self or Associated type requirements

This is because a protocol with a Self requirement is also an incomplete protocol. Even if we could define a variable of the above protocol type Animal, how would we use this variable since we cannot know from the protocol with which concrete type of animal this animal can mate.

The solution is to again think in a protocol-oriented way and, rather than using a protocol to create an abstraction for the animals themselves, use a protocol to create an abstraction for what animals can do in this context, i.e., they can mate with other animals of the same type. Using this approach, we use a protocol to create an appropriate trait and a protocol extension to implement the trait.

protocol Mating {
    func mate(with: Self)
}

extension Mating {
    func mate(with other: Self) {
        print("Mating with another \(type(of: other))")
    }
}

It is simple to make concrete animal types conform to this protocol. If we don’t have these concrete types, we can declare the conformance when we define the concrete types; if we already have them, we can declare conformance via extensions to the concrete types. Here, we show the former approach, with conformance declared at the time of creating concrete animal types.

struct Cow: Mating {}
struct Deer: Mating {}

This affords us the same benefit of the Swift type system that we had when we gave the Animal protocol a Self requirement, i.e., animals are allowed to mate with their own type but not with an animal of another type.

cow.mate(with: anotherCow)
// Mating with another Cow
deer.mate(with: anotherDeer)
// Mating with another Deer

cow.mate(with: deer)
// Error: Cannot convert value of type 'Deer' to expected argument type 'Cow' 

Note that the Mating protocol has a Self requirement and cannot be used as a first-class type. Unlike the Animal protocol, however, where not being able to use the protocol type as a first-class type posed a problem because animal is a domain entity, this limitation actually makes sense when we think about the use cases of Mating. When and why would we want to create a variable of type Mating or an array with elements of type Mating. Even if we could do so, how would we use such a variable or such an array since we will not know the concrete type of animals that can mate with an animal instance stored in such a variable or in an element of such an array.

Use of Self requirements in the standard library

Self requirements are quite common in protocols found in the standard library. Take the Equatable protocol as an example, which enables two instances of a type to be compared for value equality using the == operator and value inequality using != operator. The following function is required to conform to the Equatable protocol.

static func ==(Self, Self) -> Bool

Use of Self placeholders for the types of both parameters ensures that, in implementations provided by conforming types, both the parameters must be of the conforming type. Consider the case where we have the following Course type.

struct Course {
    var code: Int
    var name: String
}

We can make Course conform to the Equatable protocol through the following extension, where the == function we implement replaces both the Self placeholders with the Course type.

extension Course: Equatable {
    static func ==(lhs: Course, rhs: Course) -> Bool {
        lhs.code == rhs.code && lhs.name == rhs.name
    }
}

The above implementation of == is shown for illustrative purposes only. In practice, Swift is able to automatically synthesize Equatable conformance for most structs and enums, provided Equatable conformance is declared in the type’s original declaration. So we could simply have defined our Course type as follows to make it conform to Equatable.

struct Course: Equatable {
    var code: Int
    var name: String
}

Two other commonly used standard library protocols Comparable and Hashable, both of which inherit from Equatable, also have Self requirements. Another protocol with Self requirements that is used often is Numeric, which provides a basis for arithmetic on scalar values, such as integers and floating-point numbers. The Numeric protocol can be used to write generic methods that can operate on any numeric type in the standard library. It can also be used as a constraint on type parameters of generic types to ensure that certain type parameters can only be used with numeric types, without having to specify the specific numeric type until the moment of instance creation.

Other advantages of protocol-oriented design

So far in this article, we have seen how protocols can help avoid having to use associated types in many situations while also enabling associated types and Self requirements to be used effectively where appropriate. The protocol-oriented approach that we have used offers other advantages as well. To see these advantages in action, let’s quickly recap the two design approaches we have used to deal with a domain dealing with animals. In the first approach, we used a protocol to create an abstract Animal type, with an associated type Food as a placeholder for the types of food different animals eat. Subsequently, we showed the same Animal protocol with a Self requirement to model the characteristic that animals can mate, but only with animals of the same type. If we put these requirements together, the Animal protocol would look like this.

protocol Animal {
    associatedtype Food
    func eat(_: Food)
    func mate(with: Self)
}

As we saw, this approach creates issues as it renders the Animal type unusable as a first-class type, making the abstraction quite useless. We then presented a protocol-oriented approach, using protocols as abstractions for what animals can do rather than for the animals themselves. We created the following protocols to model the ability of animals to eat different types of food.

protocol GrassEating {
    func eat(_: Grass)
}

protocol MeatEating {
    func eat(_: Meat)
}

With this approach, we could use the protocols as first-class types, using the abstractions to represent individual animals and groups of animals according to the food they eat. We then used the same approach to represent the mating trait, with the following protocol.

protocol Mating {
    func mate(with: Self)
}

These traits, as we have seen, can be easily added not only to new animal types by declaring conformance to the protocol at the time of defining the concrete types but also to existing animal types through retroactive modeling by declaring conformance to the protocol in extensions to the concrete types.

Now let’s see what other advantages, besides the ones we have already seen, we get with a protocol-oriented design where we use protocols as abstractions for traits rather than as abstractions for domain entities. We will briefly cover two additional benefits of this protocol-oriented approach in the remainder of this article.

1. Dealing with exceptional conditions

If we use a protocol to model a domain entity, as we did with the Animal protocol, the implicit assumption is that all animals have the same characteristics, i.e., they eat a particular type of food and they mate with their own kind. But what if there are animals that do one but not the other. One example of such an animal is a mule, which is produced by breeding a jack (male donkey) with a mare (female horse). Ignoring for a moment that in our model, where an animal can mate only with its own kind, a mule cannot be born, how would we use a model where every animal must have a mate(with:) method, to deal with a case where an animal needs to eat but cannot mate.

Such exceptions do occur in real-life domains and using protocols to create abstractions for domain entities can sometimes back us into corners from which there may not be a graceful exit. What would we do if we need to model an animal that needs to eat but cannot mate? Make the mate(with:) method throw an error to deal with the exceptional case? That would make the API awkward to use at client sites, in particular when the overwhelming majority of concrete animal types will actually not throw an error but client sites will have to incorporate error handling to be able to cater to the few exceptions. Moreover, if the exception is not known during initial design, introducing it later may risk breaking existing client code.

With a protocol-oriented approach, however, we can easily handle such exceptions because we use abstractions to represent the individual traits rather than the animals themselves. If we need to model an animal that does not need one or more of the traits, we simply don’t make that concrete type conform to those traits. We show this by creating two new kinds of animals, both of which eat grass but one of them does not mate.

struct Horse: GrassEating, Mating {}
struct Mule: GrassEating {}

let horse = Horse()
let mule = Mule()
let anotherHorse = Horse()
let anotherMule = Mule()

horse.eat(grass)
// Eating Grass
horse.mate(with: anotherHorse)
// Mating with another Horse

mule.eat(grass)
// Eating Grass
mule.mate(with: anotherMule)
// Error: Value of type 'Mule' has no member 'mate'

This approach obviates the need to anticipate or design for all possible exceptions at the outset. With individual traits represented by separate protocols and concrete types composed using these traits, it is quite easy to deal with exceptions regardless of whether they are known during initial design or present themselves at a later stage.

2. Extending the domain

Another advantage of protocol-oriented design is that it allows us to easily extend the domain. This is because traits modeled using protocols represent certain characteristics or abilities that apply to domain entities. The traits don’t say anything about what the domain entities are and can be used for any domain entity that has the same characteristics or abilities. Let’s consider the eating and mating traits as an example, which we have thus far applied to animals because that was the requirement of the domain at the time. We are free, however, to apply the same traits to any domain entity that needs to have the same characteristic or ability. In this case, for instance, we could easily extend the domain to include humans in the same context, since humans also eat specific types of food and mate only with other humans. As humans have a more diversified palate than animals, we just need to create some new types of food.

struct Vegetable {}
struct Fruit {}

let vegetable = Vegetable()
let fruit = Fruit()

Next, we create traits for eating these new types of food, using protocols and protocol extensions, as we did for other eating traits.

protocol VegetableEating {
    func eat(_: Vegetable)
}

extension VegetableEating {
    func eat(_: Vegetable) {
        print("Eating a Vegetable")
    }
}

protocol FruitEating {
    func eat(_: Fruit)
}

extension FruitEating {
    func eat(_: Fruit) {
        print("Eating a Fruit")
    }
}

Now we can create a concrete type to model humans.

struct Human: MeatEating, VegetableEating, FruitEating, Mating {}

Note that we have used the newly defined traits VegetableEating and FruitEating seamlessly with the traits MeatEating and Mating that we had defined for animals.

An instance of the Human type defined above will automatically get methods to eat the three types of food that we have specified and a method to mate with other humans.

let human = Human()

human.eat(meat)
// Eating Meat
human.eat(vegetable)
// Eating a Vegetable
human.eat(fruit)
// Eating a Fruit

let anotherHuman = Human()
human.mate(with: anotherHuman)
// Mating with another Human

Conclusion

Associated types and Self requirements are important features of Swift and are used extensively in the standard library. It is crucial to bear in mind, however, that a protocol with an associated type or a Self requirement is an incomplete protocol and cannot be used as a first-class type. This is why it is not advisable to use such protocols to represent abstractions for domain entities since such abstractions will be unusable. With a protocol-oriented approach, however, where protocols are used to represent things domain entities can do and roles domain entities can play rather than the domain entities themselves, it is possible in many cases to avoid having to use associated types in protocols. This approach also makes it possible to use associated types and Self requirements in protocols, where appropriate, to leverage the advantages that associated types and Self requirements offer while avoiding the pitfalls.

Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a discussion on Twitter.

Subscribe to get notifications of new posts

No spam. Unsubscribe any time.

Reader Interactions

Comments

  1. Maheen says

    September 16, 2020 at 6:04 pm

    Brilliant! Always look forward to these posts, hope to see more soon.

    Reply
    • Khawer Khaliq says

      September 16, 2020 at 6:12 pm

      Thanks. Great to see that you enjoyed the article.

      Reply
  2. Ivan says

    December 1, 2020 at 1:12 pm

    Thanks! Very enlightening article.

    Reply
    • Khawer Khaliq says

      December 1, 2020 at 7:00 pm

      Thanks Ivan. Happy to see that you found the article useful.

      Reply
  3. Paul says

    December 31, 2020 at 11:30 am

    This is a brilliantly thorough and insightful article. As the sole iOS developer for a startup, I’ve written 30k lines across 350 swift files (and 90 protocols) this past year. Your article has made it easy for me to understand why some of the protocols in my project have been awkward to work with. Thanks Khawer for writing this and providing me with the thought-process on how to address the issues!

    Reply
    • Khawer Khaliq says

      January 2, 2021 at 10:04 am

      Thanks Paul, for your kind words. Really glad that you found the article helpful.

      Reply
  4. Eric says

    March 19, 2021 at 5:10 pm

    Really nice article, which explains the core idea of protocol oriented approach in a simple but effective way. Thanks.

    Reply
    • Khawer Khaliq says

      March 26, 2021 at 1:31 pm

      Thanks Eric. Really happy that you liked the article.

      Reply
  5. Ramon says

    June 26, 2022 at 1:27 pm

    Good article with solid examples.

    I have one question though: In your questions example, how would you then go about creating an array of questions (e.g., to create a quiz of varying question types)? Because when using [AutoMarkable] as the array’s type obviously yields the same problem as before. Introducing another protocol or parent struct for this would work, but then you won’t be able to directly call the AutoMarkable parameters anymore. Introducing an enum with a QuestionType as a protocol could be a solution, but then you would need to downcast the type before being able to use the concrete questions. Do you have any ideas about this and how to neatly address this issue?

    Reply
    • Khawer Khaliq says

      July 15, 2022 at 6:56 am

      Hi Ramon,

      Thanks for your comment. So glad that you liked the article.

      All the approaches that you have indicated will work but, as you have noted, they have their own drawbacks and which approach should be adopted will depend on the context. Unfortunately, to my knowledge, there is no ‘neat’ way to address this issue using a protocol with an associated type.

      Reply
  6. Thet Tun says

    September 9, 2022 at 6:14 am

    Love it! Been looking for an article like this for quite some time. Not only did I learn more about the limits of protocols in swift language, but also how to properly use the protocol-oriented approach to solve its limitations. Big thanks for the great explanation.

    Reply
    • Khawer Khaliq says

      September 11, 2022 at 8:16 pm

      Many thanks for your kind remarks. Really glad that you found the article useful.

      Reply
  7. Olya says

    October 15, 2022 at 5:57 am

    Brilliant. There were ‘a-ha’ moments that I really needed.

    Reply
  8. Saikumar Kankipati says

    February 16, 2023 at 1:14 am

    Came across this article so late, but nevertheless its a worthy read, as you have emphasized using protocols for designing traits of domain objects but not as abstractions for domain objects clearly with examples, do you have any other article that talks about how design abstractions for domain objects in swift ? a deep dive in that would complete the great points you have discussed here

    Reply
    • Khawer Khaliq says

      February 16, 2023 at 6:59 am

      Hi,

      Glad to see that you liked the article. Having come from an object-oriented background, I always had trouble with protocols because I was using them to model domain objects, as with classes in object-oriented programming. Using protocols to model traits is a subtle shift in the thinking process, which makes protocols feel more natural and intuitive. Regarding your question on other resources, there is a series of articles by Rob Napier on the same subject. I found these articles very insightful. Hope you do too. Here is a link to the first article: https://robnapier.net/start-with-a-protocol

      I have also written an article on retroactive modeling using protocols, which demonstrates some of the same concepts. The essential point is that, unlike object-oriented programming where the conventional wisdom is to start with abstractions, with protocol-oriented programming you can start concrete and add protocols to model domain abstractions as and when required. This has two advantages. First, you don’t use unnecessary abstractions which can make code more complex and often inefficient due to indirection. Second, you avoid getting stuck with abstractions that don’t scale well with your design. With retroactive modeling, you can add and remove abstractions as required, making designs simpler and more flexible and scalable. Here is a link to the article: https://khawerkhaliq.com/blog/swift-protocols-retroactive-modeling/

      Reply
  9. Manish Nahar says

    September 18, 2023 at 5:50 am

    An excellent explanation for the Associated type. The Associated type is a powerful feature in Swift.
    The opaque type, introduced in Swift 5.1, serves as an enhancement to the existing Associated type mechanism. Please extend your article to explain how opaque type helps in solving the problems associated with Associated types

    Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Footer

Protocol-Oriented Programming (POP) in Swift

Use protocol-oriented programming to think about abstractions in a completely different way, leveraging retroactive modeling to introduce appropriate abstractions at any point in the development cycle, and creating traits that can let types opt into functionality simply by conforming to a protocol.

Pattern Matching With Optionals in Swift

The optional pattern explained in detail, including how to use it with a variety of conditional statements and loops, and how to add extra conditions when required, with a section on creating more complex pattern matching code involving optionals.

Test-Driven Development (TDD) in Swift

Learn how to use Test-Driven Development (TDD) in Swift which not only enables you to write more reliable and maintainable code but also allows refactoring of code in small increments and with greater ease and confidence.

Unwrapping Optionals With Optional Binding in Swift

Learn how to use optional binding to extract the value wrapped by an optional to a constant or variable, as part of a conditional statement or loop, exploring where optional chaining may be used in place of optional binding, and where these techniques can be used together.

Unit Testing and UI Testing in Swift

Learn how to use unit testing to gain confidence in the correctness of code at the unit level, and use UI testing to ensure that the application fulfills user requirements, explained in detail and illustrated using an example application built using SwiftUI.

When and How to Use the Equatable and Identifiable Protocols in Swift

Detailed coverage of the Equatable and Identifiable protocols, and how they can be used to model not only values but also domain entities with identity using Swift value types, to create code that is more efficient, easier to reason about, easily testable and more concurrency-friendly.

The Power of Optional Chaining in Swift

Learn how to use optional chaining to work safely with optionals, to set and retrieve the value of a property of the wrapped instance, set and retrieve a value from a subscript on the wrapped instance, and call a method on the wrapped instance, all without having to unwrap the optional.

What Are Swift Optionals and How They Are Used

This article explains what Swift optionals are, why Swift has them, how they are implemented, and how Swift optionals can be used to better model real-world domains and write safer and more expressive code.

Encapsulating Domain Data, Logic and Business Rules With Value Types in Swift

Leverage the power of Swift value types to manage domain complexity by creating rich domain-specific value types to encapsulate domain data, logic and business rules, keeping classes lean and focused on maintaining the identity of entities and managing state changes through their life cycles.

Rethinking Design Patterns in Swift – State Pattern

The State pattern, made simpler and more flexible with the power of Swift, with a detailed worked example to illustrate handling of new requirements, also looking at key design and implementation considerations and the benefits and practical applications of the pattern.

Conditional Logic With and Without Conditional Statements in Swift

Shows how to implement conditional logic using conditional statements as well as data structures, types and flow control mechanisms such as loops, balancing simplicity and clarity with flexibility and future-proofing.

Better Generic Types in Swift With the Numeric Protocol

Covers use of the Numeric protocol as a constraint on type parameters of generic types to ensure that certain type parameters can only be used with numeric types, using protocol composition to add relevant functionality as required.

Understanding, Preventing and Handling Errors in Swift

Examines the likely sources of errors in an application, some ways to prevent errors from occurring and how to implement error handling, using the error handling model built into Swift, covering the powerful tools, associated techniques and how to apply them in practice to build robust and resilient applications.

When and How to Use Value and Reference Types in Swift

Explores the semantic differences between value and reference types, some of the defining characteristics of values and key benefits of using value types in Swift, leading into a discussion on how value and reference types play a complementary role in modeling real-world domains and designing applications.

Swift Protocols Don’t Play Nice With Equatable. Or Can They? (Part Two)

Uses type erasure to implement Equatable conformance at the protocol level, allowing us to program to abstractions using protocol types while safely making equality comparisons and using functionality provided by the Swift Standard Library only available to types that conform to Equatable.

Copyright © Khawer Khaliq, 2017-25. All rights reserved.