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
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
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)
$ 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)
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))
$ 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.