Lezione 3 di 8 · 18 min di lettura
Funzioni come valori
Una funzione si può mettere in una variabile, passare a un'altra funzione, creare al volo e perfino restituire. E il trattino basso che ne costruisce una in un colpo.
Una funzione è un valore
Fin qui abbiamo trattato le funzioni come “comandi”: le definisci, le chiami, finito. Ma in Gleam una funzione è un valore, esattamente come 42 o "Ada". La puoi mettere in una variabile, e poi chiamare la variabile:
import gleam/io
pub fn main() -> Nil {
let f = double
echo f(21)
echo double
io.println("Done")
}
fn double(x: Int) -> Int {
x * 2
}src/values.gleam:5
42
src/values.gleam:6
//fn(a) { ... }
DoneNota la differenza tra double e double(21). Con le parentesi chiami la funzione e ottieni il suo risultato; senza parentesi parli della funzione stessa, come oggetto. echo double non sa bene come mostrarla, e stampa //fn(a) { ... }: “qui c’è una funzione con un argomento”.
Anche le funzioni hanno un tipo
Se una funzione è un valore, deve avere un tipo. Il tipo di double si scrive:
fn(Int) -> Int“Una funzione che riceve un Int e restituisce un Int.” È la riga di definizione senza i nomi. greet della lezione 1 ha tipo fn(String) -> String, area ha tipo fn(Int, Int) -> Int, e main ha tipo fn() -> Nil.
Con questo tipo possiamo scrivere una funzione che riceve un’altra funzione come argomento:
import gleam/io
pub fn main() -> Nil {
echo twice(5, double)
echo twice(5, add_three)
io.println("Done")
}
fn twice(value: Int, f: fn(Int) -> Int) -> Int {
f(f(value))
}
fn double(x: Int) -> Int {
x * 2
}
fn add_three(x: Int) -> Int {
x + 3
}src/twice.gleam:4
20
src/twice.gleam:5
11
Donetwice non sa cosa farà f: sa solo che riceve un Int e restituisce un Int, e la applica due volte. Con double, 5 diventa 10 e poi 20; con add_three, 8 e poi 11. Una funzione che riceve (o restituisce) altre funzioni si chiama funzione di ordine superiore, un nome altisonante per un’idea semplice: separare il “come si ripete” (lo decide twice) dal “cosa si fa” (lo decide chi la chiama).
Il compilatore controlla anche qui: se passi a twice una funzione del tipo sbagliato, per esempio string.uppercase, ti ferma.
import gleam/io
import gleam/string
pub fn main() -> Nil {
echo twice(5, string.uppercase)
io.println("Done")
}
fn twice(value: Int, f: fn(Int) -> Int) -> Int {
f(f(value))
}error: Type mismatch
┌─ /home/ada/learn-gleam/exercises/src/twice.gleam:5:17
│
5 │ echo twice(5, string.uppercase)
│ ^^^^^^^^^^^^^^^^
Expected type:
fn(Int) -> Int
Found type:
fn(String) -> StringFunzioni senza nome
Definire una funzione con un nome solo per passarla una volta a twice è un po’ eccessivo. Gleam permette di crearla al volo, nel punto in cui serve, con fn senza nome:
twice(5, fn(x) { x * 10 })fn(x) { x * 10 } è una funzione anonima: stessa forma di una funzione normale, ma senza nome e senza pub. Vale 500. Le annotazioni di tipo sono facoltative anche qui, e di solito non si scrivono: il compilatore sa già che twice vuole un fn(Int) -> Int, quindi x è un Int.
Una funzione anonima si può anche mettere in una variabile, che diventa di fatto una funzione locale:
let exclaim = fn(text: String) -> String { text <> "!" }
exclaim("Hi")Funzioni che si ricordano
Una funzione anonima può usare le variabili che esistono nel punto in cui viene creata, e se le porta dietro. Guarda adder, una funzione che restituisce una funzione:
import gleam/io
pub fn main() -> Nil {
let add_ten = adder(10)
let add_one = adder(1)
echo add_ten(5)
echo add_one(5)
io.println("Done")
}
fn adder(amount: Int) -> fn(Int) -> Int {
fn(x) { x + amount }
}src/adders.gleam:6
15
src/adders.gleam:7
6
DoneIl tipo di ritorno di adder è fn(Int) -> Int: restituisce una funzione. Quando chiami adder(10), la funzione anonima viene creata in un momento in cui amount vale 10, e se lo ricorda anche dopo che adder ha finito. add_one è un’altra funzione, creata con amount uguale a 1. Una funzione che si porta dietro le variabili del posto in cui è nata si chiama closure (chiusura).
Dettagli nerd Come fa una funzione a ricordarsi una variabile?
Il codice di fn(x) { x + amount } esiste una volta sola, dentro il programma compilato. Ma quando la BEAM la crea come valore, non salva solo “dov’è il codice”: costruisce in memoria una piccola struttura con un riferimento al codice più una copia dei valori che quel codice usa da fuori, qui amount. add_ten e add_one sono due di queste strutture: stesso codice, amount diverso.
Siccome in Gleam i valori non cambiano mai, la copia non può “andare fuori sincrono” con l’originale. In linguaggi dove le variabili si modificano, le closure sono una fonte classica di sorprese; qui no.
Quiz
Con adder e twice di questa lezione, quanto vale twice(1, adder(5))?
La cattura: il trattino basso
Molto spesso la funzione anonima che ti serve non fa altro che chiamare un’altra funzione, con un argomento che manca:
fn(x) { add_vat(x, rate: 22) }Per questo caso Gleam ha una scorciatoia, la cattura (function capture): scrivi la chiamata normale e metti un trattino basso _ al posto dell’argomento mancante.
add_vat(_, rate: 22)Le due righe sono identiche: una funzione che riceve un valore e lo mette al posto del _. Il trattino basso può stare in qualsiasi posizione, e ce n’è sempre uno solo.
La cattura risolve anche un problema del tubo. |> mette il valore sempre al primo posto; se ti serve altrove, cattura:
import gleam/io
import gleam/string
pub fn main() -> Nil {
let suffix = "!"
"Hello"
|> string.append(suffix)
|> io.println
let prefix = "> "
"Hello"
|> string.append(prefix, _)
|> io.println
}Hello!
> Hellostring.append(a, b) attacca b in fondo ad a. Nel primo tubo "Hello" entra come primo argomento, e il punto esclamativo finisce dopo. Nel secondo, grazie al _, "Hello" entra come secondo argomento, e il prefisso finisce davanti.
Funzioni che vanno bene per tutto
La nostra twice funziona solo con gli Int. Ma “applica due volte una funzione” ha senso per qualsiasi tipo. Gleam permette di scrivere un tipo “qualsiasi” con un nome minuscolo, una variabile di tipo:
import gleam/io
pub fn main() -> Nil {
echo twice(5, fn(x) { x * 10 })
echo twice("Hi", fn(text) { text <> "!" })
io.println("Done")
}
fn twice(value: a, f: fn(a) -> a) -> a {
f(f(value))
}src/generic.gleam:4
500
src/generic.gleam:5
"Hi!!"
Donea sta per “un tipo qualsiasi, ma lo stesso ovunque compaia”. Nella prima chiamata a diventa Int, nella seconda String. Non è un modo per spegnere il controllo dei tipi: se provi twice(10, fn(x) { x <> "!" }), il valore è un Int ma la funzione restituisce una String, le due a non possono essere lo stesso tipo, e il compilatore ti ferma. Queste funzioni si chiamano generiche; le ritroverai spesso nella libreria standard, e le useremo molto quando arriveranno le liste.
Esercizio · sul tuo computer
Le promozioni
Nel progetto exercises crea src/promos.gleam. Un negozio applica promozioni diverse ai prezzi, e ogni promozione è una funzione fn(Int) -> Int che riceve un prezzo in centesimi e restituisce il prezzo scontato. Scrivi:
checkout(cents: Int, promo: fn(Int) -> Int) -> String, che applica la promozione al prezzo e restituisce"To pay: "seguito dall’importo formattato (riusaformat_cents);no_promo(cents: Int) -> Int, che restituisce il prezzo così com’è;percent_off(cents: Int, percent: Int) -> Int, che toglie una percentuale.
In main stampa il checkout di un prezzo di 4990 centesimi tre volte: con no_promo, con una funzione anonima che toglie 5 euro fissi, e con una cattura di percent_off al 10%. L’output deve essere:
To pay: 49.90
To pay: 44.90
To pay: 44.91Mostra una soluzione (prima prova da solo!)
import gleam/int
import gleam/io
import gleam/string
pub fn main() -> Nil {
io.println(checkout(4990, no_promo))
io.println(checkout(4990, fn(cents) { cents - 500 }))
io.println(checkout(4990, percent_off(_, 10)))
}
fn checkout(cents: Int, promo: fn(Int) -> Int) -> String {
"To pay: " <> format_cents(promo(cents))
}
fn no_promo(cents: Int) -> Int {
cents
}
fn percent_off(cents: Int, percent: Int) -> Int {
cents - cents * percent / 100
}
fn format_cents(cents: Int) -> String {
int.to_string(cents / 100)
<> "."
<> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}Tre modi di scrivere un fn(Int) -> Int: una funzione con nome, una anonima, una cattura. Per checkout sono tutte uguali. Il 10% di 4990 è 499, quindi l’ultima riga è 44,91.
Ricapitolando
- Una funzione è un valore:
doubleè la funzione,double(21)è il suo risultato. - Il tipo di una funzione si scrive
fn(Int, String) -> Bool: argomenti e risultato, senza nomi. - Una funzione può ricevere funzioni come argomenti e restituirne: è una funzione di ordine superiore.
fn(x) { x + 1 }è una funzione anonima, da creare al volo dove serve.- Una funzione anonima si ricorda le variabili del posto in cui è nata: è una closure.
- La cattura
f(_, 22)è la scorciatoia perfn(x) { f(x, 22) }; con il tubo, porta il valore in una posizione diversa dalla prima. - Un nome di tipo minuscolo (
a) è una variabile di tipo: la funzione diventa generica, ma ogniaè sempre lo stesso tipo.
Adesso sappiamo scrivere funzioni di ogni forma, ma vanno ancora sempre dritte, dalla prima riga all’ultima. Nella prossima lezione impareremo a far prendere decisioni al programma.