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

Bindings, Scope, and Mutability

Ferlium lets you name values, limit where those names are visible, and decide which names refer to values that can be modified. This chapter explains let and let mut, how scopes work, and the intuitive model behind Ferlium’s mutable value semantics.

let bindings

A let binding introduces a new name for a value:

let x = 1;
let y = x + 2;
y

Bindings are immutable by default. That means you can read x, but you cannot modify the value of x later.

let mut bindings

Use let mut when you want a variable that can be modified after it is defined:

let mut counter = 0;
counter = counter + 1;
counter

Assignment itself produces the unit value ():

let mut a = 1;
a = 2;   // value is ()
a

Scope

Every block introduces a new scope. A name is visible from its definition to the end of the block where it is defined.

let x = 1;
{
    let y = x + 1;
    y
}

Here, y exists only inside the block. The value of the block is the value of its last expression.

Shadowing

You can reuse a name in an inner scope. The new binding temporarily hides the outer one. This is called shadowing.

let x = 1;
{
    let x = 2;
    x
}

The inner x does not change the outer x. When the block ends, the outer x is visible again.

You can also shadow a name in the same scope:

let x = 1;
let x = x + 1;
x

In this case, the first x is not accessible after the second x is defined. The type of the new x can be different from the old one. For example, you can shadow an integer with a string:

let x = 1;
let x = f"number {x}";
x

Shadowing can be done even though the binding is not mutable, because the new binding is a different name that happens to shadow the old one.

Function-local variables

Function parameters behave like local bindings and are scoped to the function body. Whether a function can modify a value depends on how that value is passed, which will be discussed later. Function parameters can also be shadowed:

fn add_one(n) {
    let n = n + 1;
    n
}

add_one(10)

This uses a new local n that shadows the parameter inside the function body.

Mutable function parameters

You can write mut before a function parameter name to get a mutable local binding for it inside the function body:

fn add_one(mut n) {
    n += 1;
    n
}

add_one(10)

This is equivalent to writing:

fn add_one(n) {
    let mut n = n;
    n += 1;
    n
}

add_one(10)

The mut notation is purely a convenience: the function signature is unchanged, and the caller’s value is never affected. You can mix mut and non-mut parameters freely:

fn add_n(mut x, n) {
    x += n;
    x
}

add_n(5, 3)

Mutable value semantics

Think of a binding as a named cell that holds a value. With let, the cell is read-only. With let mut, the cell is writable. Reassigning a mutable binding replaces the value stored in that cell.

In Ferlium, values are not implicitly shared between bindings. Reassigning a mutable binding replaces the value stored in that binding, and updates to compound values (such as tuples or arrays) affect only that binding unless the value was explicitly passed in a mutable way. This model avoids hidden aliasing: two separate bindings do not unexpectedly refer to the same mutable storage.

Compound values can be updated through a mutable binding:

let mut p = (1, 2);
p.0 = 3;

let mut xs = [1, 2];
xs[0] = 3;

(p, xs)

If the binding is not mutable, these updates are rejected at compile time.

Destructuring bindings

let can destructure product values instead of binding the whole value to a single name.

Tuples

Tuple destructuring binds each element by position:

let point = (10, 20);
let (x, y) = point;

(x, y)

Use _ to ignore elements you do not need:

let triple = (1, 2, 3);
let (first, _, third) = triple;

(first, third)

Patterns can be nested:

let ((feet, inches), (x, y)) = ((3, 10), (4, 5));

feet + inches + x + y

Records

Anonymous records can be destructured by field name:

let person = { name: "Ada", age: 36 };
let { age, name } = person;

f"{name} is {age}"

Field order does not matter.

You can rename fields while destructuring:

let p = { x: 3, y: 4 };
let { x: horizontal, y: vertical } = p;

horizontal + vertical

Named structs

Tuple structs use tuple-style destructuring:

struct Color(int, int, int)

let c = Color(10, 20, 30);
let Color(r, g, b) = c;

r + g + b

Record structs use record-style destructuring:

struct Point { x: int, y: int }

let p = Point { x: 3, y: 4 };
let Point { x, y } = p;

x + y

For named tuple and record structs, .. ignores the remaining elements or fields:

struct Point3 { x: int, y: int, z: int }

let p = Point3 { x: 1, y: 2, z: 3 };
let Point3 { x, .. } = p;

x

Mutability in destructuring

Mutability applies to each binding individually:

let (mut a, b) = (1, 2);
a = 10;

a + b

This is different from let mut value = ..., which makes the whole binding mutable.

What comes next

The next chapter introduces functions and type inference, showing how Ferlium automatically determines types for your code.