Operator Overloading, to_string, and Chaining
Overloading by name convention
Zolo's VM resolves operators by looking for canonical-name methods on the left
operand's type: add for +, neg for unary -, eq for ==. Because the
static type checker is conservative, use the direct form a.add(b) to pass
zolo check:
Vec2 with add, neg, eq, and to_string — called directly by method name.
// Feature: Operator overloading via special methods
// Syntax: define `add`, `eq`, `neg`, `to_string` etc. in the `impl`
// When to use: make custom types usable with `+`, `-`, `==` at
// runtime. The VM resolves the operator by looking up the method
// with the canonical name on the type of the left operand.
//
// Note: the conservative static checker may reject `a + b` for
// structs, so the DIRECT form is `a.add(b)`. The runtime effect is
// the same. We use the direct form here to satisfy the checker.
struct Vec2 {
x: float,
y: float,
}
impl Vec2 {
fn new(x: float, y: float) -> Vec2 {
return Vec2 { x: x, y: y }
}
// `+` convention.
fn add(self, other: Vec2) -> Vec2 {
return Vec2 { x: self.x + other.x, y: self.y + other.y }
}
// Unary `-` convention.
fn neg(self) -> Vec2 {
return Vec2 { x: -self.x, y: -self.y }
}
// `==` convention.
fn eq(self, other: Vec2) -> bool {
return self.x == other.x && self.y == other.y
}
// String conversion used by `print` / interpolation.
fn to_string(self) -> str {
return "Vec2({self.x}, {self.y})"
}
}
let a = Vec2::new(3.0, 4.0)
let b = Vec2::new(1.0, 2.0)
// Direct form — always works in the checker.
let c = a.add(b)
print(c.to_string()) // Vec2(4, 6)
let n = a.neg()
print(n.to_string()) // Vec2(-3, -4)
print(a.eq(b)) // false
let d = Vec2::new(3.0, 4.0)
print(a.eq(d)) // true
Overloading with @op / @operator
The decorator form explicitly binds a method to an operator symbol. The type
checker recognizes this declaration, allowing you to write a + b directly in
code. The method name is free — choose whatever is most readable in context:
@op("+"), @op("-"), @op("unary-"), and @operator("==") — natural operator syntax accepted by the checker.
// Feature: Operator overloading via `@op` / `@operator` decorators
// Syntax: `@op("symbol") fn <any_name>(self, other) -> T`
// When to use: when you want the method name to read well in the
// rest of the code (e.g. `scale`, `dot`, `combine`) but still bind
// it to an operator. Unlike the name-convention form (see file 13),
// the decorator form is recognized by the static type checker, so
// you can write `a + b` directly without the `a.add(b)` workaround.
//
// Supported symbols: `+`, `-`, `*`, `/`, `%`, `==`, `<`, `<=`, `..`,
// and the unary forms `unary-`, `not`, `len`. The full table lives
// in `docs/08-decorators.md`.
struct Vec2 {
x: float,
y: float,
}
impl Vec2 {
fn new(x: float, y: float) -> Vec2 {
return Vec2 { x: x, y: y }
}
// Method name is free; the decorator does the wiring.
@op("+")
fn plus(self, other: Vec2) -> Vec2 {
return Vec2 { x: self.x + other.x, y: self.y + other.y }
}
@op("-")
fn minus(self, other: Vec2) -> Vec2 {
return Vec2 { x: self.x - other.x, y: self.y - other.y }
}
// Unary minus uses the `unary-` symbol to disambiguate from `-`.
@op("unary-")
fn invert(self) -> Vec2 {
return Vec2 { x: -self.x, y: -self.y }
}
// `@operator` is the long alias for `@op`.
@operator("==")
fn same(self, other: Vec2) -> bool {
return self.x == other.x && self.y == other.y
}
fn to_string(self) -> str {
return "Vec2({self.x}, {self.y})"
}
}
let a = Vec2::new(3.0, 4.0)
let b = Vec2::new(1.0, 2.0)
// Natural operator syntax — the static checker accepts it because
// the decorator makes the binding explicit at the declaration site.
let sum = a + b
print(sum.to_string()) // Vec2(4, 6)
let diff = a - b
print(diff.to_string()) // Vec2(2, 2)
let neg = -a
print(neg.to_string()) // Vec2(-3, -4)
print(a == b) // false
let a2 = Vec2::new(3.0, 4.0)
print(a == a2) // true
Text representation with to_string
Define fn to_string(self) -> str in impl to control what print and the
interpolation "{obj}" display. The builtin function tostring(x) delegates
to to_string when present, and returns the natural form for primitives:
User with to_string; tostring applied to int, float, bool, str, nil, and array.
// Feature: `to_string` — controls textual representation
// Syntax: implement `fn to_string(self) -> str` in the `impl`
// When to use: so that `print(obj)` and interpolation `"{obj}"`
// return a readable string instead of the default dump.
struct User {
name: str,
age: int,
}
impl User {
fn to_string(self) -> str {
return "User({self.name}, {self.age})"
}
}
let u = User { name: "Alice", age: 30 }
print(u.to_string()) // User(Alice, 30)
print("user: {u.to_string()}") // user: User(Alice, 30)
// `tostring(x)` — builtin function that delegates to `to_string`
// when present; for primitives returns the natural form.
print(tostring(42)) // 42
print(tostring(3.14)) // 3.14
print(tostring(true)) // true
print(tostring("hi")) // hi
print(tostring(nil)) // nil
print(tostring([1, 2, 3])) // [1, 2, 3]
Method chaining
When each method returns self (or a new instance of the same type), calls can
be chained in sequence. This pattern, known as the builder, is idiomatic for
constructing values step by step:
Builder::new().word("Hello").punct(", ").word("world").punct("!").build() — fluent chain.
// Feature: Method chaining — fluent calls
// Syntax: each method returns `self` (or a new `Type`)
// When to use: builder pattern, successive transformations, DSLs.
use std::Array
struct Builder {
parts: [str],
}
impl Builder {
fn new() -> Builder {
return Builder { parts: [] }
}
// Mutation + returning self => allows chaining.
fn word(self, w: str) -> Builder {
self.parts.push(w)
return self
}
fn punct(self, p: str) -> Builder {
self.parts.push(p)
return self
}
fn build(self) -> str {
var out = ""
for p in self.parts {
out = out + p
}
return out
}
}
let s = Builder::new().word("Hello").punct(", ").word("world").punct("!").build()
print(s) // Hello, world!
// expected:
// Hello, world!
Challenge
Extend Builder with a method upper(self) -> Builder that converts the last
added word to uppercase before returning self.