Heroes / Guida / capitolo 7

Nomi e modifiche

Il simbolo @= dichiara una variabile modificabile, e @ indica le operazioni che la cambiano.

Per capire un programma serve sapere quali operazioni modificano i dati. In Heroes queste operazioni si riconoscono dal simbolo @.

= definisce un valore fisso, @= uno modificabile

x = 5  # un nome per un valore, per sempre
v @= 0  # una cella che può cambiare
v @ v + 1  # la si cambia

= associa un nome a un valore che non potrai riassegnare. @= dichiara invece una cella, cioè una variabile modificabile, e @ le assegna un nuovo valore. Più avanti vedrai come @ indica anche i parametri che una funzione può modificare.

Una cella prende il tipo dal suo primo valore, come un nome definito con =, e puoi scriverlo sulla riga quando lo vuoi lì: v: i64 @= 0. Il valore può cambiare, ma il tipo resta quello. Se provi a modificare un nome definito con =, il compilatore segnala l’errore:

$ 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>`

La correzione è indicata come guess, cioè una proposta da verificare, anziché certain, cioè certa. Potresti aver voluto dichiarare una cella oppure aver usato il nome sbagliato. Il compilatore può applicare automaticamente solo le correzioni certain.

Ogni nome visibile ha una sola definizione

Non puoi riutilizzare un nome finché la sua dichiarazione precedente è visibile in quel punto del programma:

$ 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
     |     ^^^^^

Riutilizzare un nome già visibile si chiama shadowing: la nuova dichiarazione nasconde la precedente. Nei linguaggi che lo consentono, puoi così usare il valore sbagliato senza un errore di compilazione. Heroes rifiuta lo shadowing.

Ogni valore è una copia

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, righe 46–51

b è una copia di a: aggiungere un elemento a b non modifica a. La regola vale per array, mappe, stringhe e record, anche quando contengono altri valori. Per evitare copie inutili, il compilatore può condividere la memoria finché non avviene una modifica. Questo dettaglio interno non cambia il comportamento delle copie nel programma.

In b @ b.push(4), push restituisce un nuovo array. @ assegna il risultato a b.

Quando una funzione deve davvero cambiare il suo argomento

Una funzione può anche modificare un argomento, per esempio la posizione di lettura in un testo. In questo caso il parametro deve avere il simbolo @:

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

Devi scrivere @ anche nella chiamata, altrimenti il programma non compila:

# 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, righe 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`

Leggendo advance(@r) sai che la funzione può modificare r. Non serve aprire la definizione di advance per scoprirlo.

Il comportamento è copia dentro, copia fuori: la funzione lavora su una propria copia e, quando termina, sostituisce il valore originale con quello aggiornato. Questo avviene anche con un return anticipato.

# 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, righe 28–35

Una funzione può modificare il suo argomento e restituire un valore. Qui word_at avanza la posizione oltre la parola e restituisce la parola letta. La chiamata con il punto, r.word_at(), è vietata quando il primo parametro è @: devi scrivere word_at(@r) per rendere visibile la modifica.

Un nome che nessuno legge è un errore

$ 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
     |     ^^^^^

Il compilatore segnala un nome dichiarato ma mai letto: potrebbe essere rimasto dopo una modifica o indicare un risultato dimenticato. Per scartare intenzionalmente un risultato, scrivi _ = f(x). Il simbolo _ non dichiara un nome e può essere riutilizzato.

Il programma

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

Poi: Generici e valori funzione: una forma sola che funziona per tanti tipi, e le funzioni passate in giro come valori. Torna all’elenco dei capitoli. ← Prima: Il flusso di controllo

Esegui un programma tuo: bastano quattro comandi. Oppure prendi tutto il linguaggio in un file solo, e tutti i programmi già scritti.