Method Syntax

Methods are functions defined within the context of a type. In Oxide, we define methods using extension blocks, which associate functions with a struct, enum, or trait. This is one of Oxide's major features that differs significantly from Rust's impl blocks.

Defining Methods with Extension Blocks

Let's transform our calculateArea function from the previous section into a method on Rectangle:

#[derive(Debug)]
struct Rectangle {
    width: Int,
    height: Int,
}

extension Rectangle {
    fn area(): Int {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    }

    println!("Area: \(rect.area()) square pixels")
}

Key points:

  • extension Rectangle { } replaces Rust's impl Rectangle { }
  • Methods have implicit access to self
  • No self parameter is written in the method signature
  • Call methods with dot notation: rect.area()

Understanding self in Oxide Methods

In Oxide, self is implicit in non-static methods. The method modifier determines how self is accessed:

ModifierSelf TypeDescription
(none)&selfImmutable borrow (default)
mutating&mut selfMutable borrow
consumingselfTakes ownership
static(none)No self parameter

This is a fundamental difference from Rust, where you explicitly write &self, &mut self, or self as the first parameter.

Default Methods: Immutable Borrow

When you define a method without any modifier, it receives an immutable borrow of self:

extension Rectangle {
    // Default: borrows self immutably (&self)
    fn area(): Int {
        self.width * self.height
    }

    fn perimeter(): Int {
        2 * (self.width + self.height)
    }

    fn isSquare(): Bool {
        self.width == self.height
    }
}

These methods can read from self but cannot modify it. This is the most common method type and makes sense for any operation that doesn't change the object's state.

Rust Equivalent

The Oxide code above translates to this Rust code:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn area(&self) -> i32 {
        self.width * self.height
    }

    fn perimeter(&self) -> i32 {
        2 * (self.width + self.height)
    }

    fn is_square(&self) -> bool {
        self.width == self.height
    }
}
}

Mutating Methods: Mutable Borrow

Use the mutating modifier when a method needs to modify self:

extension Rectangle {
    mutating fn scale(factor: Int) {
        self.width *= factor
        self.height *= factor
    }

    mutating fn setWidth(newWidth: Int) {
        self.width = newWidth
    }

    mutating fn setHeight(newHeight: Int) {
        self.height = newHeight
    }

    mutating fn double() {
        self.scale(2)  // Can call other mutating methods
    }
}

fn main() {
    var rect = Rectangle { width: 10, height: 20 }
    println!("Before: {:?}", rect)

    rect.scale(3)
    println!("After scale(3): {:?}", rect)

    rect.setWidth(100)
    println!("After setWidth(100): {:?}", rect)
}

Output:

Before: Rectangle { width: 10, height: 20 }
After scale(3): Rectangle { width: 30, height: 60 }
After setWidth(100): Rectangle { width: 100, height: 60 }

Important notes:

  • You can only call mutating methods on mutable bindings (var, not let)
  • The mutating keyword clearly signals that the method modifies state
  • This pattern is inspired by Swift's mutating keyword

Rust Equivalent

#![allow(unused)]
fn main() {
impl Rectangle {
    fn scale(&mut self, factor: i32) {
        self.width *= factor;
        self.height *= factor;
    }

    fn set_width(&mut self, new_width: i32) {
        self.width = new_width;
    }
}
}

Consuming Methods: Taking Ownership

Use the consuming modifier when a method takes ownership of self:

extension Rectangle {
    consuming fn intoSquare(): Rectangle {
        let size = (self.width + self.height) / 2
        Rectangle { width: size, height: size }
    }

    consuming fn decompose(): (Int, Int) {
        (self.width, self.height)
    }

    consuming fn destroy() {
        // self is dropped at the end of this method
        println!("Rectangle destroyed!")
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 }
    let (w, h) = rect.decompose()
    println!("Width: \(w), Height: \(h)")

    // rect can no longer be used - ownership was consumed
    // println!("{:?}", rect)  // ERROR: value moved
}

Consuming methods are used when:

  • Transforming a value into something else
  • Extracting owned data from a struct
  • Intentionally consuming a resource (like closing a file handle)

The naming convention often uses "into" prefix (like intoSquare) to signal that the original value will be consumed.

Rust Equivalent

#![allow(unused)]
fn main() {
impl Rectangle {
    fn into_square(self) -> Rectangle {
        let size = (self.width + self.height) / 2;
        Rectangle { width: size, height: size }
    }

    fn decompose(self) -> (i32, i32) {
        (self.width, self.height)
    }
}
}

Static Methods: No Self Parameter

Use the static modifier for functions that don't operate on an instance:

extension Rectangle {
    static fn new(width: Int, height: Int): Rectangle {
        Rectangle { width, height }
    }

    static fn square(size: Int): Rectangle {
        Rectangle { width: size, height: size }
    }

    static fn zero(): Rectangle {
        Rectangle { width: 0, height: 0 }
    }

    static fn fromDimensions(dimensions: (Int, Int)): Rectangle {
        Rectangle {
            width: dimensions.0,
            height: dimensions.1,
        }
    }
}

fn main() {
    let rect1 = Rectangle.new(30, 50)
    let rect2 = Rectangle.square(25)
    let rect3 = Rectangle.zero()

    println!("rect1: {:?}", rect1)
    println!("rect2: {:?}", rect2)
    println!("rect3: {:?}", rect3)
}

Static methods are called on the type itself using dot notation: Rectangle.new(30, 50) rather than rect.new(30, 50).

Common uses for static methods:

  • Constructors (like new, default, fromXxx)
  • Factory methods that create instances
  • Utility functions related to the type

Using Self in Static Methods

Inside an extension block, Self (capital S) refers to the type being extended:

extension Rectangle {
    static fn square(size: Int): Self {
        Self { width: size, height: size }
    }

    static fn default(): Self {
        Self.zero()  // Can call other static methods
    }
}

Rust Equivalent

#![allow(unused)]
fn main() {
impl Rectangle {
    fn new(width: i32, height: i32) -> Rectangle {
        Rectangle { width, height }
    }

    fn square(size: i32) -> Self {
        Self { width: size, height: size }
    }
}
}

Note: In Rust, these are called "associated functions" when they don't take self. Oxide uses static to make this explicit.

Methods with Additional Parameters

Methods can take additional parameters beyond the implicit self:

extension Rectangle {
    fn canHold(other: &Rectangle): Bool {
        self.width > other.width && self.height > other.height
    }

    mutating fn resizeTo(width: Int, height: Int) {
        self.width = width
        self.height = height
    }

    fn areaRatio(other: &Rectangle): Float {
        self.area() as Float / other.area() as Float
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 }
    let rect2 = Rectangle { width: 10, height: 20 }

    println!("rect1 can hold rect2: \(rect1.canHold(&rect2))")
    println!("Area ratio: \(rect1.areaRatio(&rect2))")
}

Multiple Extension Blocks

You can split methods across multiple extension blocks:

struct Rectangle {
    width: Int,
    height: Int,
}

// Constructors
extension Rectangle {
    static fn new(width: Int, height: Int): Self {
        Self { width, height }
    }

    static fn square(size: Int): Self {
        Self { width: size, height: size }
    }
}

// Geometry calculations
extension Rectangle {
    fn area(): Int {
        self.width * self.height
    }

    fn perimeter(): Int {
        2 * (self.width + self.height)
    }
}

// Mutations
extension Rectangle {
    mutating fn scale(factor: Int) {
        self.width *= factor
        self.height *= factor
    }
}

This helps organize methods by category, though it's also fine to keep everything in a single block.

Implementing Traits with Extension Blocks

Extension blocks also implement traits using the syntax extension Type: Trait:

import std.fmt.{ Display, Formatter, Result }

#[derive(Clone)]
struct Rectangle {
    width: Int,
    height: Int,
}

extension Rectangle: Display {
    fn fmt(f: &mut Formatter): Result {
        write!(f, "Rectangle(\(self.width) x \(self.height))")
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 }
    println!("\(rect)")  // Uses Display implementation
}

This replaces Rust's impl Trait for Type syntax. The colon reads naturally: "extend Rectangle with Display capability."

Multiple Trait Implementations

import std.fmt.{ Display, Formatter, Result }
import std.cmp.{ Ord, Ordering }

extension Rectangle: Display {
    fn fmt(f: &mut Formatter): Result {
        write!(f, "\(self.width)x\(self.height)")
    }
}

extension Rectangle: PartialOrd {
    fn partialCmp(other: &Self): Ordering? {
        self.area().partialCmp(&other.area())
    }
}

Visibility in Extension Blocks

Use public to make methods accessible from other modules:

public struct Rectangle {
    public width: Int,
    public height: Int,
}

extension Rectangle {
    public static fn new(width: Int, height: Int): Self {
        Self { width, height }
    }

    public fn area(): Int {
        self.width * self.height
    }

    // Private helper method
    fn validate(): Bool {
        self.width > 0 && self.height > 0
    }

    public mutating fn scale(factor: Int) {
        if self.validate() {
            self.width *= factor
            self.height *= factor
        }
    }
}

Complete Example: A Point Struct

Here's a comprehensive example showing all method modifiers:

#[derive(Debug, Clone, PartialEq)]
public struct Point {
    x: Float,
    y: Float,
}

extension Point {
    // Static: constructors
    public static fn new(x: Float, y: Float): Self {
        Self { x, y }
    }

    public static fn origin(): Self {
        Self { x: 0.0, y: 0.0 }
    }

    public static fn fromAngle(angle: Float, distance: Float): Self {
        Self {
            x: distance * angle.cos(),
            y: distance * angle.sin(),
        }
    }

    // Default (&self): read-only operations
    public fn distanceFromOrigin(): Float {
        (self.x * self.x + self.y * self.y).sqrt()
    }

    public fn distanceTo(other: &Point): Float {
        let dx = self.x - other.x
        let dy = self.y - other.y
        (dx * dx + dy * dy).sqrt()
    }

    public fn midpointTo(other: &Point): Point {
        Point {
            x: (self.x + other.x) / 2.0,
            y: (self.y + other.y) / 2.0,
        }
    }

    // Mutating (&mut self): modifications
    public mutating fn translate(dx: Float, dy: Float) {
        self.x += dx
        self.y += dy
    }

    public mutating fn scale(factor: Float) {
        self.x *= factor
        self.y *= factor
    }

    public mutating fn normalize() {
        let dist = self.distanceFromOrigin()
        if dist != 0.0 {
            self.x /= dist
            self.y /= dist
        }
    }

    // Consuming (self): ownership transfer
    public consuming fn intoPolar(): (Float, Float) {
        let r = self.distanceFromOrigin()
        let theta = self.y.atan2(self.x)
        (r, theta)
    }

    public consuming fn add(other: Point): Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    // Using static methods
    var point = Point.new(3.0, 4.0)
    let origin = Point.origin()

    // Using default (immutable) methods
    println!("Distance from origin: \(point.distanceFromOrigin())")
    println!("Distance to origin: \(point.distanceTo(&origin))")

    // Using mutating methods
    point.translate(1.0, 1.0)
    println!("After translate: {:?}", point)

    point.scale(2.0)
    println!("After scale: {:?}", point)

    // Using consuming method
    let polar = point.intoPolar()
    println!("Polar coordinates: r=\(polar.0), theta=\(polar.1)")

    // point is now consumed and cannot be used
}

Summary of Method Modifiers

ModifierSelf AccessUse CaseExample
(none)&selfReading data, calculationsfn area(): Int
mutating&mut selfModifying statemutating fn scale(f: Int)
consumingselfOwnership transfer, transformsconsuming fn into(): T
static(none)Constructors, utilitiesstatic fn new(): Self

Comparison with Rust

AspectRustOxide
Implementation blockimpl Type { }extension Type { }
Trait implementationimpl Trait for Type { }extension Type: Trait { }
Immutable borrowfn foo(&self)fn foo()
Mutable borrowfn foo(&mut self)mutating fn foo()
Take ownershipfn foo(self)consuming fn foo()
No selffn foo() (associated fn)static fn foo()
Method callobj.method()obj.method() (same)
Static callType::method()Type.method()

Note: Rust's :: path separator does not exist in Oxide. Using Type::method() in Oxide code will cause a syntax error. Oxide uses . as its only path separator.

The key insight is that Oxide makes the method's relationship to self explicit through modifiers rather than through the first parameter. This makes the intent clearer when reading method signatures.

Summary

Extension blocks are Oxide's way of adding methods to types:

  • Use extension Type { } for inherent methods
  • Use extension Type: Trait { } for trait implementations
  • Method modifiers (mutating, consuming, static) replace explicit self parameters
  • Default methods borrow immutably (&self)
  • mutating methods can modify state (&mut self)
  • consuming methods take ownership (self)
  • static methods have no self and are called on the type
  • Use public for visibility, just like with structs

This syntax makes method signatures more readable and the relationship between methods and their data more explicit, while compiling to exactly the same code as equivalent Rust.