Skip to main content
Klar

Traits

Traits define shared behavior that types can implement. They are similar to interfaces or type classes in other languages.

Defining Traits

trait Describable {
    fn describe(self: Self) -> string
}
  • Self refers to the implementing type
  • Methods must be implemented by each type

Implementing Traits

Use impl Type: Trait syntax:

struct Point {
    x: i32,
    y: i32,
}

impl Point: Describable {
    fn describe(self: Point) -> string {
        return "Point({self.x}, {self.y})"
    }
}

Using Trait Methods

let p: Point = Point { x: 10, y: 20 }
let desc: string = p.describe()  // "Point(10, 20)"

Multiple Traits

A type can implement multiple traits:

trait Printable {
    fn to_string(self: Self) -> string
}

trait Comparable {
    fn compare(self: Self, other: Self) -> i32
}

impl Point: Printable {
    fn to_string(self: Point) -> string {
        return "({self.x}, {self.y})"
    }
}

impl Point: Comparable {
    fn compare(self: Point, other: Point) -> i32 {
        let diff: i32 = (self.x - other.x) + (self.y - other.y)
        return diff
    }
}

Default Method Implementations

Traits can provide default implementations:

trait Greetable {
    fn name(self: Self) -> string

    fn greet(self: Self) -> string {
        return "Hello, {self.name()}!"
    }
}

struct Person {
    full_name: string,
}

impl Person: Greetable {
    fn name(self: Person) -> string {
        return self.full_name
    }
    // greet() uses the default implementation
}

Trait Bounds

Constrain generic types to types implementing specific traits:

fn print_description#[T: Describable](item: T) {
    println(item.describe())
}

Multiple Bounds

Use + to require multiple traits:

fn process#[T: Printable + Comparable](a: T, b: T) {
    println(a.to_string())
    if a.compare(b) > 0 {
        println("a is greater")
    }
}

Trait Inheritance

Traits can inherit from other traits:

trait Base {
    fn base_method(self: Self) -> i32
}

trait Derived: Base {
    fn derived_method(self: Self) -> i32
}

When implementing Derived, you must also implement Base:

struct MyType {
    value: i32,
}

impl MyType: Derived {
    fn base_method(self: MyType) -> i32 {
        return self.value
    }

    fn derived_method(self: MyType) -> i32 {
        return self.value * 2
    }
}

Multiple Inheritance

trait A {
    fn method_a(self: Self) -> i32
}

trait B {
    fn method_b(self: Self) -> i32
}

trait C: A + B {
    fn method_c(self: Self) -> i32
}

Associated Types

Traits can have associated types:

trait Container {
    type Item

    fn get(self: Self, index: i32) -> Self.Item
    fn len(self: Self) -> i32
}

Implementing Associated Types

struct IntArray {
    data: [i32],
}

impl IntArray: Container {
    type Item = i32

    fn get(self: IntArray, index: i32) -> i32 {
        return self.data[index]
    }

    fn len(self: IntArray) -> i32 {
        return self.data.len()
    }
}

Built-in Traits

Klar provides several built-in traits:

TraitDescriptionMethods
EqEquality comparisoneq(self, other) -> bool
OrderedOrdering comparisoncmp(self, other) -> i32
CloneCreate a copyclone(self) -> Self
DropCleanup when destroyeddrop(inout self)
DefaultDefault valuedefault() -> Self
HashHash valuehash(self) -> u64
IteratorIterationnext(inout self) -> ?Self.Item

See Builtin Traits for details.

Deriving Built-in Traits

Primitive types automatically implement appropriate traits:

// i32 implements Eq, Ordered, Clone, Default, Hash
let a: i32 = 10
let b: i32 = 20
let equal: bool = a == b  // Uses Eq
let less: bool = a < b    // Uses Ordered

Example: Summable Trait

trait Summable {
    fn sum(self: Self) -> i32
}

struct Point {
    x: i32,
    y: i32,
}

impl Point: Summable {
    fn sum(self: Point) -> i32 {
        return self.x + self.y
    }
}

struct Rectangle {
    width: i32,
    height: i32,
}

impl Rectangle: Summable {
    fn sum(self: Rectangle) -> i32 {
        return self.width + self.height
    }
}

fn total#[T: Summable](items: [T]) -> i32 {
    var total: i32 = 0
    for item: T in items {
        total = total + item.sum()
    }
    return total
}

Example: Iterator Pattern

trait Iterator {
    type Item

    fn next(inout self: Self) -> ?Self.Item
}

struct RangeIter {
    current: i32,
    end: i32,
}

impl RangeIter: Iterator {
    type Item = i32

    fn next(inout self: RangeIter) -> ?i32 {
        if self.current >= self.end {
            return None
        }
        let value: i32 = self.current
        self.current = self.current + 1
        return Some(value)
    }
}

Unsafe Traits

For traits that have safety invariants the compiler cannot verify, use unsafe trait. Implementing such traits requires unsafe impl:

// An unsafe trait with invariants the compiler can't check
unsafe trait RawHandle {
    fn as_raw(self: Self) -> i32
}

struct FileDescriptor {
    fd: i32,
}

// Must use unsafe impl for unsafe traits
unsafe impl FileDescriptor: RawHandle {
    fn as_raw(self: FileDescriptor) -> i32 {
        return self.fd
    }
}

Key points:

  • unsafe trait indicates the trait has safety invariants the compiler cannot verify
  • Implementing an unsafe trait requires unsafe impl
  • Using unsafe impl on a safe trait is an error
  • Methods on unsafe traits can be called safely (the unsafety is in the implementation contract, not in calling)

See FFI for more examples of unsafe traits in practice.

Next Steps

  • Generics - Generic programming with trait bounds
  • Builtin Traits - Standard trait implementations
  • Structs - Implementing traits on structs
  • FFI - Foreign Function Interface with unsafe traits