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:
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:
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:
// 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:
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:
// 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 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
@deriveto 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.