Traits

Traits define behavior that types can share. If you’ve used interfaces in other languages, traits are similar — but more powerful.

Your First Trait

Let’s create a trait for things that can be displayed:

trait Displayable {
    @display (self) -> str;
}

This says: “Any type implementing Displayable must have a display method that returns a string.”

Implementing Traits

Now let’s implement this trait for a type:

type Point = { x: int, y: int }

impl Point: Displayable {
    @display (self) -> str = `({self.x}, {self.y})`;
}

// Now we can call display on any Point
let p = Point { x: 10, y: 20 };
let s = p.display();  // "(10, 20)"

Why Use Traits?

Traits enable polymorphism — writing code that works with any type that implements a behavior:

@print_all<T: Displayable> (items: [T]) -> void =
    for item in items do
        print(msg: item.display());

This function works with Points, Users, or any type that implements Displayable:

type User = { name: str, age: int }

impl User: Displayable {
    @display (self) -> str = `{self.name} (age {self.age})`;
}

// Both work!
print_all(items: [Point { x: 1, y: 2 }, Point { x: 3, y: 4 }]);
print_all(items: [User { name: "Alice", age: 30 }]);

Trait Methods: self and Self

Two important concepts in traits:

  • self — the instance the method is called on
  • Self — the implementing type itself
trait Clonable {
    @clone (self) -> Self;  // Returns the same type as the implementer
}

impl Point: Clonable {
    @clone (self) -> Self = Point { x: self.x, y: self.y };
}

let p = Point { x: 1, y: 2 };
let p2: Point = p.clone();  // Type is Point, not some generic type

Default Implementations

Traits can provide default method implementations:

trait Describable {
    @name (self) -> str;  // Required — implementers must provide

    @describe (self) -> str = `This is a {self.name()}`;  // Default — can be overridden
}

type Car = { model: str }

impl Car: Describable {
    @name (self) -> str = self.model;
    // describe uses the default implementation
}

let car = Car { model: "Tesla" };
car.describe();  // "This is a Tesla"

Implementers can override defaults if needed:

impl Point: Describable {
    @name (self) -> str = "Point";

    @describe (self) -> str = `Point at ({self.x}, {self.y})`;  // Override default
}

Associated Types

Traits can define types that implementers specify:

trait Container {
    type Item;  // Associated type — implementer decides

    @get (self, index: int) -> Option<Self.Item>;
    @len (self) -> int;
}

impl [int]: Container {
    type Item = int;  // For [int], Item is int

    @get (self, index: int) -> Option<int> =
        if index >= 0 && index < self.len() then Some(self[index]) else None;

    @len (self) -> int = len(collection: self);
}

Associated types let you write generic code that works with any container:

@first<C: Container> (container: C) -> Option<C.Item> =
    container.get(index: 0);

Trait Inheritance

Traits can require other traits:

trait Comparable: Eq {  // Comparable requires Eq
    @compare (self, other: Self) -> Ordering;
}

To implement Comparable, you must also implement Eq:

impl Point: Eq {
    @eq (self, other: Self) -> bool = self.x == other.x && self.y == other.y;
}

impl Point: Comparable {
    @compare (self, other: Self) -> Ordering = {
        let by_x = compare(left: self.x, right: other.x);
        if by_x != Equal then by_x else compare(left: self.y, right: other.y)
    }
}

Multiple Trait Bounds

Require multiple traits with +:

@sort_and_display<T: Comparable + Displayable> (items: [T]) -> void = {
    let sorted = items.sort();
    for item in sorted do print(msg: item.display())
}

Where Clauses

For complex bounds, use where:

@process<T, U> (input: T, transformer: (T) -> U) -> [U]
    where T: Clone,
          U: Displayable + Default = {
    let items = [input.clone(), input.clone(), input.clone()];
    for item in items yield transformer(item)
}

Generic Trait Implementations

Implement traits for generic types:

// Implement Displayable for any list of Displayable items
impl<T: Displayable> [T]: Displayable {
    @display (self) -> str = {
        let items = for item in self yield item.display();
        `[{items.join(sep: ", ")}]`
    }
}

// Now [Point] is Displayable
let points = [Point { x: 1, y: 2 }, Point { x: 3, y: 4 }];
points.display();  // "[(1, 2), (3, 4)]"

Deriving Traits

Many common traits can be automatically derived:

#derive(Eq, Clone, Debug, Printable)
type Point = { x: int, y: int }
TraitWhat It Provides
Eq==, != operators
HashableHash value for use as map keys
Comparable<, >, <=, >= operators
Clone.clone() method
Debug.debug() for developer output
Printable.to_str() for user output
DefaultType.default() constructor

Deriving generates sensible implementations based on your type’s fields.

Standard Traits

Ori provides these commonly-used traits:

Eq — Equality

trait Eq {
    @eq (self, other: Self) -> bool;
}

// Enables == and != operators
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
a == b;  // true
a != b;  // false

Comparable — Ordering

trait Comparable: Eq {
    @compare (self, other: Self) -> Ordering;
}

// Enables <, >, <=, >= operators and sorting
let points = [Point { x: 3, y: 0 }, Point { x: 1, y: 0 }];
points.sort();  // [Point { x: 1, y: 0 }, Point { x: 3, y: 0 }]

Clone — Copying

trait Clone {
    @clone (self) -> Self;
}

let original = [1, 2, 3];
let copy = original.clone();  // Independent copy

Debug and Printable — String Representations

trait Debug {
    @debug (self) -> str;  // Developer-facing, shows structure
}

trait Printable {
    @to_str (self) -> str;  // User-facing, shows content
}

#derive(Debug, Printable)
type User = { name: str, email: str }

let user = User { name: "Alice", email: "alice@example.com" };
user.debug();   // "User { name: \"Alice\", email: \"alice@example.com\" }"
user.to_str();  // "Alice (alice@example.com)" (if you customize to_str)

Default — Default Values

trait Default {
    @default () -> Self;
}

#derive(Default)
type Config = { timeout: int, retries: int }

let config = Config.default();  // Config { timeout: 0, retries: 0 }

Hashable — Hash Values

trait Hashable: Eq {
    @hash (self) -> int;
}

// Required for use as map keys
#derive(Eq, Hashable)
type UserId = { value: int }

let user_scores: {UserId: int} = {};

Trait Objects

For heterogeneous collections, use trait objects:

trait Animal {
    @speak (self) -> str;
}

type Dog = { name: str }
type Cat = { name: str }

impl Dog: Animal {
    @speak (self) -> str = "Woof!";
}

impl Cat: Animal {
    @speak (self) -> str = "Meow!";
}

// Trait object: any Animal
let animals: [Animal] = [
    Dog { name: "Rex" } as Animal,
    Cat { name: "Whiskers" } as Animal,
];

for animal in animals do
    print(msg: animal.speak());

Object Safety

Not all traits can be used as trait objects. A trait is object-safe if:

  • It doesn’t use Self in return types (except for self)
  • All methods have self as a parameter
  • No generic methods
// Object-safe
trait Drawable {
    @draw (self) -> void;
}

// NOT object-safe (returns Self)
trait Clonable {
    @clone (self) -> Self;
}

Type Conversions

Ori uses the as and as? operators for type conversions.

Infallible Conversions with as

let x: int = 42;
let y: float = x as float;  // 42.0

let n: int = 100;
let s: str = n as str;  // "100"

Fallible Conversions with as?

let s: str = "42";
let maybe_n: Option<int> = s as? int;  // Some(42)

let bad: str = "not a number";
let none: Option<int> = bad as? int;  // None

The As and TryAs Traits

Conversions are backed by traits:

trait As<T> {
    @as (self) -> T;
}

trait TryAs<T> {
    @try_as (self) -> Option<T>;
}

Implementing these traits enables your types to use as/as?:

type Celsius = { value: float }
type Fahrenheit = { value: float }

impl Celsius: As<Fahrenheit> {
    @as (self) -> Fahrenheit =
        Fahrenheit { value: self.value * 9.0 / 5.0 + 32.0 };
}

let c = Celsius { value: 100.0 };
let f = c as Fahrenheit;  // Fahrenheit { value: 212.0 }

The Into Trait

For converting into a target type:

trait Into<T> {
    @into (self) -> T;
}

Commonly used with error contexts:

// str implements Into<Error>
let result = fallible_op().context(msg: "operation failed");
// "operation failed" converts to Error via Into<Error>

Complete Example

// Define a trait for scoring
trait Scorable {
    @score (self) -> int;
}

// Define a trait for ranking
trait Rankable: Scorable + Comparable {}

// Player type
#derive(Clone, Debug)
type Player = {
    name: str,
    kills: int,
    deaths: int,
    assists: int,
}

impl Player: Scorable {
    @score (self) -> int = self.kills * 3 + self.assists - self.deaths;
}

impl Player: Eq {
    @eq (self, other: Self) -> bool = self.name == other.name;
}

impl Player: Comparable {
    @compare (self, other: Self) -> Ordering =
        compare(left: self.score(), right: other.score());
}

impl Player: Rankable {}

impl Player: Printable {
    @to_str (self) -> str = `{self.name}: {self.score()} points`;
}

@test_player_score tests @Scorable.score () -> void = {
    let player = Player { name: "Alice", kills: 10, deaths: 3, assists: 5 };
    assert_eq(actual: player.score(), expected: 32)
}

// Generic leaderboard for any Rankable
@leaderboard<T: Rankable + Printable + Clone> (
    items: [T],
    top_n: int,
) -> [str] = {
    let sorted = items.clone();
    sorted.sort_by(key: item -> -item.score());  // Descending

    sorted.iter()
        .take(count: top_n)
        .enumerate()
        .map(transform: (rank, item) -> `#{rank + 1} {item.to_str()}`)
        .collect()
}

@test_leaderboard tests @leaderboard () -> void = {
    let players = [
        Player { name: "Alice", kills: 10, deaths: 3, assists: 5 }
        Player { name: "Bob", kills: 5, deaths: 5, assists: 10 }
        Player { name: "Charlie", kills: 15, deaths: 10, assists: 2 }
    ];

    let top_2 = leaderboard(items: players, top_n: 2);
    assert_eq(actual: len(collection: top_2), expected: 2)
}

// Team type also implementing Scorable
type Team = { name: str, players: [Player] }

impl Team: Scorable {
    @score (self) -> int =
        self.players.iter()
            .map(transform: p -> p.score())
            .fold(initial: 0, op: (a, b) -> a + b);
}

@test_team_score tests @Scorable.score () -> void = {
    let team = Team {
        name: "Red Team"
        players: [
            Player { name: "Alice", kills: 10, deaths: 3, assists: 5 }
            Player { name: "Bob", kills: 5, deaths: 5, assists: 10 }
        ]
    };
    // Alice: 32, Bob: 20
    assert_eq(actual: team.score(), expected: 52)
}

Quick Reference

Define a Trait

trait Name {
    @method (self) -> Type;
    @with_default (self) -> Type = default_impl;
    type AssocType;
}

Trait Inheritance

trait Child: Parent { ... }

Implement a Trait

impl Type: Trait { ... }
impl<T: Bound> Container<T>: Trait { ... }

Derive Traits

#derive(Eq, Clone, Debug)
type Name = ...

Multiple Bounds

@fn<T: A + B> (x: T) -> T = ...;

Where Clause

@fn<T> (x: T) -> T where T: Clone, T: Debug = ...;

Type Conversions

value as Type;      // Infallible
value as? Type;     // Fallible (returns Option<T>)

Standard Traits

TraitPurpose
EqEquality (==, !=)
ComparableOrdering (<, >, etc.)
HashableHash value for maps
CloneDeep copy
DebugDeveloper string
PrintableUser string
DefaultDefault value

What’s Next

Now that you understand traits: