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'simpl Rectangle { }- Methods have implicit access to
self - No
selfparameter 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:
| Modifier | Self Type | Description |
|---|---|---|
| (none) | &self | Immutable borrow (default) |
mutating | &mut self | Mutable borrow |
consuming | self | Takes 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
mutatingmethods on mutable bindings (var, notlet) - The
mutatingkeyword clearly signals that the method modifies state - This pattern is inspired by Swift's
mutatingkeyword
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
| Modifier | Self Access | Use Case | Example |
|---|---|---|---|
| (none) | &self | Reading data, calculations | fn area(): Int |
mutating | &mut self | Modifying state | mutating fn scale(f: Int) |
consuming | self | Ownership transfer, transforms | consuming fn into(): T |
static | (none) | Constructors, utilities | static fn new(): Self |
Comparison with Rust
| Aspect | Rust | Oxide |
|---|---|---|
| Implementation block | impl Type { } | extension Type { } |
| Trait implementation | impl Trait for Type { } | extension Type: Trait { } |
| Immutable borrow | fn foo(&self) | fn foo() |
| Mutable borrow | fn foo(&mut self) | mutating fn foo() |
| Take ownership | fn foo(self) | consuming fn foo() |
| No self | fn foo() (associated fn) | static fn foo() |
| Method call | obj.method() | obj.method() (same) |
| Static call | Type::method() | Type.method() |
Note: Rust's
::path separator does not exist in Oxide. UsingType::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 explicitselfparameters - Default methods borrow immutably (
&self) mutatingmethods can modify state (&mut self)consumingmethods take ownership (self)staticmethods have noselfand are called on the type- Use
publicfor 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.