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

Protocol method bodies: defaults, final methods, and private hooks

Status: core design agreed; not implemented. This is the implemented protocol methods proposal: “implemented” means a method with a body, not a feature that has shipped. The compiler still requires protocol methods to have pass bodies; current protocols remain authoritative. Future prioritization belongs to B29.

Start with the agreed rules and short programs. The feasibility comparison below and implementation evidence record the investigation against d39e6ec on 2026-10-20. Alternatives such as automatic method injection were considered, not adopted.

Assessment: shared bodies with explicitly selected protocol calls are a small-to-medium frontend feature. Convenient calls through a protocol bound are a medium feature. Automatically adding methods to every structurally matching object, especially with private hooks, is a substantially larger language-design change. None inherently requires dynamic dispatch, an object wrapper, a new runtime representation, or a different borrow checker.

Those estimates concern sharing and lookup alone. The subsequently agreed definition-time contract checking adds substantial frontend work; the complete design with that checking and private-hook grants is not a small syntactic change.

This investigates sharing implementations. The earlier library-owned operations assessment addresses preventing callers from bypassing an algorithm; it remains relevant, because ordinary overridable defaults do not provide that guarantee.

The follow-up final-method and visibility design records the agreed core semantics: defaults honor overrides consistently, final methods reject same-named inherent methods at the structural boundary, and private hooks use member-specific grants while preserving module privacy. Three short programs illustrate the rules. The final-name check is a modest addition once provided-method selection exists; hidden external hooks require additional access-control machinery. Some specification work remains, and no compiler implementation is approved here.

Protocol bodies must also be checked against their declared contract at the definition, rather than accepted because a concrete class has extra members. This agreed requirement increases the scope beyond a generic-function lowering: the existing per-instance checker is not sufficient for a complete implementation.

One example, three distinct choices

Hypothetical syntax, not executable Plenty:

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

    def save(self: &mut DocumentStorage, document: &str) -> Result[(), IoError]:
        validate(document)?
        self.write(document)?
        record_success()
        Ok(())

FileStorage and DatabaseStorage would supply write. The application wants to call the shared save. Treating an AST body consisting solely of pass as a requirement is straightforward; an intentional no-op implementation would need an explicit unit-returning body. Comments do not change the AST body.

There are three independent questions:

  1. Sharing: where may the application call the supplied save?
  2. Replacement: may a concrete type supply its own save instead?
  3. Access: may the application call write directly?

Implementing the first does not imply either answer to the other two. In particular, the example above deliberately uses ordinary accessible hooks. Cross-module private hooks need a separate access design.

Options and relative cost

All new spellings below are illustrative.

DesignExample callScope and assessment
Existing bounded functionsave(&mut files, document)Works today. Sharing has no language implementation cost.
Protocol-owned operationDocumentStorage.save(&mut files, document)Small-to-medium. Explicitly selects the protocol body, lowers to a generic function, and adds no inherent method.
Default selected through a boundstorage.save(document) inside S: DocumentStorage codeMedium. Needs protocol-aware method resolution, missing-default handling, override rules, and receiver provenance through specialization.
Explicit adoption on a typeAn illustrative use DocumentStorage declaration in FileStorageMedium-to-large. A local place to opt into methods and diagnose conflicts, but introduces a new relationship beyond structural matching.
Automatic structural extensionfiles.save(document) wherever a matching protocol is visibleLarge relative to the above. Needs candidate discovery, overlap rules, inference limits, and a decision about whether imports change available methods.
Final provided methodReject a matching object with a same-named inherent methodModest addition to provided-method selection: check the final-name restriction at conformance and consistently select the protocol body.
Private hooks across modulesAccess controlled separately from structural matchingSeparate substantial feature. Needs explicit authority, cross-module rules, and guarantees that survive every call path.

These are engineering judgments from source inspection and experiments, not calendar estimates or measurements of a completed implementation. A narrow compiler spike would be small; a supported feature includes diagnostics, modules, generic combinations, tooling, and regression coverage.

The protocol-owned operation is closest to ordinary generic-function sugar:

# Conceptual lowering of DocumentStorage.save:
def __document_storage_save[S: DocumentStorage](storage: &mut S,
                                               document: &str) -> Result[(), IoError]:
    validate(document)?
    storage.write(document)?
    record_success()
    Ok(())

Here DocumentStorage checks only its required hooks; implemented operations do not require same-named inherent methods. The generated name is an internal detail. Calls must preserve the protocol definition’s module visibility.

This design has deliberately fixed selection: DocumentStorage.save(...) calls that body even when the object has an inherent save. It is a provided operation, not a fallback override. It avoids global method searching and preserves the existing rule that importing a protocol does not change an object’s inherent methods.

It does not, by itself, give the application the convenient files.save(...) spelling. A bounded-call design could add that later, but must deliberately choose whether it denotes the fixed operation or an overridable default.

The important design traps

Defaults are not a validation boundary. If a concrete save replaces the default, it can omit validation. Explicitly selecting a fixed protocol body protects that call’s algorithm; it does not prohibit calling a public write or a separate concrete save. An owning wrapper remains the current-language way to expose only the intended operation.

Structural compatibility is not implementation selection. Two protocols may both require write but provide different save algorithms. A concrete object can match both without selecting either. A qualified call is unambiguous. For bound-based calls, the bound should identify the protocol; searching every matching protocol makes imports and unrelated declarations affect lookup.

Inherent overrides need exact rules. If defaults are replaceable, decide whether a same-named but incompatible method is an error or is ignored. Prefer an error over silently choosing the default. Also specify calls between defaults: does save_twice invoke an override of save, or its original protocol body? Do not let these answers depend accidentally on specialization order.

Bodies must be checked against the contract. Existing generic bodies are checked per concrete instance and may use methods not listed in their bound. That behavior is not acceptable for provided protocol bodies in the agreed design. Abstract checking is required, including through aliases, helpers and closures; a syntactic scan for self.foo is insufficient. This is additional frontend work beyond the demonstrated generic-function lowering.

Inference needs real evidence. Required concrete signatures can determine protocol parameters. A supplied body cannot become circular evidence for its own signature. If a protocol parameter occurs only in a provided operation, require explicit arguments or other independent evidence. Avoid searching other protocol defaults to manufacture conformance.

Privacy is lexical today. A library-defined shared body cannot access an external class’s private method. Automatically granting that access because a public protocol happens to match would cross an existing encapsulation boundary without the class author’s consent. If pursued, access grants should identify the protocol/member explicitly and be distinct from declaring conformance. “Private to the class’s module” also differs from “callable only by protocol bodies,” which the original request might require.

Precedents

Python’s structural protocols provide a particularly relevant warning: implicit structural matching does not supply default implementations; explicit subclassing does. Its existing inheritance mechanism selects the shared code. Python typing specification.

Rust permits trait bodies as defaults that implementations may replace. Individual trait items do not have independent visibility qualifiers. Rust Reference.

Swift protocol extensions supply defaults; a conforming type’s implementation of a requirement takes precedence. Constrained extension overlap also requires selection rules. This is useful precedent for both the feature and its semantic cost, not a lookup model Plenty can copy unchanged. Swift language guide source.

Suggested direction

The selected direction uses overridable defaults and @final bodies, selected through a bound or protocol qualification. Both forms honor overrides for non-final methods, including calls inside other protocol methods. Objects do not acquire inherent methods automatically. Matching requires signature and visibility equality, and cross-module private hooks require a @protocol.Protocol annotation on the hook method; module privacy otherwise stays unchanged. Final methods are annotated @final.

The earlier qualified-call alternative that always invokes the original body is not selected. The design page separates agreed rules from the remaining specification work. Definition-time checking against the declared protocol contract is mandatory.

The implementation map and probes show what already works and where the remaining compiler work lies. No new feature is scheduled by this sketch.