Polyglot
Languages Swift protocols
Swift § protocols

Protocols

Protocols are Swift’s principal abstraction mechanism — declarations of method, property, initialiser, and subscript requirements that conforming types must satisfy. The conventional Swift discipline is protocol-oriented programming (POP) — designing through protocols with default implementations (via extensions) rather than through class hierarchies. Protocols may have associated types, Self requirements, default implementations, and may be composed (A & B). Conformance is explicit — types must declare conformance via the colon syntax. The standard library is built on protocols (Sequence, Collection, Equatable, Hashable, Codable, Comparable, CustomStringConvertible); custom types gain substantial functionality by conforming. The combination — explicit conformance, default implementations via extensions, associated types for type-level relationships, the Self keyword, the standard-library protocol-oriented design — is the substance of Swift’s protocol mechanism.

Protocol declarations

protocol Greetable {
    var name: String { get }                       // required property
    func greet() -> String                         // required method
}

struct Person: Greetable {
    let name: String

    func greet() -> String {
        "Hello, \(name)"
    }
}

let p = Person(name: "Alice")
print(p.greet())                                   // "Hello, Alice"

The form: protocol Name { requirements }. Conforming types must implement all requirements.

Property requirements

Properties may be get (readable) or get set (readable and writable):

protocol Configurable {
    var name: String { get }                       // readable; may be let or var
    var value: Int { get set }                     // must be var (read and write)
}

struct Setting: Configurable {
    let name: String                               // satisfies get-only with let
    var value: Int                                  // satisfies get set with var
}

For static properties (type-level):

protocol VersionInfo {
    static var version: String { get }
}

Method requirements

protocol Animal {
    func makeSound() -> String
    mutating func feed(_ food: Food)               // mutating allowed in struct conformance
}

struct Cat: Animal {
    var hunger = 100
    func makeSound() -> String { "Meow" }
    mutating func feed(_ food: Food) { hunger -= 10 }
}

The mutating is required in protocol method requirements that mutate self for value types; the same method may be non-mutating for class conformances.

Initialiser requirements

protocol Default {
    init()
}

struct Empty: Default {
    init() { }
}

class Origin: Default {
    required init() { }                            // class conformance requires `required`
}

The required admits subclasses to inherit the conformance.

Subscript requirements

protocol Lookup {
    subscript(key: String) -> String? { get }
}

struct Dictionary: Lookup {
    private var data: [String: String] = [:]
    subscript(key: String) -> String? { data[key] }
}

Protocol inheritance

Protocols may inherit from other protocols:

protocol Greetable {
    var name: String { get }
    func greet() -> String
}

protocol Formal: Greetable {                       // inherits from Greetable
    func formalGreet() -> String
}

struct Person: Formal {
    let name: String
    func greet() -> String { "Hi, \(name)" }
    func formalGreet() -> String { "Good day, \(name)" }
}

Conformance to Formal requires conformance to Greetable (and all its requirements).

Default implementations via extensions

Protocols may provide default implementations — methods that conformers can use without explicit definition:

protocol Describable {
    var name: String { get }
    func describe() -> String
}

extension Describable {
    func describe() -> String {                    // default implementation
        "I am \(name)"
    }
}

struct Person: Describable {
    let name: String
    // describe() is provided by the extension
}

let p = Person(name: "Alice")
p.describe()                                       // "I am Alice"

The mechanism admits substantial code reuse via the protocol-oriented programming paradigm.

For overriding the default:

struct Robot: Describable {
    let name: String

    func describe() -> String {                    // override the default
        "Robot designation: \(name)"
    }
}

Constrained extensions

Default implementations may be conditional on additional constraints:

protocol Container {
    associatedtype Item
    var items: [Item] { get }
}

extension Container where Item: Equatable {
    func contains(_ target: Item) -> Bool {
        items.contains { $0 == target }
    }
}

struct IntBag: Container {
    let items: [Int]
}

IntBag(items: [1, 2, 3]).contains(2)               // true

The where admits substantial conditional functionality — contains is admitted only when Item: Equatable.

Protocol composition

The & admits “conform to multiple protocols”:

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

func describe(_ subject: Named & Aged) -> String {
    "\(subject.name) (\(subject.age))"
}

struct Person: Named, Aged {
    let name: String
    let age: Int
}

print(describe(Person(name: "Alice", age: 30)))    // "Alice (30)"

The Named & Aged admits a value satisfying both protocols.

For substantial use cases, declaring an explicit composite protocol may be conventionally clearer:

protocol Person: Named, Aged { }

func describe(_ subject: Person) { /* ... */ }

Associated types

Treated more substantially in Generics.

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

struct IntStack: Container {
    var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}

Self requirements

The Self keyword admits “the conforming type”:

protocol Cloneable {
    func clone() -> Self
}

struct Point: Cloneable {
    let x: Int
    let y: Int
    func clone() -> Self {                         // Self = Point
        Point(x: x, y: y)
    }
}

The mechanism admits fluent APIs returning the conforming type.

Standard-library protocols

The substantial protocols built into the standard library:

Equatable

protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

struct Point: Equatable {
    let x: Int
    let y: Int
    // == is synthesised for structs whose properties are all Equatable
}

Point(x: 1, y: 2) == Point(x: 1, y: 2)            // true

Hashable

protocol Hashable: Equatable {
    func hash(into hasher: inout Hasher)
}

struct Person: Hashable {
    let name: String
    let age: Int
    // hash is synthesised
}

let set: Set<Person> = [Person(name: "Alice", age: 30)]

Comparable

protocol Comparable: Equatable {
    static func < (lhs: Self, rhs: Self) -> Bool
}

struct Distance: Comparable {
    let meters: Double

    static func < (lhs: Distance, rhs: Distance) -> Bool {
        lhs.meters < rhs.meters
    }
    // <=, >, >= derived from <
}

CustomStringConvertible

protocol CustomStringConvertible {
    var description: String { get }
}

struct Point: CustomStringConvertible {
    let x: Int
    let y: Int
    var description: String { "(\(x), \(y))" }
}

print(Point(x: 1, y: 2))                           // "(1, 2)"

Codable

typealias Codable = Encodable & Decodable

struct User: Codable {
    let id: Int
    let name: String
    let email: String
    // encoding/decoding synthesised automatically
}

let json = try JSONEncoder().encode(user)
let user = try JSONDecoder().decode(User.self, from: json)

Sequence and Collection

protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    func makeIterator() -> Iterator
}

protocol Collection: Sequence {
    associatedtype Index: Comparable
    var startIndex: Index { get }
    var endIndex: Index { get }
    subscript(position: Index) -> Element { get }
    func index(after i: Index) -> Index
}

These are the foundation of Swift’s iteration; conforming admits the substantial Sequence/Collection method library.

Protocol-oriented programming (POP)

The conventional Swift design philosophy — protocol-oriented programming:

  1. Define behaviour through protocols.
  2. Provide default implementations via extensions.
  3. Use protocol composition for substantial requirements.
  4. Use generic constraints (<T: Protocol>) for type-safe abstraction.
  5. Use some and any for runtime polymorphism.
// Behaviour:
protocol Logger {
    func log(_ message: String)
}

protocol Timestamped {
    var timestamp: Date { get }
}

// Defaults:
extension Logger where Self: Timestamped {
    func log(_ message: String) {
        print("[\(timestamp)] \(message)")
    }
}

// Composition:
struct AppLogger: Logger, Timestamped {
    var timestamp: Date { Date.now }
    // log() provided by extension
}

Protocols vs classes for inheritance

The conventional Swift discipline favours protocols over class inheritance:

ApproachExample
Class inheritanceclass Dog: Animal { ... }
Protocol conformancestruct Dog: Animal { ... }

Protocols admit:

  • Multiple conformance — a type may conform to several protocols.
  • Value types — structs (preferred) admit conformance.
  • Default implementations via extensions.
  • Retroactive conformance — adding conformance to types from other modules.

Class inheritance is reserved for genuine reference-semantic hierarchies.

Common patterns

Conformance

struct Person: Equatable, Hashable, Codable, CustomStringConvertible {
    let id: Int
    let name: String
    let age: Int

    var description: String {
        "Person(id: \(id), name: \(name), age: \(age))"
    }
    // Equatable, Hashable, Codable synthesised
}

Protocol with default implementation

protocol Trackable {
    var id: UUID { get }
    func track()
}

extension Trackable {
    func track() {
        print("Tracking \(id)")
    }
}

Protocol composition

typealias Identifiable & Hashable & Codable = StorageKey

func store<K: StorageKey, V>(_ value: V, for key: K) {
    /* ... */
}

Standard-library protocol-driven function

func sum<S: Sequence>(_ s: S) -> S.Element
where S.Element: Numeric {
    s.reduce(0, +)
}

sum([1, 2, 3])                                     // 6
sum(1...100)                                       // 5050
sum(stride(from: 0, to: 10, by: 2))                // 20

Custom Comparable

struct Version: Comparable {
    let major, minor, patch: Int

    static func < (lhs: Version, rhs: Version) -> Bool {
        (lhs.major, lhs.minor, lhs.patch) < (rhs.major, rhs.minor, rhs.patch)
    }
}

let versions = [Version(major: 1, minor: 2, patch: 0),
                Version(major: 1, minor: 1, patch: 5)]
versions.sorted()                                   // ascending
versions.max()                                      // 1.2.0

Protocol with associated type

protocol DataSource {
    associatedtype Item
    func numberOfItems() -> Int
    func item(at index: Int) -> Item
}

struct UserDataSource: DataSource {
    private let users: [User]
    func numberOfItems() -> Int { users.count }
    func item(at index: Int) -> User { users[index] }
}

Mock for testing via protocol

protocol UserService {
    func find(_ id: Int) async throws -> User?
    func save(_ user: User) async throws
}

class RealUserService: UserService {
    func find(_ id: Int) async throws -> User? { /* network */ }
    func save(_ user: User) async throws { /* network */ }
}

class MockUserService: UserService {
    var users: [Int: User] = [:]
    func find(_ id: Int) async throws -> User? { users[id] }
    func save(_ user: User) async throws { users[user.id] = user }
}

The pattern admits substantial dependency injection.

Protocol for retroactive functionality

extension String: Greetable {
    var name: String { self }
    func greet() -> String { "Hello, \(self)" }
}

"Alice".greet()                                    // "Hello, Alice"

The mechanism admits adding conformances to types from other modules — retroactive conformance.

RawRepresentable for enums with raw values

enum Status: String, RawRepresentable {
    case active
    case inactive
    case banned
}

let s = Status(rawValue: "active")                 // Optional(.active)
print(s!.rawValue)                                 // "active"

The conformance admits substantial conversion between raw values and enum cases.

Static methods in protocols

protocol Persisted {
    associatedtype Identifier
    static func find(_ id: Identifier) async throws -> Self?
}

struct User: Persisted {
    typealias Identifier = UUID
    static func find(_ id: UUID) async throws -> User? { /* ... */ }
}

A note on the conventional discipline

The contemporary Swift protocols advice:

  • Use protocols for abstraction; classes for reference semantics.
  • Provide default implementations via extensions where applicable.
  • Use protocol composition (A & B) for substantial requirements.
  • Use some for opaque return types.
  • Use any for heterogeneous collections.
  • Conform to standard-library protocols (Equatable, Hashable, Codable, CustomStringConvertible).
  • Use generic constraints with protocol bounds.
  • Use Self requirements for fluent APIs.
  • Use retroactive conformance sparingly.
  • Write protocols with substantial substance — single-method protocols may not justify the abstraction.

The combination — explicit conformance, default implementations, associated types, Self requirements, protocol composition, the standard-library’s substantial protocol surface, the protocol-oriented programming style — is the substance of Swift’s protocol mechanism. The discipline produces composable, testable, type-safe code with substantial reuse via default implementations.