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 xMultiple 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 ySupertraits
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:
| Trait | What it generates |
|---|---|
Show | String representation based on constructors and fields |
Debug | Detailed debug output with type names |
Eq | Structural equality |
Ord | Structural ordering (by field/variant order) |
Enum | Conversion 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.