Skip to main content

Traits

Traits describe the abilities of a type. For example, they can be used to specify that a type can be compared for order using operators such as < and >= via the Ordering trait, or that a value can be formatted as a string using the Debug trait.

Declaring a trait

info

Trait declarations are only allowed at the module level.

A trait is declared using the trait keyword. The body can declare function types that an instance of the trait must declare, and optionally, it can declare fallback functions.

/// Format a value in a fancy way.
trait Fancy a
// This must be defined by the instance.
has to_boring_string (value: a) -> String

// This may be defined by the instance.
// If not defined,
// the instance will use the definition provided here as the fallback.
has fancify (value: a) -> String =
"✨ " + to_boring_string value + " ✨"

The functions that are defined for the trait exist in the module-level scope.

Implementing a trait

An instance of the Fancy trait has a required function (to_boring_string) and a default function (fancify); only the required functions must be implemented.

import std/fmt

instance Fancy Int
has to_boring_string (value: Int) -> String = fmt.display value

constant example: String = fancify 123 // "✨ 123 ✨"

However, the default functions can be overridden if needed.

instance Fancy Int
has to_boring_string (value: Int) -> String = fmt.display value
has fancify (value: Int) -> String =
"🍄 " + to_boring_string value + " 🍄"

constant example: String = fancify 123 // "🍄 123 🍄"
info

A trait cannot be instantiated twice for the same type, which would be ambiguous. A trait can only be instantiated if either the trait is declared in your module or the type is declared in you module. This allows the compiler to detect overlapping trait instances only by checking in the current module!

Trait bounds

A trait can require that one or more other traits are instantiated for the type.

For example, our above Fancy trait could be written as:

import std/fmt

instance Fancy a where fmt.Display a
has fancify (value: a) -> String = "✨ " + fmt.display value + " ✨"

Now an instance of Fancy can be provided for any type which has the Display trait, such as Int.

instance Fancy Int

constant example: String = fancify 123 // "✨ 123 ✨"

A more generalized version can be written that applies to any type that has the Display trait. This means that unless a different implementation is required, all types that are Display can be used with fancify.

instance Fancy a where fmt.Display a

constant example: String = fancify(123.456) // "✨ 123.456 ✨"

Multiple trait bounds on a type are expressed as a , separated list. For example, a trait bound on a function:

function sum_stringify_repeat (first: a) (second: a) -> String
where fmt.Display a, ops.Add a =
fmt.display (first + second)

Trait bounds for multiple variables are separated by , in the where clause:

function default_pair () -> (a, b)
where Default a, Default b =
(default (), default ())

constant example: (Int, String) = default_pair () // (0, "")