Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capitolo 04: Addio a `if/else`, addio a `while` (Pattern Matching, Guardie e TCO) |
| 2 | |
| 3 | Quando 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 | |
| 7 | Beh, 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: |
| 8 | 1. ***Pattern Matching*** (quando decidi in base alla **forma o al valore esatto** del dato). |
| 9 | 2. **Guardie booleane `|`** (quando decidi in base a una **condizione o a un intervallo**). |
| 10 | 3. **Ricorsione di coda con TCO** (che il compilatore trasforma sotto il cofano in un ciclo `loop` nativo ultraveloce). |
| 11 | |
| 12 | Vediamoli in azione. |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## 4.1. *Pattern Matching*: Quando l'argomento ti dà già la risposta |
| 17 | |
| 18 | Nel 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 |
| 21 | fn :: 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 | |
| 29 | Ridgun 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 | |
| 40 | Certo che no! Per questo ci sono le **guardie (`|`)**. |
| 41 | |
| 42 | Quando 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 |
| 45 | valuta Int => String |
| 46 | valuta 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 | |
| 55 | Guardalo 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`) |
| 61 | Hai 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 | |
| 63 | In 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 | |
| 71 | Guarda come viene pulito il classico problema degli anni bisestili: |
| 72 | |
| 73 | ```ridgun |
| 74 | fn :: 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 | |
| 81 | Zero simboli criptici. Lo legge qualcuno che non conosce Ridgun e lo capisce al primo colpo. |
| 82 | |
| 83 | ### Variabili locali condizionali (`x =` + `|`) |
| 84 | Poiché 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 |
| 87 | fn :: 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 | |
| 99 | Puoi 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 |
| 102 | fn :: 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 | |
| 126 | Nota 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) |
| 131 | Proprio 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 |
| 134 | fn :: 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 | |
| 145 | Ogni 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 | |
| 151 | Parliamo dell'elefante nella stanza. |
| 152 | |
| 153 | Se 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 | |
| 155 | Questo è 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? |
| 158 | Guarda 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) |
| 162 | fn :: 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`) |
| 167 | fn :: 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*) |
| 176 | Quando `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 | |
| 180 | Risultato: |
| 181 | 1. Puoi chiamare `somma veloce ciclo 100_000_000 0` (cento milioni di giri) e **consumerà esattamente 0 byte extra di stack**. |
| 182 | 2. 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 | |
| 188 | Ogni 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 |
| 191 | fn :: 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 | |
| 197 | fn :: cerca carattere String Int => Int |
| 198 | cerca carattere testo cercato = cerca carattere ciclo testo 0 (str len testo) cercato |
| 199 | ``` |
| 200 | |
| 201 | Guarda le tre guardie di `cerca carattere ciclo`: |
| 202 | 1. Sono arrivato alla fine (`pos >= fine`)? Restituisco `-1` (fine del ciclo). |
| 203 | 2. Ho trovato il carattere in `pos`? Restituisco `pos` (fine del ciclo). |
| 204 | 3. Non è questo? Passo al successivo (`pos + 1`) con una chiamata di coda (che il compilatore converte in `continue;`). |
| 205 | |
| 206 | Pulito, impossibile avere *bug* di variabili globali sovrascritte e veloce come un fulmine. |
| 207 | |
| 208 | Nel **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**. |