Protocol
protocol declares a set of member signatures: whoever impls it can flow as the protocol type. It is an interface contract, and also the vehicle for generic constraints.
Declaration and Implementation
protocol Greeter
{
func Greet() -> string;
}
struct Tag impl Greeter
{
public var text: string = "tag";
public func Greet() -> string => $"hello, {text}";
}Protocol members are signatures only, no method bodies. The implementer attaches the protocol with an impl clause (available to both struct and class), then supplies same-shape members one by one.
Satisfaction is checked by exact match: member kind, parameter count, types and modifiers, and return type must all be strictly identical (the current version has no co- or contravariance).
Protocol-Position Assignment and Virtual Dispatch
A protocol name can be used directly as a type, holding any value that implements it:
func Introduce(g: Greeter) -> string => $"heard: {g.Greet()}";
val t = new Tag();
print(Introduce(t));
val g: Greeter = t;
print(g.Greet());
class DogImpl impl Greeter
{
public func Greet() -> string => "woof";
}
print(Introduce(new DogImpl()));heard: hello, tag
hello, tag
heard: woofIntroduce only knows the protocol surface and doesn’t care whether a struct or class is behind it. Calls through a protocol position dispatch on the runtime type; props go through virtual dispatch too, so a protocol position always reads the runtime type’s implementation.
Beyond direct assignment, as also converts a value to a protocol position: val w = r as Walker returns Walker? — a value that implements the protocol gets the reference, otherwise null. Values in any are likewise judged along the implementation chain; both o is Walker and o as Walker work. For the division of labor among is / as / T(v) see Conversions.
Self and Clone
Inside a protocol, Self refers to “the implementing type” — the key to writing contracts like Clone:
protocol Clone
{
func Clone(self) -> Self;
}After Tag impl Clone, Clone() returns Tag itself, and in generic code a T: Clone return value needs no cast either. In a derived class override, Self binds to each type on its own.
Operator Signatures
Protocols can require operators, and implementers supply same-shape operator overloads one by one. std::Math’s Numeric / Ordered are pure operator protocols; writing your own follows the same pattern:
protocol MoneyOps
{
operator +(self, other: Self) -> Self;
operator ==(self, other: Self) -> bool;
}
struct Money impl MoneyOps
{
public var cents: int = 0;
public operator +(self, other: Money) -> Money => new Money() { cents: self.cents + other.cents };
public operator ==(self, other: Money) -> bool => self.cents == other.cents;
}
val a = new Money() { cents: 100 };
val b = new Money() { cents: 50 };
print((a + b).cents);
print(a == new Money() { cents: 100 });150
TrueInside the protocol, Self lands on the implementer’s own type. Built-in scalars implicitly satisfy pure operator protocols by their built-in operator capability: in Sum<T> where T: Numeric, the whole int and float family needs no impl (see Generics).
static Members
Protocols can require static members, and the impl side must provide them as static. Static members have no receiver dispatch: when the concrete type is known at compile time the call is direct; in a generic constraint body, T.member dispatches on the concrete type at the instantiation point:
protocol Lerpable
{
static pure func Lerp(from: Self, to: Self, t: float) -> Self;
}
extension float impl Lerpable
{
public static pure func Lerp(from: float, to: float, t: float) -> float => from + (to - from) * t;
}
func Blend<T>(a: T, b: T, t: float) where T: Lerpable -> T
{
return T.Lerp(a, b, t);
}
print(float.Lerp(0.0, 10.0, 0.5));
print(Blend(1.0, 3.0, 0.5));5
2float.Lerp calls the implementer’s static method directly; T.Lerp inside Blend binds to float’s implementation when Blend(1.0, ...) is instantiated. Static protocol members are commonly attached to scalars and built-in types via extensions, which is also what lets scalars take part in generic constraints; see Extension.
fail Sets: Narrow but Never Widen
The fail set declared by a protocol member is part of the satisfaction key: the impl side’s fail set must not exceed the declaration. Narrowing is admitted, an equal set is admitted, widening is not. The safe direction is straightforward: the caller holds the protocol static type and writes consumption against the declared fail set, so what the virtual dispatch target can actually throw must not exceed that surface.
error Boom();
protocol Risky
{
func Go() fail Boom -> int;
}
struct Safe impl Risky
{
public func Go() -> int => 1;
}
val s = new Safe();
print(s.Go());1Narrowing can go as far as declaring no fail at all: calls through Safe’s own type position then have no failure surface. Call sites holding the protocol position (Risky) still consume against the declared fail Boom. The widening direction (say the impl writes fail Boom, Other) is rejected by the compiler at the impl check.
impl Propagates Along the Inheritance Chain
Protocols implemented by a base class are satisfied automatically by derived classes, and protocol positions accept the derived type directly:
protocol Named2
{
func Name() -> string;
}
class Base2 impl Named2
{
public virtual func Name() -> string => "base";
}
class Derived2 : Base2
{
public override func Name() -> string => "derived";
}
func Show(n: Named2) -> string => n.Name();
print(Show(new Derived2()));derivedDerived2 writes no impl, and Show still accepts it.
A derived class can also explicitly re-impl to override the base class’s implementation. The re-impl satisfaction check counts inherited-chain members as “its own members”: a base class’s public virtual same-shape member counts, and an error is raised only when the chain truly has no same-shape member. Protocol/virtual dispatch picks the nearest implementation on the chain by the receiver’s runtime type.
Generic Protocols
Protocols take type parameters, instantiated explicitly at impl time:
protocol Holder<T>
{
init();
func Get() -> T;
}
class IntBox impl Holder<int>
{
public var v: int = 0;
init()
{
v = 0;
}
public func Get() -> int { return v; }
}
func Pick<T>() where T: Holder<int> -> int
{
return new T().Get();
}
func main() -> int
{
val b = new IntBox() { v: 9 };
print(b.Get());
print(Pick<IntBox>());
return 0;
}9
0- The
Tin the protocol body is the type parameter, free to use in member signatures; protocols can carrywhereconstraints too. - impl must spell out the full instantiation (
impl Holder<int>); theinit();constructor signature requires the impl side to provide a matching constructor. - After the generic constraint
where T: Holder<int>,Tinside the constraint body has the protocol surface andnew T()is legal;Pick<IntBox>()gets a brand-new instance.
What a Protocol Can Require
- Method (
func) and property (prop) signatures. - Constructor signatures (
init();): the impl side must provide a matching constructor. When a protocol carries aninitsignature,new T(args)is legal in the generic body ofwhere T: P. - Operators (
operator +(self, other: Self) -> Self;): built-in scalars satisfy these implicitly by built-in operator capability; std::Math’sNumeric(the four arithmetic operations plus equality) andOrdered(adding ordering comparisons) are pure operator protocols; see Generics. - static members (
static pure func Lerp(...)): provided as static by the impl side;T.memberin a generic constraint body dispatches on the instantiated type, as above;static proptakes the same signature form, provided as a static property by the impl side. - Event signatures (
event Clicked(power: int);) and event prop signatures (event prop Level: int;): the impl side satisfies them by providing an event of the same name and payload (payload types must be equal position by position; an ordinary prop of the same name cannot masquerade as an event prop). An event prop signature must be the bare signature form — no accessor body, pipeline segments, or initializer. - Base protocols: the impl side of
protocol Ordered : Numericmust also satisfy everything Numeric requires.
protocol Clickable
{
event Clicked(power: int);
func Fire();
}
class Button impl Clickable
{
public event Clicked(power: int);
public func Fire()
{
emit Clicked(3);
}
}
val c: Clickable = new Button();
c.Clicked ~> (p) => { print($"clicked {p}"); };
c.Fire();clicked 3Notes
- impl satisfaction propagates along the inheritance chain: protocols implemented by a base class are satisfied automatically by derived classes;
- derived classes can explicitly re-impl to override the implementation provided by the base class;
- protocol member signatures may carry
pure/asyncmodifiers to express contract intent; - protocol member default visibility matches that of type members; the implementer’s same-name members are written
public; - the impl side’s fail set, static position, and signature shape are all part of the satisfaction key: narrow but never widen, and any mismatch reports.
