Skip to main content

Traits

Traits describe what values of different types can do. For example, something as mundane as the + operator is described by the Add type as an implicit argument. Any type a that has an Add[a] implementation can be used with the + operator. Thus, a trait is simply a special name for an implicit argument that describes an ability of another value.

Traits are not a language provided feature, as in there is no trait keyword. Traits are simply the name that is given to the concept of expressing abilities through implicits.

Using traits

The + opertor can be implemented for any type via the Add trait.

import std/ops for Add

type Inches[Float]
provide Add(add: fn(left, right) { Inches(left.0 + right.0) })

// The + uses Add definition above for inches.
assert_eq(Inches(3.0) + Inches(4.0), Inches(7.0))

This works because + desugars to a function call equivalent to the following add function.

// Similar to std/ops::add.
def add(x: a, y: a)(using adder: Add[a]) -> a {
adder.add(x, y)
}

Since an implicit may itself use implicits, it is possible for a traits to put constraints on the values it expects.

type Pair[a, b](a, b)

provide (using Default[a], Default[b]) -> Default[Pair[a, b]] {
Default(default: fn() Pair(default(), default()))
}

// The default_pair is automatically inserted into the default()
// function call, and the default_pair will pull in Default
// traits for Int and String to compute their defaults.
let pair: Pair[Int, String] = default()
assert_eq(pair, Pair(0, ""))

Creating new traits

A trait is just a value that provides proof that another value can be used in such a way. For example, instances of Add[a] trait are values that contain a function with some definition of addition for a type a.

type Named[a](name: fn(a) -> String)

def name(x: a)(using named: Named[a]) -> String {
named.name(x)
}

In the following example, a custom Named type describes the ability for something to have a name that can be accessed in a generic way via the name function.

type Person(name: String, age: Int)
provide Named[Person] = Named(name: fn(person) { person.name })

type File(path: String, read_only: Bool)
provide Named[File] = Named(name: fn(file) { file.name })

assert_eq(name(Person(name: "Izumi", age: 35)), "Izumi")
assert_eq(name(File(path: "/etc/file.txt", read_only: True)), "/etc/file.txt")