Skip to content

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.

13-operator-overloading.zolo
Playground
// 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.

16-operator-overloading-decorator.zolo
Playground
// 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.

15-tostring.zolo
Playground
// 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.

14-method-chaining.zolo
Playground
// 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.

enespt-br