Lezione 2 di 8 · 16 min di lettura

Etichette e tubi

Chiamate che si spiegano da sole con gli argomenti etichettati, e funzioni scritte apposta per stare in fila nel tubo |>.

Quale numero è quale?

Guarda questa chiamata della lezione 1.6:

gleam
  string.pad_start("7", 3, "0")

Il 3 è la lunghezza finale o quante volte ripetere lo zero? E lo "0" è il riempitivo o il testo da allungare? Senza aprire la documentazione non lo sai. Con le nostre funzioni è uguale: in area(3, 4) qual è la larghezza? Quando una funzione riceve più valori, soprattutto se hanno lo stesso tipo, la sola posizione non basta a capire.

Argomenti con l’etichetta

Gleam permette di dare a un parametro un’etichetta (label): un nome esterno, da usare nelle chiamate, oltre a quello interno da usare nel corpo. Si scrive prima del nome interno:

src/labels.gleam
import gleam/io

pub fn main() -> Nil {
  echo area(width: 3, height: 4)
  echo area(height: 4, width: 3)
  echo area(3, 4)
  echo area(3, height: 4)
  io.println("Done")
}

fn area(width w: Int, height h: Int) -> Int {
  w * h
}
output
src/labels.gleam:4
12
src/labels.gleam:5
12
src/labels.gleam:6
12
src/labels.gleam:7
12
Done

In width w: Int, width è l’etichetta e w è il nome del parametro. Fuori dalla funzione si usa l’etichetta, dentro si usa il nome: nel corpo width non esiste, e se lo scrivi il compilatore risponde con un Unknown variable.

Guarda le quattro chiamate:

  • con le etichette la chiamata si legge da sola: area(width: 3, height: 4);
  • con le etichette l’ordine non conta più: la seconda chiamata le scambia, e il risultato è lo stesso;
  • le etichette sono facoltative: area(3, 4) funziona ancora, per posizione;
  • si possono mescolare, ma gli argomenti senza etichetta vanno prima di quelli con l’etichetta.

Molto spesso etichetta e nome coincidono, e si scrive due volte la stessa parola: fn area(width width: Int, height height: Int). Sembra strano la prima volta, ma è la forma più comune.

Quando il compilatore ti corregge

Se sbagli a scrivere un’etichetta, il compilatore ti dice quali esistono, e prova anche a indovinare cosa intendevi:

src/labels.gleam
import gleam/io

pub fn main() -> Nil {
  echo area(3, heigth: 4)
  io.println("Done")
}

fn area(width w: Int, height h: Int) -> Int {
  w * h
}
output
error: Unknown label
  ┌─ /home/ada/learn-gleam/exercises/src/labels.gleam:4:16
  │
4 │   echo area(3, heigth: 4)
  │                ^^^^^^^^^ Did you mean `height`?

It accepts these labels:

    height
    width

E se metti un argomento senza etichetta dopo uno con l’etichetta, come in area(height: 4, 3), ottieni un Unexpected positional argument: una volta che hai cominciato con le etichette, devi continuare.

Anche la libreria standard ha le etichette

Molte funzioni della libreria standard hanno etichette, pensate proprio per rendere le chiamate leggibili. string.pad_start è definita così (qui senza il corpo):

gleam
pub fn pad_start(
  string: String,
  to desired_length: Int,
  with pad_string: String,
) -> String

Il primo parametro non ha etichetta (è ovvio cosa sia), gli altri due sì. Quindi la chiamata misteriosa di prima si può scrivere:

gleam
  string.pad_start("7", to: 3, with: "0")

“Allunga "7" fino a 3, con degli zeri.” Allo stesso modo string.repeat("-", times: 24). Le etichette di ogni funzione si trovano nella documentazione della libreria standard, su hexdocs.pm/gleam_stdlib, e l’editor te le suggerisce mentre scrivi.

Quiz

Con fn area(width w: Int, height h: Int) -> Int, quale chiamata non compila?

La scorciatoia per le etichette

Capita spesso di avere una variabile che si chiama esattamente come l’etichetta:

gleam
  let width = 3
  let height = 4
  area(width: width, height: height)

Quel width: width è ripetitivo, e Gleam ha una scorciatoia: scrivi solo l’etichetta seguita dai due punti, e il valore è la variabile con lo stesso nome.

gleam
  area(width:, height:)

Le due chiamate sono identiche. La scorciatoia premia chi dà nomi coerenti alle cose.

Funzioni fatte per il tubo

Nella lezione 1.6 abbiamo visto che x |> f(a) equivale a f(x, a): il valore a sinistra entra come primo argomento. Funziona anche con le tue funzioni, e le etichette ci stanno benissimo:

src/checkout.gleam
import gleam/int
import gleam/io
import gleam/string

pub fn main() -> Nil {
  4990
  |> discount(percent: 20)
  |> add_vat(rate: 22)
  |> format_cents
  |> io.println
}

fn discount(cents: Int, percent percent: Int) -> Int {
  cents - cents * percent / 100
}

fn add_vat(cents: Int, rate rate: Int) -> Int {
  cents + cents * rate / 100
}

fn format_cents(cents: Int) -> String {
  int.to_string(cents / 100)
  <> "."
  <> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}
output
48.70

“Prendi 49,90 euro, togli il 20%, aggiungi l’IVA al 22%, trasforma in testo, stampa.” Il programma si legge come la frase.

Perché funzioni così bene, le funzioni sono state scritte con un’idea precisa: il primo parametro è la cosa che viene trasformata (qui l’importo), e i parametri successivi sono le “impostazioni” della trasformazione, con un’etichetta. In Gleam è una convenzione diffusa, e la segue anche la libreria standard: in string.pad_start il primo argomento è la stringa da allungare, in int.to_string il numero da convertire. Quando scrivi una funzione, chiediti: “di cosa parla questa funzione?“. Quella cosa va messa per prima.

Quiz

Cosa vale 10 |> area(height: 2), con la funzione area di questa lezione?

Esercizio · sul tuo computer

Il conto alla romana

Nel progetto exercises crea src/bill.gleam. Quattro amici hanno un conto di 8450 centesimi e vogliono lasciare il 10% di mancia, poi dividere. Scrivi:

  • add_tip(cents: Int, percent percent: Int) -> Int, che aggiunge la percentuale di mancia all’importo;
  • share(total: Int, among people: Int) -> Int, che divide il totale tra le persone (divisione intera: i centesimi che avanzano restano a parte);
  • format_cents(cents: Int) -> String, la tua versione (o quella della lezione precedente).

In main, usa let people = 4 e calcola il totale con la mancia con un tubo, poi la quota di ciascuno con share(total, among: people) e i centesimi avanzati con %. L’output deve essere:

output
Total with tip: 92.95
Each of 4 pays: 23.23
Left over: 0.03
Mostra una soluzione (prima prova da solo!)
src/bill.gleam
import gleam/int
import gleam/io
import gleam/string

pub fn main() -> Nil {
  let people = 4
  let total = 8450 |> add_tip(percent: 10)
  let each = share(total, among: people)

  io.println("Total with tip: " <> format_cents(total))
  io.println(
    "Each of " <> int.to_string(people) <> " pays: " <> format_cents(each),
  )
  io.println("Left over: " <> format_cents(total % people))
}

fn add_tip(cents: Int, percent percent: Int) -> Int {
  cents + cents * percent / 100
}

fn share(total: Int, among people: Int) -> Int {
  total / people
}

fn format_cents(cents: Int) -> String {
  int.to_string(cents / 100)
  <> "."
  <> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}

L’etichetta among rende la chiamata una frase (“dividi il totale tra le persone”), mentre dentro la funzione il parametro si chiama people. I centesimi avanzati sono il resto della stessa divisione: 9295 % 4 fa 3.

Ricapitolando

  • Un’etichetta è il nome esterno di un parametro: fn area(width w: Int) si chiama con area(width: 3), e nel corpo si usa w.
  • Con le etichette l’ordine degli argomenti è libero; senza, vale la posizione. Gli argomenti senza etichetta vanno sempre per primi.
  • Spesso etichetta e nome coincidono: fn add_vat(cents: Int, rate rate: Int).
  • Scorciatoia: area(width:, height:) usa le variabili con lo stesso nome delle etichette.
  • Molte funzioni della libreria standard hanno etichette: string.pad_start("7", to: 3, with: "0").
  • Per stare bene nel tubo, una funzione riceve per primo il valore che trasforma.
  • |> echo in mezzo a un tubo mostra il valore che passa, senza cambiarlo.

Nella prossima lezione scopriremo che le funzioni sono valori come gli altri: si possono mettere in una variabile, passare a un’altra funzione e perfino creare al volo.