Available for work
Ankit Ranjan
Back to Deep Dives

Protocols and Protocol-Oriented Programming — Witness Tables, any, and some

Swift's protocols, default implementations, and protocol-oriented programming reshaped how we model behaviour. Let's see what witness tables, any, and some really mean under the hood.

June 4, 2026 7 topics 7 quiz questions
Share:
1

Protocols declare requirements, not inheritance

Let's start with the core idea. A protocol is a contract. It lists the methods, properties, initialisers, and subscripts that a conforming type must provide. It says nothing about how they work, only that they must exist.

protocol Shape {
    var area: Double { get }
    func draw()
}

struct Circle: Shape {
    let radius: Double
    var area: Double { .pi * radius * radius }
    func draw() { print("drawing a circle") }
}
The colon in struct Circle: Shape declares conformance. In Swift, conformance is always explicit. A type does not accidentally satisfy a protocol because its method names happen to line up, the way Go interfaces work. You opt in by name, and the compiler then checks that every requirement is met.

So how is this different from inheriting a base class? A protocol is not a base class. It has no stored properties of its own, no designated initialiser to chain through, and no single-parent rule. A class can inherit from exactly one superclass, but any type — struct, enum, class, or actor — can conform to as many protocols as it likes.

One Base Class vs Many ProtocolsClass inheritance — one chainViewControlButtonInherit one base, carry all itsstate whether you need it or notProtocol composition — pick manystruct ButtonEquatableIdentifiableCodableHashableConform to as many as you like,no shared state forced on youA protocol is a contract, not a base class. A type can satisfy many at once.

That difference is the whole story. Inheritance gives you one rigid spine. Protocols let you mix in behaviour from several small, focused contracts. We can write struct Button: Equatable, Identifiable, Codable, Hashable and the type now satisfies four independent contracts without inheriting a single line of shared state.

2

Protocol extensions and default implementations

A contract that only lists requirements is useful, but it gets repetitive. If every conforming type has to write the same helper method, we have not saved much. Swift 2, back in 2015, fixed this with protocol extensions.

A protocol extension lets the protocol itself ship behaviour. Conforming types then get those methods for free, without writing a line.

protocol Greeter {
    func hello() -> String
}

extension Greeter {
    func greetTwice() {
        print(hello())
        print(hello())
    }
}

struct Robot: Greeter {
    func hello() -> String { "beep" }
}

Robot().greetTwice()  // beep, then beep — for free
We can even provide a default for a requirement, so a type may skip implementing it entirely and fall back on the protocol's version. This is how the standard library hands you dozens of methods on Collection from only a handful of requirements.

Now here is the subtlety that trips up everyone at least once. Inside a protocol extension, only members that are declared as requirements in the protocol are dynamically dispatched. A member added purely in the extension — never listed in the protocol body — is statically dispatched, chosen by the variable's compile-time type.

Default Implementations — What Gets Dispatched Whereprotocol Greeterfunc hello() // requirementextension Greeterfunc bye() // extension onlystruct Robot: Greeterfunc hello() { "beep" }func bye() { "boop" }Call via let g: any Greeter = Robot()g.hello() → "beep"requirement: dynamic dispatch,runs the type's overridethe protocol knows about hello()Same g, but bye() is extension onlyg.bye() → the extension's byeextension-only: static dispatch,picked by the static type any GreeterRobot.bye() is NOT consultedOnly members declared as protocol requirements are dispatched dynamically.

What does this mean in practice? If you call a method through a variable typed as the protocol, and that method is a real requirement, you get the type's own override. If the method exists only in the extension, you get the extension's version regardless of the actual type underneath. The fix is simple: if you want an extension method to be overridable, declare it as a requirement in the protocol body too.

3

Protocol-Oriented Programming — start with a protocol

In June 2015, at WWDC, Dave Abrahams gave a talk that gave the whole approach a name: "Protocol-Oriented Programming in Swift." Fans still call it the "Crusty" talk, after the grizzled old engineer Abrahams role-played to voice every objection a die-hard object-oriented programmer might raise. The mantra that came out of it was short and memorable: start with a protocol, not a class.

Why would we want to? Classes carry baggage. They are reference types, so two variables can point at the same instance and mutate it from under each other. Inheritance forces a single lineage, and a deep base class accumulates state that every subclass drags along. Abrahams' argument was that much of what we reach inheritance for — sharing behaviour, modelling capability — is better expressed as a protocol with a default implementation, adopted by a value type.

protocol Animatable {
    var duration: Double { get }
}

extension Animatable {
    func animate() { print("animating over \(duration)s") }
}

struct Fade: Animatable { let duration = 0.3 }
struct Slide: Animatable { let duration = 0.5 }
Here Fade and Slide are structs. They are copied, not shared, so there is no spooky aliasing. They gain animate() from the protocol extension without inheriting from anything. Add a new capability later and you write another small protocol rather than reshaping a class tree.

POP is not a rule that bans classes. Reference semantics are exactly right for shared, long-lived objects like a network session or a view controller. The point is the default: reach for a protocol and a value type first, and add a class only when you genuinely need identity and shared mutable state. That single shift in starting point is what Abrahams was selling, and it stuck.

4

Associated types and where clauses

Some protocols cannot name their types up front. Take a container: it has elements, but it should not care whether they are Int, String, or anything else. Swift expresses this with an associated type, a placeholder the conforming type fills in.

protocol Container {
    associatedtype Element
    var count: Int { get }
    mutating func append(_ item: Element)
    subscript(i: Int) -> Element { get }
}

struct Stack<T>: Container {
    private var items: [T] = []
    var count: Int { items.count }
    mutating func append(_ item: T) { items.append(item) }
    subscript(i: Int) -> T { items[i] }
    // Element is inferred to be T
}
This is exactly the design behind the standard library's Sequence and Collection, where the associated type is named Element. Every map, filter, and reduce you have ever written rides on it.

When we need to constrain those associated types, we use a where clause. It lets a generic function demand that a container's elements be, say, Equatable, or that two containers share the same element type.

func allEqual<C: Container>(_ c: C) -> Bool
    where C.Element: Equatable {
    // ...
}
For a long time, writing Container<Int> as a shorthand was impossible — you had to spell out the where clause every time. That changed with primary associated types in SE-0346 (Swift 5.7). By marking the associated type as primary, protocol Container<Element>, we can now write some Container<Int> or any Collection<Int> and constrain the element directly in angle brackets. It reads like a generic, and it removed a great deal of ceremony.

5

Existentials — any P and the box

Suppose we want a single variable that can hold any shape — a circle today, a square tomorrow. We cannot use the protocol as a normal generic constraint there, because we want one storage slot that swaps its concrete type at runtime. That is an existential, and in modern Swift we spell it any Shape.

The any keyword became explicit in SE-0335 (Swift 5.6). Before that, you wrote a bare protocol name as a type and the box was invisible, which hid a real cost. The keyword makes the cost legible.

let shapes: [any Shape] = [Circle(radius: 1), Square(side: 2)]
for shape in shapes {
    shape.draw()   // dispatched through the witness table
}
What actually happens? The value is wrapped in a box. The box holds the payload — stored inline if small, or a pointer to the heap if large — alongside a pointer to the type's protocol witness table. When you call a requirement, the runtime follows that pointer, looks up the method's slot, and jumps to the concrete implementation.

Witness Table Dispatch — How a Protocol Call Finds the Codevalue: Circleradius = 5.0witness ptrconforms to ShapeShape witness tablearea → 0x4A1draw → 0x4B7one table per conformanceCircle.areaconcrete codeCircle.drawconcrete codeA requirement call follows the witness pointer, looks up the slot, then jumps to the type's real method.

That extra hop, plus the boxing, is the price of flexibility. It also explains an old frustration. A protocol with Self requirements or associated types — like Equatable, whose == needs two values of the same concrete type — could not be used as an existential at all. The compiler had no way to guarantee the boxed types matched. Swift has since relaxed much of this, and primary associated types let you write constrained existentials such as any Collection<Int>, but the underlying reason for the old limitation was the box's lost type identity.

6

Opaque types — some P, reverse generics

The existential box is flexible but costly, and sometimes we do not need the flexibility. If a function always returns the same concrete type, we just do not want to spell its name out — perhaps because it is an unpronounceable nest of generics. That is what some P is for. It arrived in SE-0244 (Swift 5.1).

An opaque type is sometimes called reverse generics. With an ordinary generic, the caller picks the type and the function adapts. With some P, the function picks one concrete type and hides it; the caller knows only that it conforms to P.

func makeShape() -> some Shape {
    Circle(radius: 5)   // always a Circle, but the caller can't see that
}
any P versus some P — Two Ways to Hide a Typeany P — existential boxfunc draw(_ s: any Shape)heap boxpayload (or ptr)witness tabletype erased at runtimedynamic dispatch + indirectioncan hold any conforming type,even a different one each callsome P — opaque typefunc make() -> some Shapeconcrete: Circleradius = 5.0no box, no extra pointercaller cannot see the name,compiler knows it exactlyidentity preserved, static dispatchone fixed type per functionany = a box that can change type at runtime. some = one real type the caller is not told.

Because the compiler knows the one true type, there is no box, no extra pointer, and dispatch can stay static. Crucially, identity is preserved: two values returned from a some Shape function are guaranteed to be the same underlying type, so the compiler will let you compare them if that type is Equatable. An any Shape gives you no such promise.

This is precisely why SwiftUI writes var body: some View. A view's body is a deeply nested generic — something like VStack<TupleView<(Text, Button<Text>)>> — that nobody wants to type or even see. Returning some View lets you hide that monstrous type while keeping it fully concrete, so SwiftUI's diffing engine can rely on the stable identity to update the screen efficiently. No boxing, full performance, and a clean signature.

7

Putting it together — conformances and any vs some

A few features tie the whole picture together. Retroactive conformance lets you make a type you do not own conform to a protocol you do not own. Want Int to conform to your own Describable protocol? Write extension Int: Describable { ... } in your module and it is done. Use it judiciously — if two modules both retroactively conform the same type to the same protocol, you get a clash.

Conditional conformance, from SE-0143, lets a generic type conform only when its parameters do. An Array is Equatable only when its Element is Equatable, and the standard library expresses that as extension Array: Equatable where Element: Equatable. You get conformance exactly when it makes sense, and not a moment sooner.

And much of the time you write no conformance code at all. The compiler can synthesise Equatable, Hashable, and Codable for you. Declare the conformance, and as long as every stored property already conforms, Swift writes ==, hash(into:), and the coding logic itself.

struct Point: Equatable, Hashable, Codable {
    let x: Int
    let y: Int
    // no method bodies needed — all synthesised
}
So when do we reach for any and when for some? Here is the short version. Use some P when there is one underlying type you simply want to hide — a return value, a SwiftUI body, anything where you want full performance and preserved identity. Use any P when you genuinely need a heterogeneous bag — an array that holds different conforming types at once, or a value whose concrete type is decided at runtime. Put plainly: some hides one real type behind a curtain; any hides many possible types inside a box. Default to some, and pay for any only when the flexibility earns its keep.

Protocols and Protocol-Oriented Programming

Protocols are the backbone of Swift’s type system. This episode covers what protocols really are, how default implementations and witness tables work, and what the any and some keywords mean under the hood.

Protocols and Protocol-Oriented Programming Quiz

7 questions

Test your understanding of Swift protocols, default implementations, witness tables, any, and some.

Search

Loading search...