Traits

A trait defines a contract: a set of method signatures that a class can promise to implement. Traits are checked at compile time and cost nothing at runtime.

Defining a trait

Traits can be defined in two places: in a dedicated .trait.laz file, or inline inside a class file with the trait keyword.

A .trait.laz file starts with an optional type parameter, then lists method signatures:

// Serializable.trait.laz
serialize(): str
deserialize(s: str): dynamic

With a type parameter:

// Container.trait.laz
<T>
get(i: int): Option<T>
len(): int
is_empty(): bool

The same trait defined inline:

// inside any .class.laz or .object.laz file
trait Serializable {
    serialize(): str
    deserialize(s: str): dynamic
}

Implementing a trait

A class declares that it implements a trait with implement TraitName. The compiler then checks that the class provides every method the trait requires, with matching signatures.

// Config.class.laz
import std.Str
implement Serializable

private data: Map<str, str>

constructor() {
    .data = [:]
}

serialize(): str {
    mut out = ""
    for k, v in .data {
        out = out ++ k ++ "=" ++ v ++ "\n"
    }
    return out
}

deserialize(s: str): dynamic {
    // simplified: returns a new Config from a serialized string
    mut cfg = Config()
    for line in Str.split(s, "\n") {
        mut parts = Str.split(line, "=")
        match parts.get(0) {
            Some(k) => {
                match parts.get(1) {
                    Some(v) => { cfg.data[k] = v }
                    None    => { }
                }
            }
            None => { }
        }
    }
    return cfg
}

If the class is missing any required method, or if a method's signature does not match, the compiler reports an error.

Trait method signatures

A trait method signature names the method, its parameters, and the return type. No body. Parameters are typed the same way as regular method parameters:

trait Drawable {
    draw(canvas: Canvas): unit
    bounds(): Rect
    visible(): bool
}

Property requirements (requiring a field of a given type) are also supported:

trait Named {
    name: str
}

A practical example

Here is a small example using a trait to define a common interface for different logging backends:

// Logger.trait.laz
log(level: str, msg: str): unit
flush(): unit
// ConsoleLogger.class.laz
import std.Sys
implement Logger

log(level: str, msg: str): unit {
    Sys.print(f"[{level}] {msg}")
}

flush(): unit { }

constructor() { }
// FileLogger.class.laz
import std.File
implement Logger

private handle: dynamic

constructor(path: str) {
    .handle = File.open(path, "w")
}

log(level: str, msg: str): unit {
    .handle.write(f"[{level}] {msg}\n")
}

flush(): unit {
    .handle.close()
}

Both ConsoleLogger and FileLogger satisfy the Logger trait. The compiler checks this at build time, so you never get a runtime failure from a missing method.