Methods

Kōdo supports inherent impl blocks, which allow you to define methods directly on struct types without requiring a trait.

Defining Methods

Use impl TypeName { ... } to add methods to a struct:

struct Point {
    x: Int,
    y: Int,
}

impl Point {
    fn translate(self, dx: Int, dy: Int) -> Point {
        return Point { x: self.x + dx, y: self.y + dy }
    }

    fn manhattan_distance(self) -> Int {
        let ax: Int = self.x
        if ax < 0 { ax = 0 - ax }
        let ay: Int = self.y
        if ay < 0 { ay = 0 - ay }
        return ax + ay
    }
}

Calling Methods

Methods are called using dot notation:

let p: Point = Point { x: 3, y: 4 }
let moved: Point = p.translate(1, 2)
let dist: Int = moved.manhattan_distance()

Static Methods

Methods that don’t take self as the first parameter act as static/associated functions:

struct Counter {
    value: Int
}

impl Counter {
    fn new() -> Counter {
        return Counter { value: 0 }
    }
}
let c: Counter = Counter.new()

Inherent vs Trait Impl

  • impl Point { ... } — inherent impl (no trait required)
  • impl Printable for Point { ... } — trait impl (implements a trait)

Both can coexist for the same type.

Method Chaining

Since methods that return Self (or the same struct type) produce a new value, you can chain method calls by assigning intermediate results:

let c: Counter = Counter { value: 0 }
let c2: Counter = c.increment()
let c3: Counter = c2.increment()
print_int(c3.get())  // 2

This works because increment returns a new Counter each time. In Kodo, the self receiver takes the value by ownership, so each call consumes the previous value and produces a new one.

Builder Pattern

You can use the returning-self pattern to build up struct values step by step:

struct Config {
    port: Int,
    host: String,
    max_connections: Int
}

impl Config {
    fn new() -> Config {
        return Config { port: 8080, host: "localhost", max_connections: 100 }
    }

    fn with_port(self, port: Int) -> Config {
        return Config { port: port, host: self.host, max_connections: self.max_connections }
    }

    fn with_host(self, host: String) -> Config {
        return Config { port: self.port, host: host, max_connections: self.max_connections }
    }

    fn with_max_connections(self, n: Int) -> Config {
        return Config { port: self.port, host: self.host, max_connections: n }
    }
}
let base: Config = Config.new()
let c1: Config = base.with_port(3000)
let c2: Config = c1.with_host("0.0.0.0")
let c3: Config = c2.with_max_connections(500)

Each with_* method takes ownership of self, modifies one field, and returns a new Config. This is the idiomatic way to build complex values in Kodo.

Methods on Enums

Methods work on built-in enum types like Option<T> and Result<T, E>:

let opt: Option<Int> = Option::Some(42)
let is_present: Bool = opt.is_some()      // true
let is_empty: Bool = opt.is_none()        // false
let val: Int = opt.unwrap_or(0)           // 42

let none_opt: Option<Int> = Option::None
let fallback: Int = none_opt.unwrap_or(99)  // 99
let ok_res: Result<Int, String> = Result::Ok(10)
let is_ok: Bool = ok_res.is_ok()          // true
let is_err: Bool = ok_res.is_err()        // false
let ok_val: Int = ok_res.unwrap_or(0)     // 10

let err_res: Result<Int, String> = Result::Err("failed")
let err_val: Int = err_res.unwrap_or(42)  // 42

Available enum methods:

TypeMethodReturnsDescription
Option<T>is_some()BoolTrue if Some
Option<T>is_none()BoolTrue if None
Option<T>unwrap()TExtracts value, aborts if None
Option<T>unwrap_or(default)TReturns value or the default
Result<T, E>is_ok()BoolTrue if Ok
Result<T, E>is_err()BoolTrue if Err
Result<T, E>unwrap()TExtracts value, aborts if Err
Result<T, E>unwrap_or(default)TReturns value or the default
Result<T, E>unwrap_err()EExtracts error, aborts if Ok

Methods with Closure Parameters

Methods can accept closures as arguments, enabling higher-order patterns:

struct Box {
    value: Int
}

impl Box {
    fn apply(self, f: (Int) -> Int) -> Int {
        return f(self.value)
    }
}
let b: Box = Box { value: 10 }
let result: Int = b.apply(|x: Int| -> Int { x * 2 })  // 20

The self Receiver

In Kōdo, the self parameter in methods always takes the value by ownership (own semantics). This means:

  • After calling p.translate(1, 2), the original p is consumed (moved).
  • If you need to use a value after a method call, the method should return a new value.
  • For types like Int, Bool, and Float64 that are Copy, self is implicitly copied and the original remains usable.

Unlike standalone function parameters, method receivers do not support explicit ref or mut qualifiers — self is always by-value. This simplifies the method call model: each call either copies (for Copy types) or moves (for non-Copy types) the receiver.

Receiver typeWhat happens on method call
Copy type (Int, Bool, Float64, Byte)self is copied — original remains usable
Non-Copy type (String, structs)self is moved — original is consumed

For standalone functions (not methods), Kōdo supports explicit ownership annotations on parameters (own, ref, mut). See Ownership for details.