Skip to content

Protocols

Protocols define a set of functions that a type must implement. They enable generic programming through bounded type parameters, with all dispatch resolved at compile time (no dynamic dispatch).

A protocol declares required method signatures using Self as a placeholder for the implementing type:

protocol Serializable {
fn serialize(a: Self) -> String
fn deserialize(raw: String) -> Result[Self, String]
}

Protocols can include effect fn methods:

protocol Storage {
effect fn save(a: Self, path: String) -> Result[Unit, String]
effect fn load(path: String) -> Result[Self, String]
}

A type declares protocol satisfaction with : ProtocolName in its type declaration. Methods are defined as convention functions with the type name prefix:

type Config: Serializable = {
key: String,
value: String,
}
fn Config.serialize(c: Config) -> String =
c.key + "=" + c.value
fn Config.deserialize(raw: String) -> Result[Config, String] = {
let parts = string.split(raw, "=")
match (list.get(parts, 0), list.get(parts, 1)) {
(some(k), some(v)) => ok({ key: k, value: v }),
_ => err("invalid format"),
}
}

Methods use UFCS, so both call styles work:

let s = Config.serialize(config)
let s = config.serialize() // equivalent via UFCS

The checker validates the convention method against the protocol’s declared signature (arity, parameter types, return type — Self substituted for the declaring type), and pins a diagnostic to the method on mismatch. There is no impl Protocol for Type { ... } block: convention methods are Almide’s only way to attach a method to a type, protocol-bound or not — they stay flat, top-level functions with no extra nesting or indentation.

Protocols constrain generic type parameters:

fn show[T: Serializable](item: T) -> String =
item.serialize()
fn save_all[T: Serializable](items: List[T], path: String) -> String = {
let lines = list.map(items, (item) => item.serialize())
string.join(lines, "\n")
}

Only types that satisfy the protocol can be used as arguments:

show(config) // OK: Config implements Serializable
show(42) // Compile error: Int does not implement Serializable

Several protocols are built into the language:

ProtocolDescriptionDeriving
EqEquality (==, !=)Structural, for the operators
HashHash computation — needed for Map keysStructural, except types containing Float
ReprString representation, used by interpolationBuilt-in convention
Ordcmp(a, b) -> IntBuilt-in convention
Codec / Encode / DecodeSerializationBuilt-in convention
NumericArithmetic — satisfied by the built-in numeric typesBuilt-in convention

Ord provides cmp, not the comparison operators: < / <= / > / >= stay limited to Int, Float, String and Bool even on a type declared : Ord.

Eq and Hash work structurally for the operators== on a record needs no declaration. Satisfying them as a generic bound ([T: Eq]) is a different thing and does require : Eq on the type declaration:

type Color = Red | Green | Blue
let same = Red == Red // true, just works
let diff = Red != Blue // true, just works

The first parameter can be named/typed explicitly (a: Point) or written as bare self, sugar for self: Self that resolves to the declaring type on a convention method the same way it does inside a protocol { ... } declaration itself.

  • No dynamic dispatch — all protocol-bounded generics are monomorphized at compile time
  • No implicit instance resolution — types explicitly declare protocol satisfaction
  • No arbitrary operator overloading — only == / != and string interpolation dispatch to a type’s eq / repr convention methods; every other operator has fixed semantics
  • No inheritance — use composition and protocols instead
  • No impl block — convention methods (fn Type.method(...)) are the only way to attach a method to a type; flat, top-level, no extra nesting