Skip to main content
v0.12.0

Tutorial: References

This tutorial introduces Metel's reference model. References are explicit aliases to existing storage:

  • &x creates a shared reference with type &T
  • &var x creates an exclusive mutable reference with type &var T
  • &var also works on addressable fields, elements, and nested lvalue paths
  • references are storable values, but they do not own the thing they point at

The file is still named pointers.mdx for link stability, but the current language surface calls these values references.

Taking a reference

fun main() {
let value = 42;
let read_ref: &i64 = &value;
let current: i64 = read_ref;
println(current); // 42

var counter = 0;
let write_ref: &var i64 = &var counter;
*write_ref = 50;
println(counter); // 50
}

&i64 is a shared reference to an i64. &var i64 is an exclusive mutable reference to an i64. Writing through a reference uses the explicit dereference operator *write_ref = ... — plain assignment to the binding itself is rejected, since the binding is not what's being updated.

tip

Read This As Two Choices

Choose reference capability at the address-taking site:

  • &x for &T
  • &var x for &var T

Why references exist

References are mainly about explicit aliasing. They let multiple places talk about the same storage instead of copying a value around.

That matters in patterns like:

  • mutable shared state between closures
  • data structures that need indirection
  • APIs that observe or update a value without taking ownership of it

Shared mutable state

References are useful when multiple parts of a program need to talk about the same value explicitly. That includes shared mutable state between closures:

fun main() {
var total = 0;
let cell: &var i64 = &var total;

let add_one = () -> {
*cell += 1;
};

let add_ten = () -> {
*cell += 10;
};

add_one();
add_ten();

println(total); // 11
}

Both closures mutate the same underlying i64 through the same reference.

Ordinary closure capture is by value. The reference is what makes the sharing explicit: both closures talk to the same storage location, not independent copies.

Taking mutable references to fields and elements

&var is not limited to a plain variable. It also works on addressable paths such as struct fields, tuple elements, and array elements:

struct Counter {
value: i64,
}

fun main() {
var counter = Counter { value = 10 };
let field_ref: &var i64 = &var counter.value;

*field_ref += 5;
println(counter.value); // 15
}

That same rule applies to nested paths. The write goes back to the original storage location, not a temporary copy.

Reading and writing through references

Reading is implicit: a value is copied out of a reference whenever the expected type is the referent type, with no operator needed. Writing is explicit: it always goes through the dereference operator, *r = ...:

fun main() {
var n = 1;
let r: &var i64 = &var n;

*r = 4; // write-through
let value: i64 = r; // type-directed read
println(value); // 4
}

*r = 4 writes through the reference to the storage it points at. Plain r = 4 is rejected — the binding itself isn't reassignable, and reassigning it wouldn't be the write you want anyway. Reading has no such operator: let value: i64 = r copies the referent out because the expected type (i64) is already known at that position.

note

Explicit Where It Matters

Metel makes aliasing visible with & and &var. Ordinary field access and method calls auto-dereference without needing *, and reading a plain value out of a reference is implicit too — but writing through a reference always requires *r = ....

Auto-deref for fields and methods

Field access and method calls automatically dereference through reference layers:

struct Counter {
value: i64,
}

extend Counter {
fun increment(&var self) {
self.value += 1;
}
}

fun main() {
var counter = Counter { value = 0 };
let r: &var Counter = &var counter;

r.increment();
println(r.value); // 1
}

Auto-deref chains through multiple reference layers, so a &&var Counter can still reach the underlying Counter for field reads and method calls.

Function references

References to closures or functions can be called directly:

fun main() {
let f = () -> i64 { return 42; };
let r: &() -> i64 = &f;
println(r()); // 42
}

References and future ownership features

Current references provide explicit aliasing for non-linear values. Future affine/linear ownership features will introduce stricter rules for exactly-once consumption; ordinary &T / &var T references are not the mechanism for consuming owned values.

caution

Boundary Rule

References are for explicit aliasing. If a feature depends on exactly-once ownership, regular references are the wrong tool.

What you learned

  • &x yields &T.
  • &var x yields &var T, and &var also works on addressable lvalue paths.
  • Writing through a &var T requires the explicit dereference operator: *r = value.
  • Reading a plain value out of a reference is implicit and type-directed; no operator needed.
  • Fields, methods, and function-reference calls auto-dereference through reference layers.

Next: Closures and Capturing — first-class functions, captured values, and shared mutation.