What are Traits?

Traits define shared behavior that types can implement. They're similar to interfaces in other languages but more powerful - traits can provide default implementations and be used as bounds on generic types.

Defining Traits

Define a trait with the trait keyword:

traits.sx
trait Describable {
    // Required method - implementors must define
    fn describe(this: Self) -> String

    // Default method - can be overridden
    fn short_description(this: Self) -> String {
        this.describe().chars().take(50).collect()
    }
}

Method Receivers

Simplex uses this: Self for method receivers, not self. Self refers to the implementing type.

Implementing Traits

Use impl to implement a trait for a type:

impl-trait.sx
struct User {
    name: String,
    email: String,
    role: String
}

impl Describable for User {
    fn describe(this: User) -> String {
        "{this.name} ({this.role}) - {this.email}"
    }
}

struct Product {
    name: String,
    price: f64,
    sku: String
}

impl Describable for Product {
    fn describe(this: Product) -> String {
        "{this.name} (${this.price:.2}) - SKU: {this.sku}"
    }
}

fn main() {
    let user = User { name: "Alice", email: "alice@example.com", role: "Admin" }
    let product = Product { name: "Laptop", price: 999.99, sku: "LAP-001" }

    print(user.describe())
    print(product.describe())
}

Generic Types

Generics let you write code that works with any type. Use angle brackets to define type parameters:

generics.sx
// Generic struct
struct Container<T> {
    value: T,
    label: String
}

// Generic function
fn wrap<T>(value: T, label: String) -> Container<T> {
    Container { value, label }
}

// Multiple type parameters
struct Pair<A, B> {
    first: A,
    second: B
}

fn main() {
    let int_container = wrap(42, "answer")
    let str_container = wrap("hello", "greeting")

    let pair: Pair<String, i64> = Pair {
        first: "count",
        second: 100
    }
}

Trait Bounds

Constrain generic types to only accept types that implement certain traits:

bounds.sx
trait Numeric {
    fn zero() -> Self
    fn add(this: Self, other: Self) -> Self
}

// T must implement Numeric
fn sum<T: Numeric>(list: List<T>) -> T {
    list.fold(T::zero(), (acc, x) => acc.add(x))
}

// Multiple bounds with +
fn process<T: Describable + Clone>(item: T) -> String {
    let copy = item.clone()
    copy.describe()
}

// Where clause for complex bounds
fn compare<T, U>(a: T, b: U) -> Bool
    where T: Comparable,
          U: Into<T>
{
    a.compare(b.into())
}

Common Built-in Traits

Simplex provides several commonly-used traits:

Trait Purpose Key Methods
Clone Create a copy of a value clone()
Eq Check equality eq(), ne()
Ord Compare ordering compare(), lt(), gt()
Hash Compute hash for Map keys hash()
Display Convert to String for output to_string()
Default Provide a default value default()

Deriving Traits

Use @derive to automatically implement common traits:

derive.sx
// Automatically implement Clone, Eq, Hash, Display
@derive(Clone, Eq, Hash, Display)
struct Point {
    x: f64,
    y: f64
}

@derive(Clone, Eq)
enum Direction {
    North,
    South,
    East,
    West
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 }
    let p2 = p1.clone()

    if p1 == p2 {
        print("Points are equal")
    }

    print("Point: {p1}")  // Uses Display
}

Trait Objects

Use trait objects for dynamic dispatch when the concrete type is unknown at compile time:

trait-objects.sx
trait Drawable {
    fn draw(this: Self)
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Drawable for Circle {
    fn draw(this: Circle) { print("Drawing circle with radius {this.radius}") }
}

impl Drawable for Square {
    fn draw(this: Square) { print("Drawing square with side {this.side}") }
}

// Function accepting any Drawable
fn render_all(shapes: List<dyn Drawable>) {
    for shape in shapes {
        shape.draw()
    }
}

fn main() {
    let shapes: List<dyn Drawable> = [
        Circle { radius: 5.0 },
        Square { side: 10.0 },
        Circle { radius: 3.0 }
    ]
    render_all(shapes)
}

Try It Yourself

Create a Serializable trait with a to_json() method. Implement it for a Config struct with fields for host, port, and debug. Then write a generic function that serializes any Serializable to a file.

Summary

In this tutorial, you learned:

  • Defining traits with required and default methods
  • Implementing traits for your types with impl Trait for Type
  • Writing generic functions and structs with type parameters
  • Constraining generics with trait bounds
  • Using @derive to auto-implement common traits
  • Using trait objects (dyn Trait) for dynamic dispatch

In the next tutorial, we'll explore async programming for handling concurrent operations efficiently.