SagaSaga
Guide

Traits

Traits give you type-driven dispatch. You define a set of operations once, then implement them differently for each type. When you call show x, the compiler looks at the type of x and picks the right implementation automatically.

Defining a Trait

A trait declares one or more function signatures that types can implement:

trait Show a {
  fun show : a -> String
}

trait Eq a {
  fun eq : a -> a -> Bool
}

The type variable a stands for the implementing type. Any type that implements Show must provide a show function that converts it to a String.

A method signature is pure unless you say otherwise. A method may also declare a needs row, which lets impls leave effects unhandled for the caller to provide — see Effectful Trait Methods.

Implementing a Trait

Use impl to provide the implementation for a specific type:

record User {
  name: String,
  age: Int,
}

impl Show for User {
  show user = $"{user.name} (age {user.age})"
}

impl Eq for User {
  eq a b = a.name == b.name && a.age == b.age
}

Now show some_user and eq user_a user_b work on User values.

Default Methods

A trait can supply a default body for any of its methods. Write the signature as usual, then on a following line repeat the method name with an implementation:

trait Greeter a {
  fun name : a -> String
  fun greet : a -> String
  greet x = "Hello, " <> name x <> "!"
}

Now an impl only has to provide the methods that don't have defaults. The defaulted ones come along for free, but can still be overridden when a type wants different behavior:

record User { name: String }
record Robot { model: String }

impl Greeter for User {
  name u = u.name
  # greet is inherited from the default
}

impl Greeter for Robot {
  name r = r.model
  greet r = "BEEP BOOP. UNIT " <> r.model <> " ONLINE."
}

A default body can call the trait's other methods — greet above calls name, which dispatches to whichever implementation the type provides. It can also reference anything in scope where the trait is defined: imports and top-level functions.

Defaults are useful for convenience methods that can be expressed in terms of a smaller required core. An implementer writes just the essentials; the trait fills in the rest.

where Clauses

Functions can require that their type parameters implement certain traits using where:

fun to_string : a -> String where {a: Show}
to_string x = show x

Multiple bounds on the same type variable use +:

fun print_if_equal : a -> a -> String where {a: Show + Eq}
print_if_equal x y =
  if eq x y then show x
  else "not equal"

Bounds on multiple type variables are comma-separated:

fun convert : a -> b -> String where {a: Show, b: Show + Eq}
convert x y = show x <> " -> " <> show y

Supertraits

A trait can require that implementing types also implement another trait:

trait Ord a where {a: Eq} {
  fun compare : a -> a -> Ordering
}

Any type that implements Ord must also implement Eq. This means functions with an Ord bound can use both compare and eq without listing both explicitly.

Conditional Implementations

An implementation can have its own where clause, making it available only when certain constraints are met:

impl Show for List a where {a: Show} {
  show xs = "[" <> String.join ", " (List.map show xs) <> "]"
}

This says: List a implements Show, but only when a itself implements Show. So show [1, 2, 3] works (because Int implements Show), but show on a list of opaque values that don't implement Show is a type error.

deriving

For common traits, the compiler can generate implementations automatically:

record Point {
  x: Int,
  y: Int,
} deriving (Show, Eq, Ord)

type Color = Red | Green | Blue
  deriving (Show, Eq, Enum)

The available derives are:

TraitWhat it generates
ShowString representation based on constructors and fields
DebugDetailed debug output with type names
EqStructural equality
OrdStructural ordering (by field/variant order)
EnumConversion to/from integers (bare ADT variants only)

deriving saves boilerplate for types where the obvious implementation is the right one. For anything custom, write an explicit impl.

Looking Ahead: Effectful traits

Traits select an implementation based on the type. When you write show x, the compiler resolves which show to call based on what x is. This is fixed at compile time.

The next section introduces effects, which look similar but work differently: the caller chooses the implementation at runtime. A useful rule of thumb: if the behavior depends on what the data is, use a trait. If it depends on where the code is running, use an effect.

The two are not mutually exclusive. A trait still selects its impl by type, but that impl can itself perform effects whose handler the caller chooses at runtime. Effectful Trait Methods covers how the two compose.