Lezione 3 di 6 · 18 min di lettura
use
La parola chiave che trasforma una scala di funzioni annidate in righe dritte. Come funziona davvero, use con result.try, e le uscite anticipate con bool.guard.
La scala, di nuovo
Alla fine della lezione precedente, parse_time era così:
fn parse_time(text: String) -> Result(Int, TimeError) {
string.split_once(text, on: ":")
|> result.replace_error(MissingColon)
|> result.try(fn(parts) {
let #(hours_text, minutes_text) = parts
parse_number(hours_text)
|> result.try(fn(hours) {
parse_number(minutes_text)
|> result.try(fn(minutes) { check_range(hours, minutes) })
})
})
}Ogni result.try riceve una funzione anonima, e tutto il resto del lavoro sta dentro quella funzione. Tre passi, tre livelli di rientro. Con dieci passi, dieci livelli. Gleam ha una parola chiave fatta apposta per questa forma: use.
La stessa funzione, con use
fn parse_time(text: String) -> Result(Int, TimeError) {
use #(hours_text, minutes_text) <- result.try(
string.split_once(text, on: ":") |> result.replace_error(MissingColon),
)
use hours <- result.try(parse_number(hours_text))
use minutes <- result.try(parse_number(minutes_text))
check_range(hours, minutes)
}Stessi passi, stesso comportamento, stessi errori. Ma ora si legge dall’alto in basso, come se ogni operazione “tirasse fuori” il valore dal suo Result: dividi il testo, prendi le ore, prendi i minuti, controlla. Se un passo fallisce, la funzione restituisce quell’errore e le righe sotto non vengono eseguite.
Nota che il valore a sinistra della freccia può essere un pattern, come #(hours_text, minutes_text): la coppia viene smontata direttamente.
Come funziona davvero
use sembra magia, ma è solo una riscrittura, fatta dal compilatore, di codice che sai già leggere. La regola è una:
Tutto quello che sta sotto una riga
use x <- f(a), fino alla fine del blocco, diventa il corpo di una funzione anonimafn(x) { ... }, che viene passata come ultimo argomento dif.
Quindi questo:
use hours <- result.try(parse_number(hours_text))
hours * 60è esattamente questo:
result.try(parse_number(hours_text), fn(hours) {
hours * 60
})Nient’altro. use non conosce i Result, non gestisce errori, non ha niente di speciale: funziona con qualsiasi funzione il cui ultimo argomento è una funzione. È result.try a decidere di non chiamare la funzione se c’è un errore; use si limita a farti scrivere quella funzione senza graffe e senza rientri.
Per convincersene, eccolo con una funzione che non ha niente a che fare con gli errori, list.each:
import gleam/io
import gleam/list
pub fn main() -> Nil {
use name <- list.each(["Ada", "Grace"])
io.println("Hello, " <> name)
}Hello, Ada
Hello, Gracelist.each(lista, fn(name) { io.println(...) }), scritto con use.
Quiz
In cosa si trasforma use x <- f(1) seguito dalla riga x + 1?
Uscire prima: bool.guard
Un’altra forma comune: “se questa condizione è vera, restituisci subito questo valore, altrimenti continua”. Negli altri linguaggi è un return anticipato in un if. In Gleam c’è bool.guard, pensata per essere usata con use:
use <- bool.guard(when: amount <= 0, return: Error(InvalidAmount))
// from here on, amount is positivebool.guard(when: condizione, return: valore, otherwise: funzione): se la condizione è vera restituisce valore, altrimenti chiama la funzione. Con use, “la funzione” è il resto del blocco. Nota che qui a sinistra della freccia non c’è niente: la funzione del resto non riceve argomenti.
Mettere tutto insieme
use con result.try e bool.guard si combinano bene per le validazioni: una serie di controlli, ognuno dei quali può fermare tutto con un errore preciso.
pub fn validate(
name: String,
age_text: String,
email: String,
) -> Result(User, FormError) {
let name = string.trim(name)
use <- bool.guard(when: name == "", return: Error(EmptyName))
use age <- result.try(parse_age(age_text))
use <- bool.guard(
when: !string.contains(email, "@"),
return: Error(BadEmail(email)),
)
Ok(User(name:, age:, email:))
}Una riga per controllo, nell’ordine in cui avvengono. E alla fine, arrivati fin lì, tutto è valido. (string.contains(testo, pezzo) dice se pezzo compare in testo; User(name:, age:, email:) usa la scorciatoia delle etichette anche nella costruzione del record.)
Esercizio · sul tuo computer
Il modulo di iscrizione
Nel progetto exercises crea src/signup.gleam con i tipi:
pub type User {
User(name: String, age: Int, email: String)
}
pub type FormError {
EmptyName
BadAge(String)
BadEmail(String)
}Scrivi:
parse_age(text: String) -> Result(Int, FormError): un’età da 0 a 150, altrimentiBadAgecon il testo;validate(name: String, age_text: String, email: String) -> Result(User, FormError), conuse,bool.guarderesult.try, come nell’esempio della lezione (il nome senza spazi ai lati non deve essere vuoto, l’email deve contenere@);describe(result: Result(User, FormError)) -> String, come nell’output.
In main prova validate con: "Ada", "36", "ada@example.com"; " ", "36", "x@y.z"; "Ada", "forty", "ada@example.com"; "Ada", "36", "ada.example.com".
Welcome, Ada (36)
Error: the name is empty
Error: not a valid age: forty
Error: not a valid email: ada.example.comMostra una soluzione (prima prova da solo!)
import gleam/bool
import gleam/int
import gleam/io
import gleam/result
import gleam/string
pub type User {
User(name: String, age: Int, email: String)
}
pub type FormError {
EmptyName
BadAge(String)
BadEmail(String)
}
pub fn main() -> Nil {
io.println(describe(validate("Ada", "36", "ada@example.com")))
io.println(describe(validate(" ", "36", "x@y.z")))
io.println(describe(validate("Ada", "forty", "ada@example.com")))
io.println(describe(validate("Ada", "36", "ada.example.com")))
}
pub fn validate(
name: String,
age_text: String,
email: String,
) -> Result(User, FormError) {
let name = string.trim(name)
use <- bool.guard(when: name == "", return: Error(EmptyName))
use age <- result.try(parse_age(age_text))
use <- bool.guard(
when: !string.contains(email, "@"),
return: Error(BadEmail(email)),
)
Ok(User(name:, age:, email:))
}
fn parse_age(text: String) -> Result(Int, FormError) {
case int.parse(text) {
Ok(age) if age >= 0 && age <= 150 -> Ok(age)
_ -> Error(BadAge(text))
}
}
fn describe(result: Result(User, FormError)) -> String {
case result {
Ok(user) ->
"Welcome, " <> user.name <> " (" <> int.to_string(user.age) <> ")"
Error(EmptyName) -> "Error: the name is empty"
Error(BadAge(text)) -> "Error: not a valid age: " <> text
Error(BadEmail(email)) -> "Error: not a valid email: " <> email
}
}In parse_age un pattern con guardia (Ok(age) if ...) e un _ finale coprono sia i testi che non sono numeri sia i numeri fuori intervallo, con lo stesso errore.
Ricapitolando
use x <- f(a)trasforma il resto del blocco infn(x) { ... }, passata come ultimo argomento dif.- Non è specifica degli errori: funziona con qualsiasi funzione che riceve una funzione come ultimo argomento.
use value <- result.try(operazione)scrive dritta una catena di passi che possono fallire; il primo errore la interrompe.- A sinistra della freccia può esserci un pattern, o niente (
use <- ...). use <- bool.guard(when: condizione, return: valore)esce subito dalla funzione convalore.usesi prende tutto il resto del blocco: usalo con misura, soprattutto perresult.tryebool.guard.
Nella prossima lezione vediamo l’altra faccia degli errori: quando è giusto fermare il programma di proposito.