15 — Generics, traits, and derive

Previous: Iteration

日本語版: 15_generics.vibe.md

Writing the same function once for every type is the problem generics solve. A type parameter goes in [T], and a constraint on it goes in [T: Eq].

One definition, many types

struct Box[T] {
  v: T
}

fn identity[T](x: T) -> T {
  x
}

fn main with Console {
  let b = Box::{
    v: 41
  }
  println("id = \{identity(b.v + 1)}")
}
id = 42

identity works for any T. Box[T] holds any T, and the literal above did not need to say which — it was inferred from v: 41. Pin it explicitly with Box[Int]::{ ... } when inference has nothing to go on.

A top-level fn annotates its parameters and return type in full, generic or not; inference fills in the call site, not the declaration. main is the one exception — it alone may leave the return type off, which is why fn main with Console parses and fn shout(msg: String) with Console does not.

derive gives you the usual operations

Most types want equality, ordering, and a printable form. Ask for them:

enum Color {
  Red; Green; Blue
} derive (Eq, Show)

fn main with Console {
  println("eq = \{Color::Red == Color::Red}")
  println("neq = \{Color::Red == Color::Blue}")
  println("show = \{Color::Green}")
}
eq = true
neq = false
show = Green

derive (Eq, Ord, Show, Hash, Default) are the five. Eq makes == structural for the type, Ord gives T::compare returning -1 / 0 / 1, and Show gives T::to_string, which is also what string interpolation calls.

Traits you write

A trait with methods is a contract, and a bound on it means "the caller passes an implementation":

trait Measured {
  measure(Self) -> Int
}

impl [T] Measured for Array[T] {
  measure(self) -> Int {
    Array::length(self)
  }
}

fn size_of[T: Measured](x: T) -> Int {
  T::measure(x)
}

fn main with Console {
  let xs = [
    1,
    2,
    3
  ]
  println("size_of = \{size_of(xs)}")
}
size_of = 3

size_of has no idea what T is. T::measure reaches the implementation through a witness the caller supplies, which is how the bound turns into a working call.

That witness is also the limit. A bound like [T: Eq] needs one, and a container whose element type has been erased has none — so passing an Array[Int] to fn eq2[T: Eq](a: T, b: T) is rejected:

no impl `Eq` for `Array[Int]`

You are told, at compile time. Equality has the rest of that story.

Two you do not implement

Send is judged structurally by the compiler — primitives, tuples, Option of Send parts, immutable structs and enums — so impl Send for X is an error rather than a way to promise something. See Concurrency.

Default is a builtin; derive(Default) registers the implementation. Generic code that calls T::default() needs import @vibe/core { trait Default }.

Next: Equality.