Protocols
A protocol is Verum's interface mechanism — a set of method and associated-type signatures that a type can implement. Protocols are the bridge between the static polymorphism you ask for and the dispatch the compiler arranges.
Defining a protocol
type Display is protocol {
fn fmt(&self, f: &mut Formatter) -> Result<(), FormatError>;
};
type Debug is protocol {
fn fmt_debug(&self, f: &mut Formatter) -> Result<(), FormatError>;
};
type Iterator is protocol {
type Item;
fn next(&mut self) -> Maybe<Self.Item>;
};
type P is protocol { ... }declares the protocol.- Method signatures use
fn name(...) -> ReturnTypewith no body. - Associated types (
type Item;) let implementations supply type-level parameters. Selfrefers to the implementing type;Self.Itemto its associated type.
Implementing
implement Display for User {
fn fmt(&self, f: &mut Formatter) -> Result<(), FormatError> {
f.write_str(&f"User({self.id})")
}
}
implement<T> Iterator for Range<T: Numeric + Ord> {
type Item = T;
fn next(&mut self) -> Maybe<T> {
if self.current < self.end {
let v = self.current;
self.current = self.current + T.one();
Maybe.Some(v)
} else {
Maybe.None
}
}
}
Protocol inheritance
Protocols can extend others:
type Clone is protocol {
fn clone(&self) -> Self;
};
type Copy is protocol extends Clone {
// Marker: types are trivially copyable. No new methods.
};
Default methods
From the real core/protocols.vr definition:
type Eq is protocol {
fn eq(&self, other: &Self) -> Bool;
fn ne(&self, other: &Self) -> Bool { !self.eq(other) }
};
type Ord is protocol where Self: Eq {
fn cmp(&self, other: &Self) -> Ordering;
// Default methods — overridable in implementations.
fn lt(&self, other: &Self) -> Bool { self.cmp(other) is Less }
fn le(&self, other: &Self) -> Bool { !(self.cmp(other) is Greater) }
fn gt(&self, other: &Self) -> Bool { self.cmp(other) is Greater }
fn ge(&self, other: &Self) -> Bool { !(self.cmp(other) is Less) }
fn max(self, other: Self) -> Self { if self.ge(&other) { self } else { other } }
fn min(self, other: Self) -> Self { if self.le(&other) { self } else { other } }
fn clamp(self, min: Self, max: Self) -> Self { self.max(min).min(max) }
};
Specialisation
A generic implementation can have a more specific override:
implement<T: Clone> List<T> {
fn copy(&self) -> List<T> { ... } // generic
}
@specialize
implement List<UInt8> {
fn copy(&self) -> List<UInt8> {
// memcpy fast path
...
}
}
The compiler checks specialisation coherence: the specialised instance must satisfy the same contracts as the generic one (a metatheorem discharged at compile time).
Generic associated types (GATs)
type LendingIterator is protocol {
type Item<'a>;
fn next<'a>(&'a mut self) -> Maybe<Self.Item<'a>>;
};
GATs let associated types depend on the lifetime/shape of each call, not on the implementation as a whole.
Static vs dynamic dispatch
impl P (static)
fn draw_each(shapes: &[impl Drawable]) { ... }
- Monomorphised per concrete type.
- Zero overhead.
- Different monomorphisations are different compiled functions.
dyn P (dynamic)
fn draw_each(shapes: &[dyn Drawable]) { ... }
- Single compiled function, virtual dispatch.
- Allows a heterogeneous collection.
- One pointer + one vtable pointer per value.
Rule of thumb: impl unless you need runtime polymorphism.
Negative bounds
fn send_to<T: Send + !Sync>(x: T) { ... }
Reads "T must be Send but must not be Sync." Useful for
single-owner-per-thread APIs.
Context protocols
A protocol can be declared as a context:
context protocol Logger {
fn info(&self, msg: Text);
fn error(&self, msg: Text);
};
Context protocols can be both required by functions (using [Logger]) and implemented by types that serve as logger backends.
See Context system.
Coherence (orphan rule)
An implementation implement P for T is valid only if either:
- The cog defining the protocol
Palso definesT, or - The cog defining
Talso defines the implementation.
Two cogs cannot both provide an implement P for T without
coordination. This rule keeps protocols unambiguous across an ecosystem.
Marker protocols
Protocols with no methods that communicate a fact to the type
checker. From core/protocols.vr:
type Send is protocol { }; // safe to move across threads
type Sync is protocol { }; // safe to share across threads
type Copy is protocol where Self: Clone { }; // trivial copy semantics
type Sized is protocol { }; // statically sized
type Unpin is protocol { }; // can be moved while pinned
The compiler auto-derives these where it can prove them; you can opt
out with !Send, !Sync, etc.