Skip to content

Trait Bounds

A bare type parameter T has no methods — the compiler does not know what T can do. A trait bound tells it: T is some type that implements a specific trait. Once the bound is in place, the body can use all that trait's operators and methods on values of type T.

Bounds are written inline (fn f<T: Ord>(x: T)) or in a where clause — equivalent forms; where reads better when several bounds apply at once. The compiler checks bounds at compile time and erases them at runtime, so they add no overhead:

max<T: Ord> uses >; sum<T: Add> uses +; announce uses where T: Add + Display for multiple bounds.

17-trait-bounds.zolo
Playground
// Feature: trait bounds on generics — `<T: Trait>` and `where`

// Syntax: `fn f<T: Ord>(...)` or `fn f<T>(...) where T: Ord`

// When to use: when a generic function needs to DO something with `T`

// (compare, add, print). The bound is what lets the body use `>`, `+`,

// etc. on a `T` — without it the compiler rejects the operator (TE821),

// and calling with a type that doesn't satisfy the bound is TE820.

//

// Std traits and the operators they enable:

//   Eq  -> == !=     Ord -> < > <= >=     Add -> +     Sub -> -

//   Mul -> *         Div -> /             Mod -> %     Neg -> unary -

//   Display -> print/interpolation        Hash -> Map key

// Primitives implement them out of the box (int/float: all; str: Eq, Ord,

// Add (concat), Display, Hash; bool: Eq, Display, Hash).

//

// Bounds are checked at COMPILE time and then erased — they add no runtime

// cost. See docs/errors/TE820, TE821, TE822.


// `T: Ord` lets the body use `>` on `T`.

fn max<T: Ord>(a: T, b: T) -> T {
  if a > b { a } else { b }
}

// `T: Add` lets the body use `+` on `T`.

fn sum<T: Add>(a: T, b: T) -> T {
  return a + b
}

// `where` form — equivalent to the inline bound, nicer with several params

// or several bounds. Here `T` must be both addable and printable.

fn announce<T>(label: str, a: T, b: T) -> T where T: Add + Display {
  let total = a + b
  print("{label}: {total}")
  return total
}

// Ord works for any ordered primitive.

print(max(3, 7))             // 7

print(max("apple", "kiwi"))  // kiwi  (lexicographic)

print(max(2.5, 1.5))         // 2.5


// Add works for numbers and string concatenation.

print(sum(10, 32))           // 42

print(sum("foo", "bar"))     // foobar


announce("score", 40, 2)     // score: 42


// expected:

// 7

// kiwi

// 2.5

// 42

// foobar

// score: 42

The common std bounds and the operators they unlock:

Trait Enables
Ord < > <= >=
Eq == !=
Add +
Sub -
Mul *
Div /
Display print, interpolation

Primitives implement all applicable bounds out of the box: int and float satisfy every numeric and comparison trait; str satisfies Eq, Ord, Add (concatenation), Display, and Hash.

Challenge

Write a generic function clamp<T: Ord>(x: T, lo: T, hi: T) -> T that returns lo if x < lo, hi if x > hi, and x otherwise. Test it with integers and strings.

enespt-br