Question 3
Protocols, generics, and where clauses
How do you use protocol-oriented design with generics and associated types? When do you need a where clause on an extension or generic method?
Answer outline
Protocols describe what a type can do. Generics let one piece of code work with any type that fits. An associated type is a placeholder in a protocol that each conformer fills in with its own type.
Use an associated type when a protocol needs a relationship between types rather than a single fixed type. Typical cases are a store tied to one model type or a coordinator that produces one specific output type.
A where clause adds extra constraints to generic code. The extension or method then exists only for types that meet them, such as an associated type that conforms to Equatable, Hashable, or Sendable.
Principles
- Protocols describe a role, so name them for the capability they promise.
- Generics keep one implementation reusable and type-safe without casting at the call site.
- Use an associated type when each conformer picks its own related type, such as a cache's value type.
- Put a
whereclause on an extension to add behavior only for types that can support it. - Prefer compile-time constraints over runtime type checks, and keep abstractions no wider than the code needs.
Every Cache works with some Value, but each conforming type chooses its own value type:
protocol Cache {
associatedtype Value
func save(_ value: Value, for key: String)
func load(for key: String) -> Value?
}
One implementation serves any value type:
final class MemoryCache<T>: Cache {
private var storage: [String: T] = [:]
func save(_ value: T, for key: String) {
storage[key] = value
}
func load(for key: String) -> T? {
storage[key]
}
}
This extension uses ==, so it only makes sense when Value is Equatable, and the where clause says so:
extension Cache where Value: Equatable {
func contains(_ value: Value, for key: String) -> Bool {
load(for: key) == value
}
}
Both forms require T to conform to Equatable, so <T: Equatable> and a trailing where clause are equivalent here:
func areEqual<T: Equatable>(_ lhs: T, _ rhs: T) -> Bool {
lhs == rhs
}
func compare<T>(_ lhs: T, _ rhs: T) -> Bool where T: Equatable {
lhs == rhs
}
This extension exists only when the repository's Model is Sendable:
protocol Repository {
associatedtype Model
func save(_ model: Model)
}
extension Repository where Model: Sendable {
func saveSafely(_ model: Model) {
save(model)
}
}



