26
Advanced Protocols & Generics
Written by Ehab Yosry Amer
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
This chapter covers more advanced uses of protocols and generics. Expanding on what you’ve learned in previous chapters, you’ll make protocols with constraints to Self
, other associated types and even recursive constraints.
Later in the chapter, you’ll discover some issues with protocols and you’ll address them using type erasure and opaque return types.
Existential protocols
In this chapter, you’ll see some fancy words that may sound unrelated to Swift, yet type system experts use these terms. It’ll be good for you to know this terminology and realize it isn’t a big deal.
Existential type is one such term. Fortunately, it’s a name for something you already know and have used. It’s simply a concrete type accessed through a protocol.
Example time. Put this into a playground:
protocol Pet {
var name: String { get }
}
struct Cat: Pet {
var name: String
}
In this code, the Pet
protocol says that pets must have a name. Then you created a concrete type Cat
which conforms to Pet
. Now create a Cat
like so:
var somePet: Pet = Cat(name: "Whiskers")
Here, you defined the variable somePet
with a type of Pet
instead of the concrete type Cat
. Here Pet
is an existential type — it’s an abstract concept, a protocol, that refers to a concrete type, a struct, that exists.
To keep things simple, from now on we’ll just call it a protocol type. These protocol types look a lot like abstract base classes in object-oriented programming, but you can apply them to enums and structs as well.
Non-existential protocols
If a protocol has associated types, you cannot use it as an existential type. For example, if you change Pet
like so:
protocol Pet {
associatedtype Food
var name: String { get }
}
protocol WeightCalculatable {
associatedtype WeightType
var weight: WeightType { get }
}
class HeavyThing: WeightCalculatable {
// This heavy thing only needs integer accuracy
typealias WeightType = Int
var weight: Int {
100
}
}
class LightThing: WeightCalculatable {
// This light thing needs decimal places
typealias as WeightType = Double
var weight: Double {
0.0025
}
}
class StringWeightThing: WeightCalculatable {
typealias WeightType = String
var weight: String {
"That doesn't make sense"
}
}
class CatWeightThing: WeightCalculatable {
typealias WeightType = Cat
var weight: Cat {
Cat(name: "What is this cat doing here?")
}
}
Constraining the protocol to a specific type
When you first thought about creating this protocol, you wanted it to define a weight through a number, and it worked perfectly when used that way. It simply made sense!
protocol WeightCalculatable {
associatedtype WeightType: Numeric
var weight: WeightType { get }
}
extension WeightCalculatable {
static func +(left: Self, right: Self) -> WeightType {
left.weight + right.weight
}
}
var heavy1 = HeavyThing()
var heavy2 = HeavyThing()
heavy1 + heavy2 // 200
var light1 = LightThing()
heavy1 + light1 // the compiler detects your coding error
Expressing relationships between types
Next, look at how you can use type constraints to express relationships between types.
protocol Product {}
protocol ProductionLine {
func produce() -> Product
}
protocol Factory {
var productionLines: [ProductionLine] {get}
}
extension Factory {
func produce() -> [Product] {
var items: [Product] = []
productionLines.forEach { items.append($0.produce()) }
print("Finished Production")
print("-------------------")
return items
}
}
struct Car: Product {
init() {
print("Producing one awesome Car 🚔")
}
}
struct CarProductionLine: ProductionLine {
func produce() -> Product {
Car()
}
}
struct CarFactory: Factory {
var productionLines: [ProductionLine] = []
}
var carFactory = CarFactory()
carFactory.productionLines = [CarProductionLine(), CarProductionLine()]
carFactory.produce()
struct Chocolate: Product {
init() {
print("Producing one chocolate bar 🍫")
}
}
struct ChocolateProductionLine: ProductionLine {
func produce() -> Product {
Chocolate()
}
}
var oddCarFactory = CarFactory()
oddCarFactory.productionLines = [CarProductionLine(), ChocolateProductionLine()]
oddCarFactory.produce()
protocol Product {
init()
}
protocol ProductionLine {
associatedtype ProductType
func produce() -> ProductType
}
protocol Factory {
associatedtype ProductType
func produce() -> [ProductType]
}
struct Car: Product {
init() {
print("Producing one awesome Car 🚔")
}
}
struct Chocolate: Product{
init() {
print("Producing one Chocolate bar 🍫")
}
}
struct GenericProductionLine<P: Product>: ProductionLine {
func produce() -> P {
P()
}
}
struct GenericFactory<P: Product>: Factory {
var productionLines: [GenericProductionLine<P>] = []
func produce() -> [P] {
var newItems: [P] = []
productionLines.forEach { newItems.append($0.produce()) }
print("Finished Production")
print("-------------------")
return newItems
}
}
var carFactory = GenericFactory<Car>()
carFactory.productionLines = [GenericProductionLine<Car>(), GenericProductionLine<Car>()]
carFactory.produce()
Mini-exercise
Here’s a little challenge for you. Try to see if you can do the following two things:
Recursive protocols
You can use a protocol type within that protocol itself, which is called a recursive protocol. For example, you can model a graph type as follows:
protocol GraphNode {
var connectedNodes: [GraphNode] { get set }
}
protocol Matryoshka {
var inside: Matryoshka {get set}
}
class HandCraftedMatryoshka: Matryoshka {
var inside: Matryoshka?
}
class MachineCraftedMatryoshka: Matryoshka {
var inside: Matryoshka?
}
var handMadeDoll = HandCraftedMatryoshka()
var machineMadeDoll = MachineCraftedMatryoshka()
handMadeDoll.inside = machineMadeDoll // This shouldn't fit
protocol Matryoshka: AnyObject {
var inside: Self? { get set }
}
final class HandCraftedMatryoshka: Matryoshka {
var inside: HandCraftedMatryoshka?
}
final class MachineCraftedMatryoshka: Matryoshka {
var inside: MachineCraftedMatryoshka?
}
handMadeDoll.inside = machineMadeDoll // compile error
Heterogeneous collections
Swift collections are homogeneous; that is, their elements must be of a single type. In this section, you’ll learn how to use the special type Any
to simulate heterogeneous collections. You’ll use WeightCalculatable
as an example:
protocol WeightCalculatable {
associatedtype WeightType: Numeric
var weight: WeightType { get }
}
var array1: [WeightCalculatable] = [] // compile error
var array2: [HeavyThing] = []
var array3: [LightThing] = []
class VeryHeavyThing: WeightCalculatable {
// This heavy thing only needs integer accuracy
typealias WeightType = Int
var weight: Int {
9001
}
}
var heavyList = [HeavyThing(), VeryHeavyThing()] // error
Type erasure
When you want to create a list of different items, you should define the list with a type that each element will conform to.
class AnyHeavyThing<T: Numeric>: WeightCalculatable {
var weight: T {
123
}
}
class HeavyThing2: AnyHeavyThing<Int> {
override var weight: Int {
100
}
}
class VeryHeavyThing2: AnyHeavyThing<Int> {
override var weight: Int {
9001
}
}
var heavyList2 = [HeavyThing2(), VeryHeavyThing2()]
heavyList2.forEach { print($0.weight) }
Opaque return types
The goal of type erasure is to hide unimportant details about concrete types but still communicate the type’s functionality using a protocol.
var carFactory = GenericFactory<Car>()
carFactory.productionLines = [GenericProductionLine<Car>(), GenericProductionLine<Car>()]
carFactory.produce()
var chocolateFactory = GenericFactory<Chocolate>()
chocolateFactory.productionLines = [GenericProductionLine<Chocolate>(), GenericProductionLine<Chocolate>()]
chocolateFactory.produce()
func makeFactory() -> Factory { // compile error
GenericFactory<Car>()
}
let myFactory = makeFactory()
func makeFactory() -> some Factory { // compiles!
GenericFactory<Car>()
}
func makeFactory(isChocolate: Bool) -> some Factory {
if isChocolate {
return GenericFactory<Chocolate>()
}
else {
return GenericFactory<Car>()
}
}
func makeFactory(numberOfLines: Int) -> some Factory {
let factory = GenericFactory<Car>()
for _ in 0..<numberOfLines {
factory.productionLines.append(GenericProductionLine<Car>())
}
return factory
}
func makeEquatableNumeric() -> some Numeric & Equatable {
return 1
}
let someVar = makeEquatableNumeric()
let someVar2 = makeEquatableNumeric()
print(someVar == someVar2) // prints true
print(someVar + someVar2) // prints 2
print(someVar > someVar2) // error
Challenges
Congratulations on making it this far! But before you come to the end of this chapter, here are some challenges to test your knowledge of advanced protocols and generics. It’s best if you try to solve them yourself, but solutions are available if you get stuck. You can find the solutions with the download or at the printed book’s source code link listed in the introduction.
Challenge 1: Robot vehicle builder
Using protocols, define a robot that makes vehicle toys.
Challenge 2: Toy train builder
Declare a function that constructs robots that make toy trains.
Challenge 3: Monster truck toy
Create a monster truck toy that has 120 pieces and a robot to make this toy. The robot is less sophisticated and can only assemble 200 pieces per minute. Next, change the makeToyBuilder()
function to return this new robot.
Challenge 4: Shop robot
Define a shop that uses a robot to make the toy that this shop will sell.
Key points
- You can use Protocols as existentials and as generic constraints.
- Existentials let you use a type, like a base class, polymorphically.
- Generic constraints express the capabilities required by a type, but you can’t use them polymorphically.
- Associated types make protocols generic. They provide great flexibility while still maintaining the type strictness.
- Constraints can be used in many contexts, even recursively.
- Type erasure is a way to hide concrete details while preserving important type information.
- Opaque return types let you return only protocol information from a concrete type.
- The more generic you write your code, the more places you will be able to use it.