Protocols

A protocol is a contract a type can satisfy. Anywhere a function constrains a type parameter to a protocol, any conforming type works — without that function knowing the concrete type.

A first protocol

protocol Drawable { func draw() } struct Circle { let radius: Float64 } extend Circle: Drawable { public func draw() { println("○ (r=\(self.radius))"); } } struct Square { let side: Float64 } extend Square: Drawable { public func draw() { println("□ (\(self.side))"); } } func render[T](shapes: [T]) where T: Drawable { for shape in shapes { shape.draw(); } } render([Circle(radius: 1.0), Circle(radius: 2.5)]); render([Square(side: 3.0)]);

Drawable is the contract. Circle and Square both conform via extend. render is written once and works on a list of any drawable type — the where T: Drawable constraint is what lets it call draw(). Each call site picks a concrete T, so polymorphism is resolved at compile time; there are no protocol-typed values in Kestrel. (To mix shapes in one collection, wrap them in an enum and dispatch with match.)

This is Kestrel's primary tool for abstraction. Anywhere you'd reach for inheritance in a class-based language, reach for a protocol here.

What you'll find here

  • Defining — declaring a protocol with required methods, properties, associated types
  • Conformance — making a type satisfy a protocol
  • Default Methods — fallback implementations the conforming type can override
  • Inheritance Rules — protocols composed of other protocols
  • Extending — adding behavior to all conforming types after the fact

When to reach for one

  • You're writing a function that doesn't care which concrete type it gets, only what it can do.
  • Several types share a capability but not a structure, and you want to write the operation once.
  • You want default behavior that types can opt into without re-implementing.

If the answer is "I want to share data layout," use a struct, not a protocol — protocols describe behavior, not fields.