Chapters

Hide chapters

Swift Apprentice

1 min

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 13 chapters
Show chapters Hide chapters

28. Advanced Protocols & Generics
Written by Ehab Amer

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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 best 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 merely 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, such as a struct, that exists.

To keep things simple, we’ll call it a protocol type from now on. 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 Truck: WeightCalculatable {
  // This heavy thing only needs integer accuracy
  typealias WeightType = Int

  var weight: Int {
    100
  }
}

class Flower: WeightCalculatable {
  // This light thing needs decimal places
  typealias 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 heavyTruck1 = Truck()
var heavyTruck1 = Truck()
heavy1 + heavy2 // 200

var lightFlower1 = Flower()
heavyTruck1 + lightFlower1 // 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:

Type erasure

Type erasure is a technique for erasing type information that is not important. The type Any is the ultimate type erasure. It expunges all type information. As a consequence, it is lengthy and error-prone to use. As an example, consider the following collection types:

let array = Array(1...10)
let set = Set(1...10)
let reversedArray = array.reversed()
for e in reversedArray {
 print(e)
}

let arrayCollections = [array, Array(set), Array(reversedArray)]
let collections = [AnyCollection(array),
                   AnyCollection(set),
                   AnyCollection(array.reversed())]
let total = collections.flatMap { $0 }.reduce(0, +) // 165

Making a type erasure

Suppose you have the following Pet protocol:

protocol Pet {
  associatedtype Food
  func eat(_ food: Food)
}
enum PetFood { case dry, wet }

struct Cat: Pet {
  func eat(_ food: PetFood) {
    print("Eating cat food.")
  }
}

struct Dog: Pet {
  func eat(_ food: PetFood) {
    print("Eating dog food.")
  }
}
let pets: [Pet] = [Dog(), Cat()] // ERROR: Pet can only be used as a generic constraint 
struct AnyPet<Food>: Pet {                  // 1
  private let _eat: (Food) -> Void          // 2

  // 3
  init<SomePet: Pet>(_ pet: SomePet) where SomePet.Food == Food {
    _eat = pet.eat(_:)
  }

  // 4
  func eat(_ food: Food) {
    _eat(food)
  }
}
let pets = [AnyPet(Dog()), AnyPet(Cat())]

Implement eraseToAnyPet

A relatively new convention is to add an eraseToAny***() method to make type erasure more convenient. For example, in Apple’s Combine framework, there’s a type-erased type called AnyPublisher and you call the method eraseToAnyPublisher() to get one.

extension Pet {
  func eraseToAnyPet() -> AnyPet<Food> {
    .init(self)
  }
}
let morePets = [Dog().eraseToAnyPet(),
                Cat().eraseToAnyPet()]

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. The Any*** wrapper type that you create conforms to the protocol so that you can take advantage of it.

func makeValue() -> some FixedWidthInteger {
  42
}
print("Two makeValues summed", makeValue() + makeValue())
func makeValueRandomly() -> some FixedWidthInteger {
  if Bool.random() {
    return Int(42)
  }
  else {
    return Int8(24) // Compiler error.  All paths must return same type.
  }
}
func makeValueRandomly() -> some FixedWidthInteger {
  if Bool.random() {
    return Int(42)
  }
  else {
    return Int(24)
  }
}
let v: FixedWidthInteger = 42 // compiler error
let v = makeValue() // works
func makeEquatableNumericInt() -> some Numeric & Equatable { 1 }
func makeEquatableNumericDouble() -> some Numeric & Equatable { 1.0 }

let value1 = makeEquatableNumericInt()
let value2 = makeEquatableNumericInt()

print(value1 == value2) // prints true
print(value1 + value2) // prints 2
print(value1 > value2) // error
// Compiler error, types don't match up
makeEquatableNumericInt() == makeEquatableNumericDouble()

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 to try to solve them yourself, but solutions are available if you get stuck. You can find the solutions with the download or 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 existential 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 greater generality and can be type-checked.
  • 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 can potentially reuse it.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2023 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now