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

Short programs with final protocol methods

Hypothetical Plenty. These complete sketches illustrate the agreed core rules; the current compiler does not accept them. The research harness runs handwritten current-language equivalents of their algorithms, not the new dispatch or conformance rules.

The examples keep hooks public to isolate implementation selection. The private-hook variation at the end adds the separate cross-module access question.

Saving a document: required hook, final operation

The protocol author supplies validation once:

enum SaveError:
    Empty

protocol DocumentStorage:
    pub def write(self: &mut DocumentStorage, document: &str) -> Result[(), SaveError]:
        pass

    @final
    pub def save(self: &mut DocumentStorage, document: &str) -> Result[(), SaveError]:
        if document.byte_len() == 0:
            return Err(SaveError.Empty)
        self.write(document)?
        Ok(())

class Memory:
    writes: i64

    pub def write(self: &mut Memory, document: &str) -> Result[(), SaveError]:
        self.writes += 1
        Ok(())

def persist[S: DocumentStorage](storage: &mut S, document: &str) -> Result[(), SaveError]:
    storage.save(document)

def main() -> Result[(), IoError]:
    mut memory = Memory(0)
    match persist(&mut memory, ""):
        case Ok(_):
            print("unexpected")?
        case Err(_):
            print("empty rejected")?
    match persist(&mut memory, "hello"):
        case Ok(_):
            print("saved")?
        case Err(_):
            print("unexpected")?
    print(memory.writes)?
    Ok(())

Expected output: empty rejected, saved, 1. The class author writes only storage behavior. The application uses persist, which explicitly names the contract and can call its final save.

If Memory adds its own save, the call to persist is rejected, even if that inherent method validates correctly. It is the attempted structural match, not the class declaration, that violates this candidate’s contract.

DocumentStorage.save(&mut memory, "hello") would be the qualified spelling. memory.save("hello") does not gain a new inherent method. Calling memory.write("hello") remains possible in this public-hook example.

Scoring: final operation calls a replaceable default

protocol Scored:
    pub def base_score(self) -> i64:
        pass

    pub def score(self) -> i64:
        self.base_score()

    @final
    pub def eligible(self) -> bool:
        self.score() >= 10

class Basic:
    points: i64

    pub def base_score(self) -> i64:
        self.points

class Bonus:
    points: i64

    pub def base_score(self) -> i64:
        self.points

    pub def score(self) -> i64:
        self.points + 5

def qualifies[S: Scored](item: &S) -> bool:
    item.eligible()

def main() -> Result[(), IoError]:
    basic = Basic(7)
    bonus = Bonus(7)
    print(qualifies(&basic))?
    print(qualifies(&bonus))?
    Ok(())

Expected output: False, True. The final threshold algorithm calls the selected score: the protocol fallback for Basic, the override for Bonus. It does not freeze every method reachable from its body.

From the class author’s perspective, Bonus.score is still an ordinary method. It may be called directly. Basic has no inherent score; uniform application code uses a bound or Scored.score(&basic). This asymmetry is the main ergonomic cost of avoiding automatic injection, and should be evaluated with users.

Same object, two protocols, different provided behavior

protocol Celsius:
    pub def celsius(self) -> i64:
        pass

    @final
    pub def display(self) -> i64:
        self.celsius()

protocol Fahrenheit:
    pub def celsius(self) -> i64:
        pass

    @final
    pub def display(self) -> i64:
        self.celsius() * 9 // 5 + 32

class Sensor:
    reading: i64

    pub def celsius(self) -> i64:
        self.reading

def show[T: Celsius, U: Fahrenheit](left: &T, right: &U) -> Result[(), IoError]:
    alias = left
    print(alias.display())?
    print(right.display())?
    Ok(())

def main() -> Result[(), IoError]:
    sensor = Sensor(20)
    show(&sensor, &sensor)?
    Ok(())

Expected output: 20, 68. Both generic arguments are concretely Sensor, but their bounds select different implementations. The alias must retain the left parameter’s protocol context. Neither default is installed globally on Sensor, and neither protocol overrides the other.

This is why simply registering a synthetic Sensor.display when first encountering a bound would be incorrect. It would make later calls depend on which protocol was instantiated first.

Making the storage hook private across modules

This variation is a syntax sketch, not another complete runnable program:

# library.plenty
pub protocol DocumentStorage:
    def write(self: &mut DocumentStorage, document: &str) -> Result[(), IoError]:
        pass

    @final
    pub def save(self: &mut DocumentStorage, document: &str) -> Result[(), IoError]:
        # Validate, call self.write(document), record success.
        ...

# backend.plenty
from library import DocumentStorage

pub class FileStorage:
    @protocol.DocumentStorage  # Agreed access-grant syntax; not implemented.
    def write(self: &mut FileStorage, document: &str) -> Result[(), IoError]:
        # Perform the actual file operation.
        ...

# application.plenty
def persist[S: DocumentStorage](storage: &mut S, document: &str) -> Result[(), IoError]:
    storage.save(document)   # Allowed.
    # storage.write(document) would be rejected.

Here the class author explicitly permits the real library’s protocol to call write; the application cannot create an equivalent protocol to acquire that permission. The compiler checks the annotation where FileStorage is defined: DocumentStorage must declare a non-public write with the same signature. No separate implementation block is required. Ordinary module privacy still lets code inside backend.plenty call its own hook. All methods defined in DocumentStorage may use the grant, but standalone helpers in library.plenty do not inherit it. Class-only privacy was not selected.

The imports run one way: backend.plenty imports library.plenty, and the application imports both. library.plenty never names FileStorage, because protocol bodies are checked against the protocol’s own contract.