Skip to content
Ridgun
English

Ridgun

The language itself: a compiler that compiles itself, the stable project tool, the specification and the book

04-pattern-matching-e-guardie.md208 lines · 11.1 KBRaw
1# Capitolo 04: Addio a `if/else`, addio a `while` (Pattern Matching, Guardie e TCO)
2
3Quando racconti a qualcuno che in Ridgun **non esiste la parola chiave `if`, né `else`, né `switch`, né `for`, né `while`**, di solito ti guarda come se gli avessi detto che costruirai una casa senza martello né chiodi:
4
5> *"Come sarebbe che non c'è `if` né `while`? E come diavolo prendo una decisione o percorro un milione di dati?"*
6
7Beh, risulta che `if/else` e `while` sono attrezzi piuttosto grezzi ereditati da come funzionavano i salti (`GOTO`) negli anni '50. In Ridgun li sostituiamo con tre pezzi che calzano a pennello:
81. ***Pattern Matching*** (quando decidi in base alla **forma o al valore esatto** del dato).
92. **Guardie booleane `|`** (quando decidi in base a una **condizione o a un intervallo**).
103. **Ricorsione di coda con TCO** (che il compilatore trasforma sotto il cofano in un ciclo `loop` nativo ultraveloce).
11
12Vediamoli in azione.
13
14---
15
16## 4.1. *Pattern Matching*: Quando l'argomento ti dà già la risposta
17
18Nel capitolo precedente ne abbiamo già visto un assaggio. Invece di ricevere una variabile generica `x` e aprire dentro la funzione cinque `if (x == ...)`, in Ridgun metti i casi direttamente sulla porta d'ingresso della funzione:
19
20```ridgun
21fn :: codice http Int => String
22 codice http 200 = "OK: Tutto è andato liscio"
23 codice http 301 = "Spostato permanentemente"
24 codice http 404 = "Non trovato (hai guardato sotto il divano?)"
25 codice http 500 = "Fuoco nel server"
26 codice http n = "Codice sconosciuto: {n}"
27```
28
29Ridgun prova le righe **dall'alto verso il basso**:
30- Se entra `404`, salta dritto alla terza riga.
31- Se entra `418`, nessuna delle prime quattro righe corrisponde, quindi cade nell'ultima (`n`), che cattura qualsiasi intero e lo chiama `n` affinché tu possa stamparlo.
32- E se nell'ultimo caso non ti importa nemmeno quale numero fosse? Metti un trattino basso `_` (*jolly*) e te ne dimentichi: `codice http _ = "Altro codice"`.
33
34---
35
36## 4.2. Guardie booleane (`|`): Il tuo bisturi per intervalli, variabili condizionali e alberi decisionali
37
38*"Ok, il pattern matching è forte quando cerco il `200` o il `404`. Ma cosa faccio se voglio controllare se `voto >= 6` o se `eta > 18 and eta < 65`? Non scriverò mica 47 righe con tutti i numeri dal 18 al 65!"*
39
40Certo che no! Per questo ci sono le **guardie (`|`)**.
41
42Quando il risultato dipende da una disuguaglianza o da una combinazione logica, dopo l'`=` della funzione (o di qualsiasi variabile locale) apri una scala di barre verticali `|`:
43
44```ridgun
45valuta Int => String
46valuta voto =
47 | voto < 0 = "Voto impossibile (punti negativi?)"
48 | voto < 6 = "Insufficiente"
49 | voto < 7 = "Sufficiente"
50 | voto < 9 = "Buono"
51 | voto <= 10 = "Ottimo"
52 | otherwise = "Sei uscito dalla scala"
53```
54
55Guardalo bene:
56- Il segno `=` dell'intestazione (`valuta voto =`) si allinea sotto `=>` proprio come in qualsiasi altra funzione.
57- Sotto si legge dall'alto verso il basso proprio come un tariffario: ogni riga `| condizione = risultato` viene controllata in ordine. Non appena una dà `True`, restituisce quel risultato ed è finita.
58- Alla fine metti **`| otherwise = ...`**, che significa letteralmente *"in qualsiasi altro caso"* (la tua rete di sicurezza affinché non rimanga mai un percorso senza ritorno).
59
60### Logica booleana in minuscolo chiaro (`and`, `or`, `xor`, `not`)
61Hai notato che molti linguaggi usano `|` per i bit, `||` per i booleani, `!` per negare... e quando leggi una condizione complessa sembra che un gatto abbia camminato sulla tastiera (`if (!a && (b || c))`)?
62
63In Ridgun, poiché `|` è già il simbolo sacro delle guardie e dei tipi algebrici, e `||` è l'operatore di parallelismo, **gli operatori booleani si scrivono con parole chiare in minuscolo**:
64
65- **`and`**: si verificano entrambe le cose con valutazione a cortocircuito (`eta >= 18 and ha_patente`: se il lato sinistro è `False`, il destro non viene nemmeno valutato).
66- **`or`**: se ne verifica almeno una con valutazione a cortocircuito (`giorno == "sabato" or giorno == "domenica"`: se il lato sinistro è `True`, il destro non viene valutato).
67- **`xor`**: si verifica l'una o l'altra, ma non entrambe contemporaneamente.
68- **`not`**: inverte il booleano (`not (e vuoto testo)`).
69- **`<`, `>`, `<=`, `>=`**: confrontano sia numeri (`Int`, `Float`) sia stringhe di testo **`String` in ordine lessicografico UTF-8** (`"alfa" < "beta"`).
70
71Guarda come viene pulito il classico problema degli anni bisestili:
72
73```ridgun
74fn :: e bisestile Int => Bool
75 e bisestile a =
76 | (a % 400) == 0 = True
77 | (a % 4) == 0 and (a % 100) != 0 = True
78 | otherwise = False
79```
80
81Zero simboli criptici. Lo legge qualcuno che non conosce Ridgun e lo capisce al primo colpo.
82
83### Variabili locali condizionali (`x =` + `|`)
84Poiché in Ridgun ogni `=` segue la stessa regola, **puoi usare `|` anche per assegnare una variabile locale** senza dover creare una funzione ausiliaria a parte:
85
86```ridgun
87fn :: prezzo biglietto Int Bool => Int
88 prezzo biglietto base vip =
89 sconto =
90 | vip = 20
91 | base > 100 = 10
92 | otherwise = 0
93 base - (base * sconto / 100)
94```
95
96### Alberi decisionali annidati e il delimitatore compatto `|.`
97*"E se dentro un ramo `| cond =` o `| otherwise =` voglio aprire un altro sotto-albero di guardie `|` profondo 3 o 4 livelli e poi tornare a un livello superiore?"*
98
99Puoi annidare `|` dentro `|` per tutti i livelli che vuoi. E quando un blocco `|` annidato finisce ma sotto continuano altri rami di un livello padre (o altre istruzioni del blocco), metti **`|.`** alla fine della sua ultima riga (`| otherwise = valore |.`) per indicare senza alcuna ambiguità che quel sotto-albero si chiude e torni al livello precedente:
100
101```ridgun
102fn :: instrada richiesta Int Int Bool Bool Int Int => Int
103 instrada richiesta metodo ruolo firmata admin kb lim =
104 | metodo == 0 =
105 | kb == 0 = 204
106 | otherwise = 200 |.
107 | metodo == 1 =
108 | not firmata = 401
109 | otherwise =
110 -- Livello 2: richiesta POST firmata
111 | lim > 0 =
112 -- Livello 3: controllo di rate-limit
113 | admin =
114 -- Livello 4: eccezione per gli amministratori
115 | kb > 10000 = 413
116 | otherwise = 202 |.
117 | otherwise = 429 |.
118 | kb > 1000 =
119 -- Livello 3: siamo tornati dal Livello 4 grazie a `|.`
120 | ruolo >= 2 and admin = 201
121 | otherwise = 413 |.
122 | otherwise = 201 |.
123 | otherwise = 405
124```
125
126Nota quanto è pulito:
127- In un albero semplice di 1 livello (o alla fine di tutta la funzione) **non serve mai mettere `|.`** (sarebbe rumore inutile).
128- Scrivi `|.` solo quando chiudi un ramo annidato sotto il quale continua un ramo di un livello superiore.
129
130### Blocchi su più righe dentro una guardia (`| cond =` + blocco indentato)
131Proprio come dopo l'`=` di una funzione normale puoi aprire un blocco indentato su più righe, **anche dopo l'`=` di qualsiasi guardia `| cond =` o `| otherwise =` puoi andare a capo e aprire un blocco sequenziale**:
132
133```ridgun
134fn :: regola finestra Int Int => Int
135 regola finestra idx limite =
136 | idx >= limite =
137 tetto = limite - 1
138 scalato = tetto * 2
139 scalato + 10
140 | otherwise =
141 successivo = idx + 1
142 successivo * 2
143```
144
145Ogni ramo della guardia ha il proprio blocco locale pulito e la sua ultima riga è il valore restituito da quel ramo.
146
147---
148
149## 4.3. "Senza `while`? E non esploderà lo stack se percorro un file grande?"
150
151Parliamo dell'elefante nella stanza.
152
153Se vieni da Python, Java o JavaScript, ti hanno insegnato fin da piccolo che *"la ricorsione è elegante sulla lavagna dell'università, ma nel mondo reale fa traboccare lo stack (`StackOverflowError`) dopo 1.000 giri; usa un ciclo `while`"*.
154
155Questo è vero nei linguaggi che **non** ottimizzano le chiamate di coda. Ma in un linguaggio funzionale di sistema come Ridgun, **una ricorsione di coda È un ciclo `while` travestito da funzione pura**.
156
157### Che differenza c'è tra una ricorsione normale e una ricorsione di coda?
158Guarda queste due funzioni che contano alla rovescia da `n` fino a `0` sommando i numeri:
159
160```ridgun
161-- Versione A: Ricorsione NORMALE (lascia lavoro in sospeso per il ritorno)
162fn :: somma lento Int => Int
163 somma lento 0 = 0
164 somma lento n = n + (somma lento (n - 1))
165
166-- Versione B: Ricorsione DI CODA (tiene il conto aggiornato in `accumulato`)
167fn :: somma veloce ciclo Int Int => Int
168 somma veloce ciclo 0 accumulato = accumulato
169 somma veloce ciclo n accumulato = somma veloce ciclo (n - 1) (accumulato + n)
170```
171
172- Nella **Versione A**, prima di poter sommare `n + ...`, la funzione deve aspettare che finisca `somma lento (n - 1)`. Ciò obbliga il computer a salvare `n` nello stack a ogni passo.
173- Nella **Versione B**, la chiamata a `somma veloce ciclo` **è l'ultima cosa che accade nella riga**. Non rimane nulla in sospeso dietro di essa. Le passi già la somma aggiornata in `accumulato + n`.
174
175### Il trucco di magia di `ridgunc` (TCO: *Tail-Call Optimization*)
176Quando `ridgunc` compila `somma veloce ciclo`, vede che la funzione chiama se stessa alla fine della clausola e dice:
177
178> *"Perché mai dovrei creare un nuovo stack frame se quello attuale non serve più a nulla? Sovrascriverò `n` con `n - 1`, sovrascriverò `accumulato` con `accumulato + n` e salterò all'inizio con un `loop { ... continue; }` nativo."*
179
180Risultato:
1811. Puoi chiamare `somma veloce ciclo 100_000_000 0` (cento milioni di giri) e **consumerà esattamente 0 byte extra di stack**.
1822. Va veloce quanto un ciclo `while` scritto a mano in Rust o C, ma senza aver dovuto dichiarare nemmeno una singola variabile mutabile nel tuo codice Ridgun.
183
184---
185
186## 4.4. Il pattern maestro che userai in ogni programma Ridgun
187
188Ogni volta che in un altro linguaggio penseresti *"farò un ciclo `while` con un indice `i` e un accumulatore"*, in Ridgun scrivi una coppia di funzioni come questa (la funzione principale pulita e il suo aiutante di coda):
189
190```ridgun
191fn :: cerca carattere ciclo String Int Int Int => Int
192 cerca carattere ciclo testo pos fine cercato =
193 | pos >= fine = -1
194 | (str char at testo pos) == cercato = pos
195 | otherwise = cerca carattere ciclo testo (pos + 1) fine cercato
196
197fn :: cerca carattere String Int => Int
198 cerca carattere testo cercato = cerca carattere ciclo testo 0 (str len testo) cercato
199```
200
201Guarda le tre guardie di `cerca carattere ciclo`:
2021. Sono arrivato alla fine (`pos >= fine`)? Restituisco `-1` (fine del ciclo).
2032. Ho trovato il carattere in `pos`? Restituisco `pos` (fine del ciclo).
2043. Non è questo? Passo al successivo (`pos + 1`) con una chiamata di coda (che il compilatore converte in `continue;`).
205
206Pulito, impossibile avere *bug* di variabili globali sovrascritte e veloce come un fulmine.
207
208Nel **Capitolo 05** metteremo le mani sulle collezioni di dati: **liste, tuple, funzioni che accettano infiniti argomenti (`...`) e le maschere di sequenze in stile J/APL**.