Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Structural protocols

protocol Readable[T]:
    def read(self) -> T:
        pass

def read[T, R: Readable[T]](source: &R) -> T:
    source.read()

A protocol declares required ordinary method signatures with pass bodies. A bound accepts a class or enum whose accessible inherent methods match every requirement, even requirements unused by the generic body. Receiver borrowing, arity, parameter types, and return type must match exactly; parameter names may differ. There is no inheritance, registration, or import-driven method activation.

Bare self means a shared receiver. An owned requirement uses self: Readable[T] or self: Readable; its implementation must also take its receiver by value. Owned, shared and mutable receivers are distinct requirements. Mutable requirements use self: &mut Readable[T] or self: &mut Readable. Within requirements the protocol’s own name denotes the implementing type, including in returned references.

Protocol arguments substitute into all requirements. Once an input determines the implementing type, its accessible concrete signatures supply inference evidence for other parameters, including nested types and returns. Evidence must agree with ordinary arguments and callable constraints. A method type that no arguments can fit, such as -> () for -> Result[(), E], does not satisfy the protocol. For a concrete implementation, a protocol parameter absent from every requirement still needs explicit arguments. A Box[P[Args...]] supplies its fixed arguments directly. Inference follows known signatures without searching for a type or implementation.

Visibility is checked from the bound’s defining module before method signatures supply evidence. Public contracts cannot expose private protocols, requirement types, or concrete arguments. Importing a protocol adds no methods to a type.

A requirement may declare its own type parameters, each unconstrained or constrained by one protocol. An implementation then declares a generic method with as many parameters, the same constraints, and the same signature once its parameter names are replaced by the requirement’s; the names may differ. Calls through the bound instantiate the implementation for each argument type:

protocol Sized:
    def size(self) -> i64:
        pass

protocol Sink:
    def put[S: Sized](self: &mut Sink, item: &S) -> Result[i64, OverflowError[i64]]:
        pass

class Crate:
    width: i64

    def size(self) -> i64:
        self.width

enum Parcel:
    Letter
    Packet(i64)

    def size(self) -> i64:
        match self:
            case Parcel.Letter:
                1
            case Parcel.Packet(width):
                *width

class Total:
    sum: i64

    def put[T: Sized](self: &mut Total, item: &T) -> Result[i64, OverflowError[i64]]:
        self.sum += item.size()
        Ok(self.sum)

def feed[K: Sink, S: Sized](sink: &mut K, item: &S) -> Result[i64, OverflowError[i64]]:
    sink.put(item)

def main() -> Result[(), IoError | OverflowError[i64]]:
    mut total = Total(0)
    feed(&mut total, &Crate(3))?
    feed(&mut total, &Parcel.Letter)?
    print(feed(&mut total, &Parcel.Packet(4))?)?
    Ok(())
8

Crate and Parcel both satisfy Sized, so feed accepts either. Use an enum to model a sum type, a value that is one of several variants, and a class to model a product type, a record that holds all of its fields. Either can meet the same protocol.

A generic method cannot satisfy a requirement without type parameters, and a non-generic method cannot satisfy one with them.

Protocols constrain class and enum arguments, the built-in bytes, and an explicit protocol box exposing the identical protocol. A protocol alone is not a runtime value type. bytes has one method, len(self) -> i64; other built-in types have none to offer. Protocol parameters cannot have their own bounds; protocol arguments exclude unit, references, generators, and closure environments. Fields, inheritance, multiple bounds, defaults, associated types, and lifecycle requirements are unsupported.

Generic bodies are checked per concrete instance. Requirements express the minimum interface rather than a separately checked abstract body. IntType, FloatType, CopyType, and KeyType remain separate built-in constraints.

std.fmt.Formattable additionally accepts an anonymous sum when every normalized member satisfies its contract. The compiler dispatches to the active member through a shared borrow without allocation. This is specific to that standard-library protocol: a user-defined protocol with the same signature does not gain sum forwarding.

Design history

The original protocol design records the shipped foundation. Protocol method bodies propose defaults, final methods, and explicit private-hook grants; those extensions are not implemented. For an existing-language solution, see library-owned operations. The boxed protocol design records the research and rationale for implemented explicit erased owners.