Chapters

Hide chapters

Swift Apprentice: Fundamentals

First Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

18. Generics
Written by Alexis Gallagher

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

The truth is, you already know about generics. Every time you use a Swift array, you’re using generics. This observation might give the impression that generics are about collections, but that impression is incorrect. In this chapter, you’ll learn the fundamentals of generics, giving you a solid foundation for understanding how to write generic code. Finally, you’ll loop back to look at generic types in the Swift standard library — arrays, dictionaries and optionals — using this new perspective.

Introducing Generics

To get started, consider how you might model pets and their keepers. You could do this using different values for each or by using different types for each. You’ll see that by using types, instead of values, the Swift type checker can reason about your code at compile time. Not only do you need to do less at runtime, but you can catch problems that would have gone under the radar had you just used values. Your code also runs faster.

Values Defined by Other Values

Suppose you’re running a pet shop that sells only dogs and cats and want to use Swift to model that business. To start, you define a type, PetKind, that can hold two possible values corresponding to the two kinds of pets that you sell:

enum PetKind {
  case cat
  case dog
}
struct KeeperKind {
  var keeperOf: PetKind
}
let catKeeper = KeeperKind(keeperOf: .cat)
let dogKeeper = KeeperKind(keeperOf: .dog)
enum EnumKeeperKind {
  case catKeeper
  case dogKeeper
}
.pal NuosirBabb(buumehOq:.fed) VisZirw kedeip JuotuvYijd zowuej .hop .oll LuobeyKikl(foepehEn:.sof) ewr.

Types Defined by Other Types

The model above fundamentally works by varying the values of types. Now consider another way to model the pet-to-keeper system — by varying the types themselves.

class Cat {}
class Dog {}
class KeeperOfCats {}
class KeeperOfDogs {}
Daj Suokag (id Rem...) Met vwliz Geojoj dwkod Kux exp. Ruekiz (aj Rev...) inw.

Anatomy of Generic Types

Generics provide a mechanism for using one set of types to define a new set of types.

class Keeper<Animal> {}
Sah Roayex<Zub> Ray sgjun Faatef wmpis Jig Juejob<Zid>

var aCatKeeper = Keeper<Cat>()

Using Type Parameters

Usually, though, you’ll want to do something with type parameters.

class Cat {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Dog {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Keeper<Animal> {
  var name: String

  init(name: String) {
    self.name = name
  }
}
class Keeper<Animal> {
  var name: String
  var morningCare: Animal
  var afternoonCare: Animal

  init(name: String, morningCare: Animal, afternoonCare: Animal) {
    self.name = name
    self.morningCare = morningCare
    self.afternoonCare = afternoonCare
  }
}
let jason = Keeper(name: "Jason",
                   morningCare: Cat(name: "Whiskers"),
                   afternoonCare: Cat(name: "Sleepy"))

Generic Function Parameters

Functions can be generic as well. A function’s type parameter list comes after the function name. You can then use the generic parameters in the rest of the definition.

func swapped<T, U>(_ x: T, _ y: U) -> (U, T) {
  (y, x)
}

swapped(33, "Jay")  // returns ("Jay", 33)

Mini-Exercises

Type Constrained Generics

In your definition of Keeper, the identifier Animal serves as a type parameter, a named placeholder for some concrete type you supply later.

class Keeper<Animal: Pet> {
   /* definition body as before */
}
protocol Pet {
  var name: String { get }  // all pets respond to a name
}
extension Cat: Pet {}
extension Dog: Pet {}
func callForDinner<Animal>(_ pet: Animal) {
   // What can you write here?
}
func callForDinner<Animal: Pet>(_ pet: Animal) {
   print("Here \(pet.name)-\(pet.name)! Dinner time!")
}
func callForDinner(_ pet: some Pet) {
  print("Here \(pet.name)-\(pet.name)! Dinner time!")
}

Conditional Conformance

In addition to simple type constraints, you can define more complex type constraints using a generic where clause. You can use a where clause in defining functions, types, member functions, protocols, and extensions. It can constrain type parameters and associated types, letting you define rich relationships on top of generic types.

func callForDinner<Animal>(_ pet: Animal) where Animal: Pet {
  print("Here \(pet.name)-\(pet.name)! Dinner time!")
}
extension Array where Element: Cat {
  func meow() {
    forEach { print("\($0.name) says meow!") }
  }
}
protocol Meowable {
  func meow()
}

extension Cat: Meowable {
  func meow() {
    print("\(self.name) says meow!")
  }
}

extension Array: Meowable where Element: Meowable {
  func meow() {
    forEach { $0.meow() }
  }
}

Advanced Generic Parameters

Suppose you wish to write a function to find a lost animal. You start with an array of lost animals:

let lost: [any Pet] = [Cat(name: "Whiskers"), Dog(name: "Hachiko")]
/// Return a lost Cat.
func findLostCat(name: String) -> Cat? {
  lost.lazy.compactMap {
    $0 as? Cat
  }.first {
    $0.name == name
  }
}
/// Return a lost Dog.
func findLostDog(name: String) -> Dog? {
  lost.lazy.compactMap {
    $0 as? Dog
  }.first {
    $0.name == name
  }
}
func findLostPet(name: String) -> (any Pet)? {
  lost.first { $0.name == name}
}
func findLost<Animal: Pet>(_ petType: Animal.Type, name: String) -> (some Pet)? {
  lost.lazy.compactMap {
    $0 as? Animal
  }.first {
    $0.name == name
  }
}
findLost(Cat.self, name: "Whiskers")
findLost(Dog.self, name: "Hachiko")
func findLost<Animal: Pet>(_ petType: Animal.Type, name: String) -> Animal? {
  lost.lazy.compactMap {
    $0 as? Animal
  }.first {
    $0.name == name
  }
}
findLost(Cat.self, name: "Whiskers")?.meow()
// Whiskers says meow!

Arrays

While the original Keeper type illustrates that a generic type doesn’t need to store anything or use its type parameter, Array, one of the most common generic types, does both.

let animalAges: [Int] = [2,5,7,9]
let animalAges: Array<Int> = [2,5,7,9]

Dictionaries

Swift generics allow multiple type parameters, each with unique constraints. A Dictionary is a straightforward example of this.

struct Dictionary<Key: Hashable, Value> // etc..
let intNames: Dictionary<Int, String> = [42: "forty-two"]
let intNames2: [Int: String] = [42: "forty-two", 7: "seven"]
let intNames3 = [42: "forty-two", 7: "seven"]

Optionals

Finally, no discussion of generics would be complete without mentioning optionals. Optionals are enumerations, but they’re just another generic type, which you could have defined yourself.

enum OptionalDate {
  case none
  case some(Date)
}
enum OptionalString {
  case none
  case some(String)
}
struct FormResults {
  // other properties here
  var birthday: OptionalDate
  var lastName: OptionalString
}
enum Optional<Wrapped> {
  case none
  case some(Wrapped)
}
var birthdate: Optional<Date> = .none
if birthdate == .none {
  // no birthdate
}
var birthdate: Date? = nil
if birthdate == nil {
  // no birthdate
}

Challenge

Before moving on, here is a challenge to test your knowledge of generics. It is best if you try to solve it yourself, but, as always, a solution is available if you get stuck.

Challenge 1: Build a Collection

Consider the pet and keeper examples from earlier in the chapter:

class Cat {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Dog {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Keeper<Animal> {
  var name: String
  var morningCare: Animal
  var afternoonCare: Animal

  init(name: String, morningCare: Animal, afternoonCare: Animal) {
    self.name = name
    self.morningCare = morningCare
    self.afternoonCare = afternoonCare
  }
}
let christine = Keeper<Cat>(name: "Christine")

christine.lookAfter(someCat)
christine.lookAfter(anotherCat)

Key Points

  • Generics are everywhere in Swift: optionals, arrays, dictionaries, other collection structures, and most basic operators like + and ==.
  • Generics express systematic variation at the level of types via type parameters that range over possible concrete types.
  • Generics are like functions for the compiler. They are evaluated at compile-time and result in new types – specializations of the generic type.
  • A generic type is not a concrete type but more like a recipe, program, or template for defining new types.
  • Swift provides a rich system of type constraints, which lets you specify what types are allowed for various type parameters.
  • some Protocol refers to a concrete, generic type, while any Protocol refers to a concrete type in an existential box.
  • There are many ways to write generics with constraints, the most general being the generic where clause.
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.
© 2024 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