Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Return a reference to a field

A function that returns a reference borrows it from its first reference parameter; for a method, that is self. The returned value must borrow that parameter, never a later one or a local owner. The caller keeps the original value alive and borrowed until the result’s last use.

class Pair:
    left: i64
    right: i64
    def left_ref(self: &mut Pair) -> &mut i64:
        &mut self.left

def main() -> Result[(), IoError]:
    mut pair = Pair(1, 2)
    left = pair.left_ref()
    *left = 8
    print(pair.left)?
    Ok(())
8

A mutable returned reference requires the first reference parameter to be &mut. A temporary receiver cannot supply a reference that outlives it.

Later reference parameters only lend their values for the call. A lookup can take its key by reference and still return a borrow of the collection:

def score(scores: &dict[str, i64], name: &str) -> &i64:
    &scores[*name]

def main() -> Result[(), AllocError | IoError]:
    scores = {"ada": 3, "bob": 5}?
    mut name = "bob"
    points = score(&scores, &name)
    name = "eve"
    print(*points)?
    print(name)?
    Ok(())
5
eve

The key’s loan ends when score returns, so name can change while points is live. To return a borrow of name, it would have to be the first parameter.

Return a borrow that may be absent

A search may find nothing. Return Option[&T], or Option[&mut T] to let the caller change what was found. Some must borrow the first reference parameter; Nothing borrows nothing:

class Item:
    name: str
    count: i64

def find(items: &mut list[Item], name: &str) -> Option[&mut Item]:
    for item in items:
        if item.name == name:
            return Some(item)
    Nothing

def add(items: &mut list[Item], name: str) -> Result[(), AllocError | OverflowError[i64]]:
    match find(items, &name):
        case Some(item):
            item.count += 1
        case Nothing:
            items.append(Item(name, 1))?
    Ok(())

def main() -> Result[(), AllocError | IoError | AllocError | OverflowError[i64]]:
    mut items: list[Item] = []?
    add(&mut items, "ada")?
    add(&mut items, "bob")?
    add(&mut items, "ada")?
    for item in items:
        print(item.name)?
        print(item.count)?
    Ok(())
ada
2
bob
1

Match the result directly. In the Some arm, item borrows items, so the list cannot change there. The Nothing arm holds no borrow, so it can append. Result[&T, E] works the same way with Ok; its error cannot hold a reference. ? and unwrap() also produce the reference.

The result can be held in a local until it is matched, but it cannot be stored in a list, a field, or a mut binding: match it and store an owned copy or an index. The reference lists every rule.

Shared arguments can borrow temporaries, as in size("ada") for a size(s: &str) function. The temporary lives through the expression. A result borrowing it may be used immediately, but cannot be stored or returned. Mutable temporaries are unsupported.

An exclusive method can use a completed read of its receiver as an argument. This also works through an existing exclusive reference:

import std.bytes
from std.bytes import Bytes

def clear(buffer: &mut Bytes) -> ():
    buffer.fill(0, buffer.len(), 0u8)

def main() -> Result[(), AllocError | IoError]:
    mut buffer = std.bytes.from_list([1u8, 2u8]?)?
    clear(&mut buffer)
    print(buffer.to_list()?)?
    Ok(())
[0, 0]

The receiver is reserved first, arguments run left to right, and exclusive access begins when the method starts. Arguments may read the receiver, but cannot mutate or move it, or pass a reference into it that remains live during the call. For nested mutations, compute the inner result in a separate local before calling the outer method.