Lezione 4 di 7 · 20 min di lettura

Valori, variabili e tipi

Dare un nome ai valori con let, capire perché in Gleam niente cambia mai, e conoscere Int, Float, String e Bool.

Un nome per un valore

Un programma lavora con dei valori: numeri, testi, risposte sì/no. Per non doverli riscrivere ogni volta, gli si dà un nome con let:

src/variables.gleam
import gleam/io

pub fn main() -> Nil {
  let age = 34
  let name = "Ada"
  echo age
  echo name
  io.println("Done")
}

Crea questo file dentro il progetto exercises della lezione precedente ed eseguilo con gleam run -m variables:

output
src/variables.gleam:6
34
src/variables.gleam:7
"Ada"
Done

let age = 34 si legge “sia age il valore 34”. Da quella riga in poi, scrivere age è come scrivere 34. Il nome si chiama variabile (anche se, come vedremo tra poco, in Gleam non varia affatto).

echo: la lente d’ingrandimento

echo è una parola del linguaggio che stampa qualsiasi valore, di qualsiasi tipo, preceduto dal file e dalla riga in cui l’hai scritto. È pensato per il debug, cioè per sbirciare dentro il programma mentre lo scrivi: nota che le stringhe compaiono tra virgolette, "Ada", così vedi esattamente cosa contengono.

io.println invece è per l’output “vero” del programma, e accetta solo testo. Useremo echo in questa lezione per esplorare i tipi, e poi vedremo come stampare un numero con io.println.

Niente cambia, mai

Ecco la prima grande differenza rispetto a molti altri linguaggi: in Gleam un valore non si può modificare. Si dice che i valori sono immutabili. Se provi ad assegnare un nuovo valore a una variabile esistente, il compilatore ti ferma:

gleam
  let score = 10
  score = 15
output
error: Syntax error
  ┌─ /home/ada/learn-gleam/exercises/src/variables.gleam:5:9
  │
5 │   score = 15
  │         ^ There must be a 'let' to bind variable to value

See: https://tour.gleam.run/basics/assignments/

Hint: Use let for binding.

Il compilatore suggerisce di usare let. E infatti puoi scrivere un nuovo let con lo stesso nome:

src/rebind.gleam
import gleam/io

pub fn main() -> Nil {
  let score = 10
  echo score
  let score = score + 5
  echo score
  io.println("Done")
}
output
src/rebind.gleam:5
10
src/rebind.gleam:7
15
Done

Sembra una modifica, ma non lo è. La seconda riga let crea un valore nuovo (15) e gli dà il nome score, che da lì in poi indica quello. Il 10 non è stato toccato: semplicemente il nome è passato a un altro valore. Si dice che il secondo score oscura il primo.

La differenza si vede quando qualcun altro stava guardando il valore vecchio:

src/shadow.gleam
import gleam/io

pub fn main() -> Nil {
  let x = "Original"
  let y = x
  let x = "New"
  io.println(x)
  io.println(y)
}
output
New
Original

y era stato legato al valore "Original", e quel valore non è cambiato: il nome x ora indica un altro valore, ma y no.

Ogni valore ha un tipo

Ogni valore in Gleam ha un tipo, che dice che genere di dato è e cosa ci si può fare. Non devi quasi mai scriverlo: il compilatore lo deduce da solo guardando il valore. 34 è un Int, "Ada" è una String, quindi age è un Int e name una String.

Se vuoi, puoi comunque scriverlo esplicitamente, con un’annotazione di tipo dopo il nome:

gleam
  let age: Int = 34
  let name: String = "Ada"

L’annotazione non cambia niente nel programma: è una promessa, e il compilatore controlla che sia mantenuta. Se non lo è, si ferma:

gleam
  let price: Float = 3
output
error: Type mismatch
  ┌─ /home/ada/learn-gleam/exercises/src/variables.gleam:4:22
  │
4 │   let price: Float = 3
  │                      ^

Expected type:

    Float

Found type:

    Int

Nel codice Gleam di tutti i giorni le annotazioni sui let si usano poco: il compilatore sa già tutto. Le vedrai invece sempre sulle funzioni, come il -> Nil di main.

I tipi fondamentali sono quattro, più uno speciale. Vediamoli uno per uno.

Int: i numeri interi

Int è il tipo dei numeri interi, positivi o negativi: 0, 42, -7. Per rendere leggibili i numeri lunghi puoi separare le cifre con il trattino basso, che il compilatore ignora: 1_000_000 è un milione. E se ti servono, ci sono anche le basi 2, 8 e 16:

Scritto cosìBaseVale
1_000_00010un milione
0b10102 (binario)10
0o178 (ottale)15
0xFF16 (esadecimale)255

Sulla BEAM gli Int non hanno un limite di grandezza: crescono quanto serve.

src/big.gleam
import gleam/io

pub fn main() -> Nil {
  echo 12_345_678_901_234_567_890 * 98_765_432_109_876_543_210
  io.println("Done")
}
output
src/big.gleam:4
1219326311370217952237463801111263526900
Done
Dettagli nerd Perché sulla BEAM gli interi non traboccano?

Il processore lavora con numeri di dimensione fissa, di solito 64 bit: il più grande intero che ci sta è circa 9,2 miliardi di miliardi. In molti linguaggi, superare quel limite fa “traboccare” il numero, che ricomincia dal negativo o fa andare in crash il programma.

La BEAM invece usa gli interi del processore finché bastano, e quando un numero diventa troppo grande passa da sola a una rappresentazione a precisione arbitraria (in gergo bignum): il numero viene spezzato in tanti pezzi da 64 bit messi in fila, e le operazioni lavorano pezzo per pezzo. È più lento, ma il risultato è sempre giusto. Il limite diventa solo la memoria del computer.

Attenzione però: se compili per JavaScript, gli Int diventano numeri di JavaScript, precisi solo fino a circa 9 milioni di miliardi (2⁵³). Il compilatore ti avvisa se scrivi un letterale più grande.

Float: i numeri con la virgola

Float è il tipo dei numeri decimali. In Gleam, come in inglese, la virgola si scrive con il punto: 3.14, -0.5, 2.0. E il punto è obbligatorio: 2 è un Int, 2.0 è un Float, e per Gleam sono due tipi diversi che non si mescolano mai da soli.

Per i numeri molto grandi o molto piccoli c’è la notazione scientifica: 1.5e3 significa 1,5 × 10³, cioè 1500.0, e 3.0e-4 è 0.0003.

Dettagli nerd Come si scrive 3,14 con solo 0 e 1?

Un Float occupa 64 bit ed è codificato secondo lo standard IEEE 754, lo stesso usato da quasi tutti i processori. L’idea è quella della notazione scientifica, ma in base 2: il numero viene scritto come segno × mantissa × 2^esponente. Un bit per il segno, 11 bit per l’esponente, 52 bit per le cifre significative (la mantissa).

Il prezzo da pagare: molti numeri che in base 10 sono “tondi”, come 0,1, in base 2 hanno infinite cifre, proprio come 1/3 in base 10 è 0,333… Il computer tiene le prime 52 cifre binarie e butta il resto, quindi 0,1 viene memorizzato come un valore vicinissimo a 0,1, ma non esattamente 0,1. Lo toccheremo con mano nella prossima lezione.

Bool: vero o falso

Bool ha solo due valori: True (vero) e False (falso). Con la maiuscola: in Gleam true minuscolo non esiste. Li userai ovunque ci sia da prendere una decisione, e nella prossima lezione vedremo come ottenerli dai confronti.

gleam
  let is_ready = True
  let is_raining = False

String: il testo

String è il tipo del testo, scritto tra virgolette doppie. Una stringa può contenere qualsiasi carattere: lettere accentate, alfabeti diversi, perfino emoji. Due stringhe si attaccano con l’operatore <>:

gleam
  let first = "Ada"
  let full = first <> " Lovelace"

Ora full vale "Ada Lovelace". Attenzione allo spazio: <> attacca e basta, lo spazio tra le due parole l’abbiamo messo noi dentro la seconda stringa.

Per lavorare sulle stringhe c’è il modulo gleam/string della libreria standard. Per esempio string.length conta i caratteri:

src/strlen.gleam
import gleam/io
import gleam/string

pub fn main() -> Nil {
  let drink = "caffè"
  echo string.length(drink)
  echo string.byte_size(drink)
  io.println("Done")
}
output
src/strlen.gleam:6
5
src/strlen.gleam:7
6
Done

Cinque caratteri, ma sei byte. Come mai?

Dettagli nerd Byte, caratteri e UTF-8

La memoria del computer è fatta di byte: gruppi di 8 bit, ognuno capace di contenere un numero da 0 a 255. Il testo va quindi trasformato in numeri, e il modo standard per farlo oggi è UTF-8.

In UTF-8 le lettere dell’alfabeto inglese, le cifre e la punteggiatura comune occupano un byte ciascuna. Le lettere accentate ne occupano due, molti simboli e ideogrammi tre, le emoji quattro. Le stringhe di Gleam sono sempre in UTF-8: "caffè" ha 5 caratteri, ma la è pesa 2 byte, per un totale di 6.

string.length conta i caratteri come li vede una persona; string.byte_size conta i byte in memoria. Quasi sempre ti interessa il primo. Ma se un giorno una lunghezza “non torna” con del testo accentato, adesso sai dove guardare.

Nil: il niente

L’hai già incontrato in -> Nil: Nil è sia un tipo sia il suo unico valore, e significa “niente di interessante”. È quello che restituiscono le funzioni che fanno qualcosa (come stampare) invece di calcolare un risultato. Non lo userai quasi mai direttamente, ma ora sai cos’è quando lo vedi.

Quiz

Quale di queste righe non compila?

Le regole dei nomi

Gleam è rigido sui nomi, e il compilatore ti corregge se sbagli. Le regole sono poche:

  • Variabili, funzioni e costanti sono in snake_case: minuscole, cifre e trattini bassi, e iniziano con una lettera. age, total_price, player_2.
  • Tipi sono in PascalCase: ogni parola con la maiuscola, niente trattini. Int, String, Bool, e i tipi che creerai tu.
  • Moduli sono in snake_case, come i file che li contengono.

Se scrivi un nome nello stile sbagliato, non è solo una questione di gusto: non compila.

gleam
  let myName = "Ada"
output
error: Invalid variable name
  ┌─ /home/ada/learn-gleam/exercises/src/variables.gleam:4:7
  │
4 │   let myName = "Ada"
  │       ^^^^^^ This is not a valid variable name

Hint: Variable names start with a lowercase letter and contain a-z, 0-9, or
_.
Try: my_name

C’è anche una regola di buona educazione, che il compilatore fa rispettare con un avvertimento (warning): se crei una variabile e poi non la usi, te lo fa notare.

output
warning: Unused variable
  ┌─ /home/ada/learn-gleam/exercises/src/unused.gleam:4:7
  │
4 │   let score = 1000
  │       ^^^^^ This variable is never used

Hint: You can ignore it with an underscore: `_score`.

Un avvertimento non blocca il programma, che parte lo stesso. Ma di solito segnala una svista: ti sei dimenticato di usare qualcosa. Se invece lo fai apposta, il suggerimento ti dice come zittirlo: un nome che inizia con _, come _score, dice “lo so che non lo uso”.

Costanti: valori con un nome per tutto il modulo

Un let vive dentro una funzione. Se un valore serve in tutto il modulo (un’aliquota, un limite, un nome fisso), lo puoi definire fuori da ogni funzione con const:

gleam
const max_players = 4

const vat_rate = 0.22

Le costanti vanno in cima al file, dopo gli import. Hanno le stesse regole dei nomi delle variabili: snake_case, non in maiuscolo come in molti altri linguaggi.

output
error: Syntax error
  ┌─ /home/ada/learn-gleam/exercises/src/variables.gleam:3:7
  │
3 │ const MAX = 3
  │       ^^^ I'm expecting a lowercase name here

A variable name must start with a lowercase letter, and can contain a-z,
0-9, or _.

Il valore di una costante deve essere scritto direttamente (un numero, un testo, True…): non puoi calcolarlo chiamando una funzione. Se vuoi che anche altri moduli la vedano, scrivi pub const.

Mettiamo tutto insieme

Manca un ultimo pezzo: come si stampa un numero con io.println, che accetta solo String? In Gleam niente si converte da solo, quindi lo converti tu, con le funzioni dei moduli della libreria standard dedicati a ciascun tipo:

DaFunzioneEsempio
Intint.to_string (modulo gleam/int)int.to_string(42) → "42"
Floatfloat.to_string (modulo gleam/float)float.to_string(12.5) → "12.5"
Boolbool.to_string (modulo gleam/bool)bool.to_string(True) → "True"

Ecco una scheda completa, con una costante, variabili di ogni tipo e un compleanno festeggiato con un nuovo let:

src/pet.gleam
import gleam/bool
import gleam/float
import gleam/int
import gleam/io

const species = "Cat"

pub fn main() -> Nil {
  let name = "Tom"
  let age = 7
  let weight = 4.5
  let indoor = True

  // Happy birthday!
  let age = age + 1

  io.println("Name: " <> name)
  io.println("Species: " <> species)
  io.println("Age: " <> int.to_string(age))
  io.println("Weight: " <> float.to_string(weight) <> " kg")
  io.println("Indoor: " <> bool.to_string(indoor))
}
output
Name: Tom
Species: Cat
Age: 8
Weight: 4.5 kg
Indoor: True

Esercizio · sul tuo computer

La tua scheda

Nel progetto exercises crea il modulo src/card.gleam, che stampi esattamente:

output
Name: Rex
Species: Dog
Age: 4
Weight: 12.5 kg
Vaccinated: True

Regole del gioco:

  • la specie deve essere una costante species;
  • l’età deve partire da 3 e diventare 4 con un secondo let (il compleanno);
  • il peso deve essere un Float e la vaccinazione un Bool, convertiti in testo con le funzioni giuste.

Eseguilo con gleam run -m card e copia qui sotto l’output.

Mostra una soluzione (prima prova da solo!)
src/card.gleam
import gleam/bool
import gleam/float
import gleam/int
import gleam/io

const species = "Dog"

pub fn main() -> Nil {
  let name = "Rex"
  let age = 3
  let weight = 12.5
  let vaccinated = True

  // Happy birthday!
  let age = age + 1

  io.println("Name: " <> name)
  io.println("Species: " <> species)
  io.println("Age: " <> int.to_string(age))
  io.println("Weight: " <> float.to_string(weight) <> " kg")
  io.println("Vaccinated: " <> bool.to_string(vaccinated))
}

Ricapitolando

  • let nome = valore dà un nome a un valore; il tipo viene dedotto, ma puoi annotarlo: let age: Int = 34.
  • I valori sono immutabili: non si riassegna, si crea un nuovo let con lo stesso nome.
  • echo stampa qualsiasi valore per il debug (e lo restituisce: occhio all’ultima riga di main).
  • Int per gli interi (senza limiti sulla BEAM), Float per i decimali (sempre con il punto), Bool per True/False, String per il testo, Nil per il niente.
  • Le stringhe si attaccano con <>; string.length conta i caratteri.
  • Nomi in snake_case, tipi in PascalCase; _ davanti a un nome zittisce l’avvertimento “non usato”.
  • const definisce un valore per tutto il modulo, sempre in snake_case.
  • Niente conversioni automatiche: int.to_string, float.to_string, bool.to_string.

Nella prossima lezione mettiamo i numeri al lavoro: operatori, confronti, logica, e una sorpresa sulla divisione per zero.