Lezione 1 di 8 · 20 min di lettura
Le tue funzioni
Dare un nome a un pezzo di programma e riusarlo. Parametri, tipi, funzioni private e pubbliche, e lo scontrino che si accorcia di metà.
Lo stesso codice, tre volte
Ricordi lo scontrino della lezione 1.6? Per trasformare tre importi in testo abbiamo scritto tre volte la stessa formula, cambiando solo il nome della variabile:
let coffee_text =
int.to_string(coffee / 100)
<> "."
<> { coffee % 100 |> int.to_string |> string.pad_start(2, "0") }Copiare e incollare funziona, ma ha due difetti. Il primo è la noia. Il secondo è peggio: se un giorno scopri un errore nella formula, devi correggerlo in tre punti, e prima o poi uno lo dimentichi. La soluzione è scrivere la formula una volta sola, darle un nome e chiamarla quando serve. È esattamente quello che fa una funzione.
La prima funzione tua
Finora hai scritto una sola funzione, main, e ne hai chiamate tante scritte da altri: io.println, int.to_string, string.pad_start. Scriverne una nuova è identico a scrivere main, solo con un altro nome:
import gleam/io
pub fn main() -> Nil {
io.println(greet("Ada"))
io.println(greet("Grace"))
}
fn greet(name: String) -> String {
"Hello, " <> name <> "!"
}Hello, Ada!
Hello, Grace!Leggiamo la riga che definisce greet, pezzo per pezzo:
| Pezzo | Significato |
|---|---|
fn | “Sto per definire una funzione.” |
greet | Il nome, in snake_case come le variabili. |
(name: String) | Il parametro: la funzione riceve un valore, lo chiama name, e deve essere una String. |
-> String | Il tipo di ritorno: la funzione restituisce una String. |
{ ... } | Il corpo. Come in main, il valore dell’ultima espressione è il risultato. |
Quando main esegue greet("Ada"), succede questo: name diventa "Ada", il corpo calcola "Hello, Ada!", e quel valore prende il posto della chiamata. io.println(greet("Ada")) diventa io.println("Hello, Ada!").
Il valore che passi nella chiamata ("Ada") si chiama argomento; il nome che lo riceve dentro la funzione (name) si chiama parametro. Sono le due facce della stessa porta.
Più parametri
Una funzione può ricevere quanti valori vuoi, separati da virgole. L’ordine conta: il primo argomento va nel primo parametro, il secondo nel secondo.
import gleam/int
import gleam/io
pub fn main() -> Nil {
io.println("Area: " <> int.to_string(area(3, 4)))
io.println("Perimeter: " <> int.to_string(perimeter(3, 4)))
io.println("Twice the area: " <> int.to_string(2 * area(3, 4)))
}
fn area(width: Int, height: Int) -> Int {
width * height
}
fn perimeter(width: Int, height: Int) -> Int {
2 * { width + height }
}Area: 12
Perimeter: 14
Twice the area: 24Una chiamata di funzione è un’espressione come le altre: puoi metterla dentro un calcolo (2 * area(3, 4)), dentro un’altra chiamata, in un let. E una funzione può chiamarne un’altra: main chiama area, ma anche area potrebbe chiamare altre funzioni tue.
Nota anche i nomi: nella chiamata area(3, 4) non c’è scritto quale numero è la larghezza. Lo decide solo la posizione. Nella prossima lezione vedremo come rendere le chiamate più leggibili con le etichette.
Quiz
Con le funzioni qui sopra, quanto vale area(perimeter(1, 1), 2)?
Il compilatore controlla ogni chiamata
I tipi scritti nella definizione sono un contratto, e il compilatore lo fa rispettare a chiunque chiami la funzione. Se sbagli il numero di argomenti:
import gleam/io
pub fn main() -> Nil {
io.println(greet("Ada", "Grace"))
}
fn greet(name: String) -> String {
"Hello, " <> name <> "!"
}error: Incorrect arity
┌─ /home/ada/learn-gleam/exercises/src/greetings.gleam:4:14
│
4 │ io.println(greet("Ada", "Grace"))
│ ^^^^^^^^^^^^^^^^^^^^^ Expected 1 argument, got 2L’arità è il numero di parametri di una funzione: greet ne vuole uno, ne ha ricevuti due. Se invece sbagli il tipo, per esempio greet(42), ottieni il Type mismatch che conosci già: Expected type: String, Found type: Int.
Il contratto vale anche nell’altro senso: la funzione deve restituire quello che promette. Qui greet promette un Int, ma costruisce una String:
import gleam/io
pub fn main() -> Nil {
echo greet("Ada")
io.println("Done")
}
fn greet(name: String) -> Int {
"Hello, " <> name <> "!"
}error: Type mismatch
┌─ /home/ada/learn-gleam/exercises/src/greetings.gleam:9:3
│
9 │ "Hello, " <> name <> "!"
│ ^^^^^^^^^^^^^^^^^^^^^^^^
The type of this returned value doesn't match the return type
annotation of this function.
Expected type:
Int
Found type:
StringQuello che succede nella funzione resta nella funzione
Le variabili create dentro una funzione esistono solo lì dentro. Da fuori non si vedono:
import gleam/io
pub fn main() -> Nil {
io.println(greet("Ada"))
io.println(text)
}
fn greet(name: String) -> String {
let text = "Hello, " <> name <> "!"
text
}error: Unknown variable
┌─ /home/ada/learn-gleam/exercises/src/scope.gleam:5:14
│
5 │ io.println(text)
│ ^^^^
The name `text` is not in scope here.La zona di codice in cui un nome esiste si chiama scope (portata). Lo scope di text è il corpo di greet; lo stesso vale per il parametro name. È una garanzia preziosa: una funzione comunica con il resto del programma solo attraverso i suoi argomenti e il suo risultato. Per capire cosa fa, ti basta leggere lei.
pub, o non pub
main ha pub davanti, greet no. Una funzione senza pub è privata: si può usare solo nel modulo in cui è scritta. Con pub diventa pubblica, e altri moduli la possono chiamare dopo averla importata, come tu chiami string.pad_start dopo import gleam/string.
La regola pratica: tutto privato, tranne quello che deve servire fuori. main deve essere pub perché la chiama gleam run; le funzioni di supporto di un esercizio restano private.
Una funzione privata che nessuno chiama è quindi codice morto, e il compilatore te lo fa notare:
import gleam/io
pub fn main() -> Nil {
io.println(greet("Ada"))
}
fn greet(name: String) -> String {
"Hello, " <> name <> "!"
}
fn shout(name: String) -> String {
name <> "!!!"
}warning: Unused private function
┌─ /home/ada/learn-gleam/exercises/src/greetings.gleam:11:1
│
11 │ fn shout(name: String) -> String {
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This private function is never used
Hint: You can safely remove it.
Hello, Ada!È solo un avviso (warning): il programma parte lo stesso, e infatti sotto l’avviso c’è il saluto. Ma quasi sempre significa che hai dimenticato di chiamare qualcosa, o che puoi cancellarla.
Quiz
Quale di queste affermazioni è vera?
Lo scontrino, finalmente corto
Ora possiamo mantenere la promessa della lezione 1.6. La formula dei centesimi diventa una funzione, format_cents, e anche la riga dello scontrino, che era sempre uguale tranne etichetta e importo, diventa una funzione:
import gleam/int
import gleam/io
import gleam/string
pub fn main() -> Nil {
let coffee = 120 * 2
let croissant = 130 * 3
let total = coffee + croissant
io.println(receipt_line("Coffee x2", coffee))
io.println(receipt_line("Croissant x3", croissant))
io.println(string.repeat("-", 24))
io.println(receipt_line("Total", total))
}
/// Formats an amount in cents as euros with two decimals: 690 -> "6.90".
fn format_cents(cents: Int) -> String {
int.to_string(cents / 100)
<> "."
<> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}
/// A receipt line, 24 characters wide: label, dots, amount.
fn receipt_line(label: String, cents: Int) -> String {
string.pad_end(label <> " ", 19, ".")
<> string.pad_start(format_cents(cents), 5, " ")
}Coffee x2 ......... 2.40
Croissant x3 ...... 3.90
------------------------
Total ............. 6.30Stesso output, ma main ora si legge come una descrizione dello scontrino, e i dettagli di formattazione stanno ognuno nella sua funzione, con un nome che dice cosa fa. Se domani vuoi la virgola al posto del punto, la cambi in un posto.
Nota i commenti /// sopra le due funzioni: sono i commenti di documentazione della lezione 1.2. Descrivono cosa fa la funzione, non come lo fa, e gli editor li mostrano quando passi il mouse su una chiamata.
Esercizio · sul tuo computer
L'orologio digitale
Nel progetto exercises crea src/clock.gleam con due funzioni oltre a main:
two_digits(n: Int) -> String, che trasforma un numero in testo di almeno due cifre, con uno zero davanti se serve:two_digits(7)vale"07",two_digits(45)vale"45";clock(seconds: Int) -> String, che trasforma un numero di secondi nel formatoHH:MM:SS, usandotwo_digitsper ogni pezzo.
In main stampa clock(7384), clock(45) e clock(86_399). L’output deve essere:
02:03:04
00:00:45
23:59:59Il calcolo di ore, minuti e secondi è lo stesso dell’esercizio della lezione 1.5: / 3600, poi % 3600 / 60, poi % 60.
Mostra una soluzione (prima prova da solo!)
import gleam/int
import gleam/io
import gleam/string
pub fn main() -> Nil {
io.println(clock(7384))
io.println(clock(45))
io.println(clock(86_399))
}
fn two_digits(n: Int) -> String {
n |> int.to_string |> string.pad_start(2, "0")
}
fn clock(seconds: Int) -> String {
two_digits(seconds / 3600)
<> ":"
<> two_digits(seconds % 3600 / 60)
<> ":"
<> two_digits(seconds % 60)
}Nota come clock si legge quasi come la sua descrizione: ore, due punti, minuti, due punti, secondi. Tutti i dettagli sulle cifre stanno in two_digits.
Ricapitolando
- Una funzione si definisce con
fn nome(parametro: Tipo) -> TipoDiRitorno { corpo }, e restituisce la sua ultima espressione. - Gli argomenti della chiamata finiscono nei parametri della definizione, in ordine di posizione.
- Il compilatore controlla ogni chiamata: numero di argomenti (
Incorrect arity), tipi degli argomenti e tipo del risultato. - Le annotazioni di tipo sono facoltative, ma sulle funzioni conviene scriverle sempre.
- Le variabili di una funzione (parametri compresi) esistono solo nel suo scope.
- Senza
pubuna funzione è privata del modulo; se nessuno la usa, il compilatore te lo segnala. ///sopra una funzione la documenta.
Nella prossima lezione rendiamo le chiamate più leggibili con le etichette, e facciamo passare le nostre funzioni nel tubo |>.