Heroes / Guide / chapter 7

Bindings and mutation

The @= symbol declares a mutable variable, and @ marks the operations that change it.

To understand a program, you need to know which operations change its data. In Heroes, the @ symbol identifies those operations.

= binds a fixed value, @= a mutable one

x = 5  # a name for a value, forever
v @= 0  # a cell that may change
v @ v + 1  # changing it

= binds a name to a value that cannot be reassigned. @= declares a cell, a variable whose value can change, and @ assigns a new value to it. Later in this chapter, you will see how @ also marks parameters that a function can change.

A cell takes its type from its first value, as a binding does, and you may write the type on the line when you want it there: v: i64 @= 0. Its value can change, but its type stays the same. If you try to change a name bound with =, the compiler reports an error:

$ heroes check mutation.hero
error[not_mutable]: `start` was bound with `=`, which binds once, forever โ€” a cell is declared `start @= <value>`
  at mutation.hero:31:5
     |
  31 |     start @ start + 1
     |     ^^^^^
  fix (guess): declare `start` as a cell: `start @= <value>`

The fix is tagged guess, a suggestion to review, rather than certain, a definite correction. You may have intended to declare a cell or used the wrong name. The compiler can apply only certain fixes automatically.

Each visible name has one definition

You cannot reuse a name while its earlier declaration is visible at that point in the program:

$ heroes check mutation.hero
error[shadowed_binding]: `start` is already in scope, bound at line 30 โ€” shadowing is an error here: a name means one thing for as long as it is visible
  at mutation.hero:31:5
     |
  31 |     start = r.pos + 1
     |     ^^^^^

Reusing a visible name is called shadowing: the new declaration hides the earlier one. In languages that allow it, you can end up using the wrong value without a compile error. Heroes rejects shadowing.

Every value is a copy

test "values are copies"
    a = [1, 2, 3]
    b: [i64] @= a
    b @ b.push(4)
    assert a.len() == 3
    assert b.len() == 4
examples/gallery/05-mutation.hero, lines 46–51

b is a copy of a: adding an element to b does not change a. This holds for arrays, maps, strings and records, including nested values. To avoid unnecessary copies, the compiler can share memory until a value changes. This internal detail does not affect how copies behave in your program.

In b @ b.push(4), push returns a new array. @ assigns the result to b.

When a function does need to change its argument

A function can also change an argument, such as the current reading position in a text. Mark that parameter with @:

# The `@` is the whole signature's news: this one moves.
function advance(@r: Reader)
    r.pos @ r.pos + 1
examples/gallery/05-mutation.hero, lines 19–21

The call must also include @, or the program will not compile:

# Consumes spaces. The caller sees `@r` and knows the reader moved.
function skip_spaces(@r: Reader)
    while !at_end(r) && here(r) == ' '
        advance(@r)
examples/gallery/05-mutation.hero, lines 23–26
$ heroes check mutation.hero
error[marker_mismatch]: `advance` changes this argument, so the call site writes `@` too
  at mutation.hero:26:17
     |
  26 |         advance(r)
     |                 ^
  fix (certain): mark the argument: `@x`

Reading advance(@r) tells you that the function can change r. You do not need to open the definition of advance to find out.

The behaviour is copy in, copy out: the function works on its own copy, then replaces the caller’s value with the updated copy when it returns. This also happens on an early return.

# Reads a run of letters and hands it back, leaving the reader after it.
function word_at(@r: Reader) -> str
    start = r.pos

    while !at_end(r) && here(r) >= 'a' && here(r) <= 'z'
        advance(@r)

    return r.text.slice(from: start, to: r.pos)
examples/gallery/05-mutation.hero, lines 28–35

A function can change its argument and return a value. Here word_at advances the position past the word and returns the word it read. A call with a dot, r.word_at(), is not allowed when the first parameter is @: write word_at(@r) to make the change visible.

A binding nobody reads is an error

$ heroes check mutation.hero
error[unused_binding]: `spare` is bound and never read โ€” remove the binding, or read it
  at mutation.hero:55:5
     |
  55 |     spare = 5
     |     ^^^^^

The compiler reports a name that is declared but never read: it may be left over from an edit or indicate a forgotten result. To discard a result deliberately, write _ = f(x). The symbol _ binds no name and can be reused.

The program

function main()
    r: Reader @= Reader(text: "one two", pos: 0)
    print(word_at(@r))
examples/gallery/05-mutation.hero, lines 53–55
$ heroes run examples/gallery/05-mutation.hero
one

Next: Generics and function values: one shape that works for many types, and functions passed around as values. Back to the chapter list. ← Previous: Control flow

Run a program of your own: it takes four commands. Or take the whole language in one file, and every program already written in it.