Lezione 5 di 8 · 18 min di lettura
Pattern e guardie
Pattern che smontano le stringhe, case su più valori insieme e guardie per le condizioni che un pattern non sa esprimere. Con un classico dei colloqui di lavoro, FizzBuzz.
Pattern che guardano dentro le stringhe
Nella lezione precedente i pattern erano valori esatti: 1, "yes". Ma un pattern può anche descrivere una forma e prenderne i pezzi. Con le stringhe si può dire “comincia con questo testo, e il resto chiamalo così”:
import gleam/io
pub fn main() -> Nil {
io.println(reply("/help"))
io.println(reply("/say Hello, Joe"))
io.println(reply("/dance"))
}
fn reply(command: String) -> String {
case command {
"/help" -> "Commands: /help, /say <text>"
"/say " <> text -> "You said: " <> text
_ -> "Unknown command"
}
}Commands: /help, /say <text>
You said: Hello, Joe
Unknown commandIl pattern "/say " <> text corrisponde a ogni stringa che comincia con "/say ", e lega quello che resta al nome text. È lo stesso <> che usi per attaccare le stringhe, usato al contrario: invece di costruire, smonta.
C’è una regola: a sinistra di <> ci deve essere un testo scritto per intero (un letterale). Il pattern word <> "!", “qualsiasi cosa che finisce con il punto esclamativo”, non è permesso, e il compilatore lo spiega con un This must be a string literal: senza sapere quanto è lungo il pezzo iniziale, non saprebbe dove tagliare. Per controllare come finisce una stringa c’è la funzione string.ends_with.
Quiz
Quale stringa corrisponde al pattern "Hello, " <> name, e cosa vale name?
Più soggetti insieme
Un case può guardare più valori contemporaneamente: li separi con una virgola dopo case, e ogni clausola ha un pattern per ciascuno, nello stesso ordine.
È il momento di un classico. FizzBuzz è un piccolo problema famoso perché lo si usa nei colloqui di lavoro: per ogni numero, se è divisibile per 3 scrivi “Fizz”, se è divisibile per 5 “Buzz”, se lo è per entrambi “FizzBuzz”, altrimenti il numero stesso. Con due soggetti, i resti delle due divisioni, è quasi una tabella:
import gleam/int
import gleam/io
pub fn main() -> Nil {
io.println(fizzbuzz(15))
io.println(fizzbuzz(9))
io.println(fizzbuzz(10))
io.println(fizzbuzz(7))
}
fn fizzbuzz(n: Int) -> String {
case n % 3, n % 5 {
0, 0 -> "FizzBuzz"
0, _ -> "Fizz"
_, 0 -> "Buzz"
_, _ -> int.to_string(n)
}
}FizzBuzz
Fizz
Buzz
7Leggi le clausole come righe di una tabella: “resto 0 e resto 0”, “resto 0 e qualsiasi cosa”, “qualsiasi cosa e resto 0”, “tutto il resto”. L’ordine conta: 15 corrisponderebbe anche a 0, _, ma la clausola 0, 0 sta sopra e vince lei. Se provassi a scambiarle, il compilatore ti segnalerebbe che 0, 0 non è più raggiungibile.
Le guardie
Alcune condizioni non si possono scrivere come pattern. “Un numero negativo” non è un valore preciso, è una condizione. Per questi casi una clausola può avere una guardia: if seguito da una condizione, dopo il pattern. La clausola vince solo se il pattern corrisponde e la condizione è vera.
import gleam/io
pub fn main() -> Nil {
io.println(grade(95))
io.println(grade(80))
io.println(grade(64))
io.println(grade(-3))
}
fn grade(score: Int) -> String {
case score {
s if s < 0 || s > 100 -> "invalid score"
s if s >= 90 -> "A"
s if s >= 75 -> "B"
s if s >= 60 -> "C"
_ -> "F"
}
}A
B
C
invalid scoreSì, qui compare la parola if, ma non è l’if degli altri linguaggi: esiste solo dentro un case, come condizione aggiuntiva di una clausola.
Nota come l’ordine delle clausole semplifichi le condizioni: quando arrivi a s >= 75 sai già che s è minore di 90, altrimenti avrebbe vinto la clausola prima. Non serve scrivere s >= 75 && s < 90.
Nelle guardie puoi usare confronti, operatori aritmetici e logici, e qualsiasi variabile visibile in quel punto, non solo quelle del pattern:
fn size(n: Int, limit: Int) -> String {
case n {
x if x > limit -> "too big"
x if x * 2 > limit -> "medium"
_ -> "small"
}
}Le guardie hanno due limiti
Il primo: in una guardia non si possono chiamare funzioni. Se provi a scrivere x if int.is_even(x) -> ..., il compilatore risponde Functions cannot be called in clause guards. Le guardie devono essere semplici e veloci da valutare; se ti serve il risultato di una funzione, calcolalo prima in un let e usa quella variabile (o fanne il soggetto del case).
Il secondo: il compilatore non ragiona sulle guardie quando controlla l’esaustività. A te è evidente che un numero è per forza negativo oppure maggiore o uguale a zero; a lui no:
import gleam/io
pub fn main() -> Nil {
io.println(sign(3))
}
fn sign(n: Int) -> String {
case n {
x if x < 0 -> "negative"
x if x >= 0 -> "positive"
}
}error: Inexhaustive patterns
┌─ /home/ada/learn-gleam/exercises/src/sign.gleam:8:3
│
8 │ ╭ case n {
9 │ │ x if x < 0 -> "negative"
10 │ │ x if x >= 0 -> "positive"
11 │ │ }
│ ╰───^
This case expression does not have a pattern for all possible values. If it
is run on one of the values without a pattern then it will crash.
The missing patterns are:
_La soluzione è che l’ultima clausola non abbia guardia: _ -> "positive". È anche più chiaro, perché dice apertamente “tutto il resto”.
Quiz
Quale clausola non compila?
Tutto insieme
Pattern su più soggetti e guardie si combinano: la guardia viene scritta una volta sola, dopo tutti i pattern della clausola, e può usare tutte le variabili che i pattern hanno legato.
case age, has_coupon {
a, _ if a < 6 -> "free"
_, True -> "discounted"
_, False -> "full price"
}Sotto i sei anni si entra gratis, coupon o no. Altrimenti decide il coupon. Tre clausole, e tutti i casi possibili sono coperti.
Esercizio · sul tuo computer
Il biglietto del museo
Nel progetto exercises crea src/museum.gleam. Il biglietto intero costa 10 euro. I bambini sotto i 6 anni entrano gratis; chi ha 65 anni o più paga 5 euro; gli studenti pagano 7 euro. Se una persona rientra in più categorie, vale la prima di questo elenco (quindi un anziano studente paga 5 euro).
Scrivi:
price(age: Int, student: Bool) -> Int, il prezzo in centesimi, con uncasesu due soggetti e le guardie;ticket(age: Int, student: Bool) -> String, che restituisce"free"se il prezzo è 0, altrimenti l’importo formattato conformat_cents. Usa uncasesul prezzo.
In main stampa i biglietti per: un bambino di 4 anni, uno studente di 20, un non studente di 70, uno studente di 70 e un non studente di 35:
Child: free
Student: 7.00
Senior: 5.00
Senior student: 5.00
Adult: 10.00Mostra una soluzione (prima prova da solo!)
import gleam/int
import gleam/io
import gleam/string
pub fn main() -> Nil {
io.println("Child: " <> ticket(4, False))
io.println("Student: " <> ticket(20, True))
io.println("Senior: " <> ticket(70, False))
io.println("Senior student: " <> ticket(70, True))
io.println("Adult: " <> ticket(35, False))
}
fn price(age: Int, student: Bool) -> Int {
case age, student {
a, _ if a < 6 -> 0
a, _ if a >= 65 -> 500
_, True -> 700
_, False -> 1000
}
}
fn ticket(age: Int, student: Bool) -> String {
case price(age, student) {
0 -> "free"
cents -> format_cents(cents)
}
}
fn format_cents(cents: Int) -> String {
int.to_string(cents / 100)
<> "."
<> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}L’ordine delle clausole traduce l’ordine dell’elenco: l’età viene controllata prima dello status di studente. Nota anche il soggetto di ticket: è una chiamata di funzione. Il soggetto di un case può essere qualsiasi espressione; sono solo le guardie a non poter chiamare funzioni.
Ricapitolando
"/say " <> textcorrisponde alle stringhe che cominciano con"/say "e lega il resto atext. A sinistra di<>ci vuole un testo letterale.case a, b { 0, 0 -> ... }guarda più soggetti insieme: un pattern per ciascuno, separati da virgole.- Una guardia
pattern if condizione -> ...aggiunge una condizione alla clausola. - Nelle guardie: confronti, aritmetica, logica e variabili, ma niente chiamate di funzione.
- Il compilatore non ragiona sulle guardie: l’ultima clausola di solito è senza guardia.
- L’ordine delle clausole semplifica le condizioni: quelle sopra hanno già escluso dei casi.
Adesso il programma sa scegliere. Nella prossima lezione impara a ripetere, senza for e senza while.