Skip to main content
v0.8.0

Tutorial: Structs, Enums, and Methods

This tutorial shows how to define your own types in Metel. Structs hold named data; enums represent a fixed set of alternatives; impl blocks attach methods to either.

Structs​

A struct is a named collection of typed fields:

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

Create an instance by naming every field:

fun main() {
let origin = Point { x: 0.0, y: 0.0 };
let p = Point { x: 3.0, y: 4.0 };

println("x = ${p.x}, y = ${p.y}");
}

Fields are accessed with .field_name. They are read-only through immutable bindings.

note

Struct Literals Are Explicit

When you construct a struct, you name every field unless shorthand field init applies. There is no positional constructor syntax here.

Adding methods with impl​

Methods live in an impl block for the type. The first parameter self is the receiver:

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

impl Point {
fun distance_from_origin(self) -> f64 {
let x2 = self.x * self.x;
let y2 = self.y * self.y;
// Metel has no sqrt builtin yet — return the squared distance
return x2 + y2;
}

fun to_string(self) -> String {
return "(${self.x}, ${self.y})";
}
}

fun main() {
let p = Point { x: 3.0, y: 4.0 };
println(p.to_string()); // (3, 4)
println(p.distance_from_origin()); // 25
}

Methods are called with .method_name(args). self is passed implicitly.

tip

Think “Function Namespaced Under The Type”

An impl method is still just a function. Dot syntax is the ergonomic call form, but the main extra idea is the implicit self.

The Self type​

Inside an impl block, Self is an alias for the type being implemented. This is useful for methods that return a modified copy:

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

impl Point {
fun translate(self, dx: f64, dy: f64) -> Self {
return Point { x: self.x + dx, y: self.y + dy };
}

fun scale(self, factor: f64) -> Self {
return Point { x: self.x * factor, y: self.y * factor };
}
}

fun main() {
let p = Point { x: 1.0, y: 2.0 };
let moved = p.translate(3.0, 4.0);
println("${moved.x}, ${moved.y}"); // 4, 6

let big = moved.scale(2.0);
println("${big.x}, ${big.y}"); // 8, 12
}

Self is equivalent to writing the type name explicitly, but stays correct if the type is renamed.

Enums​

An enum defines a type with a fixed set of named variants. Variants can carry data:

enum Direction {
North,
South,
East,
West,
}

enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}

Unit variants (like North) carry no data. Struct variants (like Circle) carry named fields.

Create enum values with :::

fun main() {
let dir = Direction::North;
let s = Shape::Circle { radius: 5.0 };
let r = Shape::Rectangle { width: 3.0, height: 4.0 };
}

Pattern matching with match​

match is how you inspect an enum value. Every variant must be handled:

enum Direction {
North,
South,
East,
West,
}

fun describe(d: Direction) -> String {
match d {
Direction::North => "heading north",
Direction::South => "heading south",
Direction::East => "heading east",
Direction::West => "heading west",
}
}

fun main() {
println(describe(Direction::North)); // heading north
println(describe(Direction::West)); // heading west
}

For variants with fields, destructure them in the pattern:

enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}

fun area(s: Shape) -> f64 {
match s {
Shape::Circle { radius } => 3.14159 * radius * radius,
Shape::Rectangle { width, height } => width * height,
}
}

fun main() {
let c = Shape::Circle { radius: 5.0 };
let r = Shape::Rectangle { width: 3.0, height: 4.0 };

println("circle area: ${area(c)}"); // circle area: 78.53975
println("rectangle area: ${area(r)}"); // rectangle area: 12
}

The field names in a pattern bind to local variables — radius, width, and height are available in the arm body.

caution

match Must Stay Exhaustive

When you add a new enum variant later, every match over that enum must be updated unless you already use a wildcard arm.

Methods on enums​

impl works the same way on enums:

enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}

impl Shape {
fun area(self) -> f64 {
match self {
Shape::Circle { radius } => 3.14159 * radius * radius,
Shape::Rectangle { width, height } => width * height,
}
}

fun describe(self) -> String {
match self {
Shape::Circle { radius } => "circle with radius ${radius}",
Shape::Rectangle { width, height } => "rectangle ${width}×${height}",
}
}
}

fun main() {
let shapes = [
Shape::Circle { radius: 2.0 },
Shape::Rectangle { width: 5.0, height: 3.0 },
Shape::Circle { radius: 1.0 },
];

let mut total_area = 0.0;
for (let s in shapes) {
println("${s.describe()} — area ${s.area()}");
total_area += s.area();
}
println("total area: ${total_area}");
}

Output:

circle with radius 2 — area 12.56636
rectangle 5×3 — area 15
circle with radius 1 — area 3.14159
total area: 30.70795

A complete example: a card game hand​

Here is a longer program that combines structs and enums to represent a simple card game:

enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}

enum Rank {
Number { value: i64 },
Jack,
Queen,
King,
Ace,
}

struct Card {
suit: Suit,
rank: Rank,
}

impl Suit {
fun to_string(self) -> String {
match self {
Suit::Clubs => "Clubs",
Suit::Diamonds => "Diamonds",
Suit::Hearts => "Hearts",
Suit::Spades => "Spades",
}
}
}

impl Rank {
fun to_string(self) -> String {
match self {
Rank::Number { value } => value.to_string(),
Rank::Jack => "Jack",
Rank::Queen => "Queen",
Rank::King => "King",
Rank::Ace => "Ace",
}
}

fun point_value(self) -> i64 {
match self {
Rank::Number { value } => value,
Rank::Jack => 10,
Rank::Queen => 10,
Rank::King => 10,
Rank::Ace => 11,
}
}
}

impl Card {
fun to_string(self) -> String {
return "${self.rank.to_string()} of ${self.suit.to_string()}";
}

fun value(self) -> i64 {
return self.rank.point_value();
}
}

fun hand_value(hand: Card[]) -> i64 {
let mut total = 0;
for (let card in hand) {
total += card.value();
}
return total;
}

fun main() {
let hand = [
Card { suit: Suit::Hearts, rank: Rank::Ace },
Card { suit: Suit::Spades, rank: Rank::King },
Card { suit: Suit::Diamonds, rank: Rank::Number { value: 7 } },
];

for (let card in hand) {
println(" ${card.to_string()} (${card.value()} pts)");
}
println("Hand total: ${hand_value(hand)}");
}

Output:

Ace of Hearts (11 pts)
King of Spades (10 pts)
7 of Diamonds (7 pts)
Hand total: 28
note

This Is The Main Payoff Of ADTs

Structs and enums become more useful together: structs model stable records, enums model alternatives, and match ties them into explicit control flow.

What you learned​

  • struct defines a named record with typed fields; impl attaches methods to it.
  • self is the receiver; Self is an alias for the implementing type.
  • enum defines a type with named variants, each optionally carrying data.
  • match exhaustively handles every variant and destructures field data into local variables.
  • Both structs and enums support impl blocks in the same way.

Next: Pointers — address-of, dereference, and shared mutable state.