Protocols
Describing what a type can do, so code can depend on behaviour instead of concrete types.
A protocol is a list of requirements — methods, properties, initialisers — that a conforming type promises to provide. Code written against the protocol works with any type that conforms, present or future.
In an object-oriented language you'd reach for a base class here. Protocols do the job without the inheritance: a struct can conform, an enum can conform, a type can conform to many protocols, and you can add conformance to a type you don't own via an extension. That's why Swift can push value types so hard and still let you abstract.
The everyday payoff is testability. A view model that depends on ContentLoading rather than NetworkContentLoader can be handed a stub in a test without a server in sight.
protocol ContentLoading { func loadConcepts() async throws -> [String]}
// A struct conforms — no base class, no inheritancestruct BundleLoader: ContentLoading { func loadConcepts() async throws -> [String] { ["arc", "actors"] }}
// So does a test doublestruct FailingLoader: ContentLoading { func loadConcepts() async throws -> [String] { throw URLError(.notConnectedToInternet) }}
// Default behaviour for every conformer, in one placeextension ContentLoading { func loadCount() async throws -> Int { try await loadConcepts().count }}
// Retroactive conformance: teach a type you don't own a new trickextension Int: @retroactive Identifiable { public var id: Int { self }}
Show all 23 linesShow less
protocol ContentLoading { func loadConcepts() async throws -> [String]}
// A struct conforms — no base class, no inheritancestruct BundleLoader: ContentLoading { func loadConcepts() async throws -> [String] { ["arc", "actors"] }}
// So does a test doublestruct FailingLoader: ContentLoading { func loadConcepts() async throws -> [String] { throw URLError(.notConnectedToInternet) }}
// Default behaviour for every conformer, in one placeextension ContentLoading { func loadCount() async throws -> Int { try await loadConcepts().count }}
// Retroactive conformance: teach a type you don't own a new trickextension Int: @retroactive Identifiable { public var id: Int { self }}some versus any
some versus any
some ContentLoading means *one specific conforming type, chosen at compile time*. The compiler knows exactly which type it is, so the call is static and there's no box. This is an opaque type, and it's shorthand for a generic parameter.
any ContentLoading means *any conforming type, possibly different each time*. The value is wrapped in an existential box and calls go through a witness table at runtime. That flexibility costs an indirection — and it's what you need when you're storing a heterogeneous array of conformers.
Rule of thumb: some when there's one type and the caller doesn't need to know it, any when the type genuinely varies at runtime.
some and generics are the same mechanism seen from two directions.// One concrete type, resolved at compile time — no runtime boxfunc makeLoader() -> some ContentLoading { BundleLoader() }
// Genuinely heterogeneous — needs the existential boxlet loaders: [any ContentLoading] = [BundleLoader(), FailingLoader()]
// Generic constraint: the caller picks the type, the body stays staticfunc count<L: ContentLoading>(using loader: L) async throws -> Int { try await loader.loadConcepts().count}Under the hood
Calling a method through any Protocol uses a *witness table*: a table of function pointers, one per requirement, produced when the type declares conformance. The existential value carries a pointer to the value (or the value inline if it's small enough for the three-word buffer) plus a pointer to that table.
With some Protocol or a generic parameter, the compiler knows the concrete type at the call site, so it can look the witness up statically and often inline it entirely.
A protocol with associated types or Self requirements couldn't be used as any at all before Swift 5.7, because the box has no way to express "and the element type is the same one you got last time". It's allowed now, but the compiler asks you to open the existential before using those members — which is why you sometimes need a generic function to do the work.
Quick check
What's the practical difference between some P and any P as a return type?
- None; `some` is just newer syntax for `any`
- `some P` is one specific type fixed at compile time with static dispatch; `any P` is an existential box with dynamic dispatch that can hold different types
- `some P` allows returning different types on different calls, `any P` doesn't
- `any P` is only allowed for class-bound protocols
Reveal answer
`some P` is one specific type fixed at compile time with static dispatch; `any P` is an existential box with dynamic dispatch that can hold different types
some P is an opaque type: the compiler knows exactly which concrete type it is, the caller doesn't, and every call returns that *same* type. any P boxes the value with a witness table so calls dispatch dynamically and different calls can return different types. Option (c) has it exactly backwards — that flexibility is any's, not some's.
Interview question
When would you reach for a protocol, and when is one actively making a codebase worse?
Think first, then reveal
The case for a protocol is a *seam*: a point where I genuinely need to swap what's on the other side. Two real implementations, or one real implementation plus a test double that lets me test without a network, a database, or a clock. That's a protocol earning its keep — it turns an untestable dependency into an injectable one.
It makes things worse when it's speculative. A protocol with one conformer, added because it felt like good architecture, costs you a second file, an indirection that breaks jump-to-definition, and often a pile of any boxing. Nobody can see what the code actually does without opening two files, and the abstraction usually turns out to be the wrong shape when the second implementation finally arrives — because it was designed against a sample size of one.
The other failure mode is protocols that mirror a single type's entire API surface. That's not an abstraction, it's a duplicate declaration you now maintain twice. A good protocol is narrower than its conformers: ContentLoading with one method, not ContentLoaderProtocol with all fourteen.
Extracting a protocol later is a small, safe, mechanical refactor. Deleting one that's wired through forty files is not. So I let the second implementation pull it into existence.
What an interviewer listens for
- Ties the decision to a real second implementation, counting test doubles
- Names concrete costs: navigation, boxing, extra file, wrong-shape abstraction
- Calls out protocols that merely mirror one type's full API
- Observes that extraction later is cheaper than removal later
- Prefers narrow protocols over broad ones
Key takeaway
Protocols let code depend on behaviour rather than concrete types, without inheritance. Use some when one type is involved and any only when the type genuinely varies at runtime — and don't add a protocol until there's a second implementation.