Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítol 04: Adéu a l'`if/else`, adéu al `while` (Pattern Matching, Guardes i TCO) |
| 2 | |
| 3 | Quan li expliques a algú que en Ridgun **no existeix la paraula clau `if`, ni `else`, ni `switch`, ni `for`, ni `while`**, sol mirar-te com si li haguessis dit que construiràs una casa sense martell ni claus: |
| 4 | |
| 5 | > *"Que no hi ha `if` ni `while`? I com nassos prenc una decisió o recorro un milió de dades?"* |
| 6 | |
| 7 | Doncs resulta que `if/else` i `while` són eines força tosques heretades de com funcionaven els salts (`GOTO`) als anys 50. En Ridgun els substituïm per tres peces que encaixen com un guant: |
| 8 | 1. ***Pattern Matching*** (quan decideixes segons la **forma o valor exacte** de la dada). |
| 9 | 2. **Guardes booleanes `|`** (quan decideixes segons una **condició o rang**). |
| 10 | 3. **Recursió de cua amb TCO** (que el compilador converteix per sota en un bucle `loop` natiu ultraràpid). |
| 11 | |
| 12 | Vegem-les en acció. |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## 4.1. *Pattern Matching*: Quan l'argument ja et dóna la resposta |
| 17 | |
| 18 | Al capítol anterior ja en vam veure un avançament. En lloc de rebre una variable genèrica `x` i obrir dins de la funció cinc `if (x == ...)`, en Ridgun poses els casos directament a la porta d'entrada de la funció: |
| 19 | |
| 20 | ```ridgun |
| 21 | codi http Int => String |
| 22 | codi http 200 = "OK: Tot ha anat sobre rodes" |
| 23 | codi http 301 = "Mogut permanentment" |
| 24 | codi http 404 = "No trobat (has mirat sota el sofà?)" |
| 25 | codi http 500 = "Foc al servidor" |
| 26 | codi http n = "Codi desconegut: {n}" |
| 27 | ``` |
| 28 | |
| 29 | Ridgun prova les files **de dalt a baix**: |
| 30 | - Si entra `404`, salta directe a la tercera fila. |
| 31 | - Si entra `418`, cap de les quatre primeres files coincideix, així que cau a l'última (`n`), que atrapa qualsevol enter i l'anomena `n` perquè puguis imprimir-lo. |
| 32 | - I si en l'últim cas ni tan sols t'importa quin número era? Poses un guió baix `_` (*comodí*) i te n'oblides: `codi http _ = "Un altre codi"`. |
| 33 | |
| 34 | --- |
| 35 | |
| 36 | ## 4.2. Guardes booleanes (`|`): El teu bisturí per a rangs, variables condicionals i arbres de decisió |
| 37 | |
| 38 | *"D'acord, el pattern matching mola quan busco el `200` o el `404`. Però què faig si vull comprovar si `nota >= 5` o si `edat > 18 and edat < 65`? No escriuré pas 47 files amb tots els números del 18 al 65!"* |
| 39 | |
| 40 | És clar que no! Per a això hi ha les **guardes (`|`)**. |
| 41 | |
| 42 | Quan el resultat depèn d'una desigualtat o d'una combinació lògica, després de l'`=` de la funció (o de qualsevol variable local) obres una escala de barres verticals `|`: |
| 43 | |
| 44 | ```ridgun |
| 45 | qualificar Int => String |
| 46 | qualificar nota = |
| 47 | | nota < 0 = "Nota impossible (punts negatius?)" |
| 48 | | nota < 5 = "Suspès" |
| 49 | | nota < 7 = "Aprovat" |
| 50 | | nota < 9 = "Notable" |
| 51 | | nota <= 10 = "Excel·lent" |
| 52 | | otherwise = "T'has passat de l'escala" |
| 53 | ``` |
| 54 | |
| 55 | Mira-t'ho bé: |
| 56 | - El signe `=` de la capçalera (`qualificar nota =`) s'alinea sota `=>` igual que en qualsevol altra funció. |
| 57 | - A sota es llegeix de dalt a baix igual que una taula de tarifes: cada línia `| condicio = resultat` es comprova en ordre. Tan bon punt una dóna `True`, retorna aquell resultat i s'ha acabat. |
| 58 | - Al final poses **`| otherwise = ...`**, que significa literalment *"en qualsevol altre cas"* (la teva xarxa de seguretat perquè mai no quedi un camí sense retorn). |
| 59 | |
| 60 | ### Lògica booleana en minúscules clares (`and`, `or`, `xor`, `not`) |
| 61 | T'has fixat que molts llenguatges fan servir `|` per a bits, `||` per a booleans, `!` per negar... i quan llegeixes una condició complexa sembla que un gat hagi caminat sobre el teclat (`if (!a && (b || c))`)? |
| 62 | |
| 63 | En Ridgun, com que `|` ja és el símbol sagrat de les guardes i dels tipus algebraics, i `||` és l'operador de paral·lelisme, **els operadors booleans s'escriuen amb paraules clares en minúscules**: |
| 64 | |
| 65 | - **`and`**: es compleixen totes dues coses amb avaluació en curtcircuit (`edat >= 18 and te_carnet`). |
| 66 | - **`or`**: se'n compleix almenys una amb avaluació en curtcircuit (`dia == "dissabte" or dia == "diumenge"`). |
| 67 | - **`xor`**: es compleix l'una o l'altra, però no totes dues alhora. |
| 68 | - **`not`**: inverteix el booleà (`not (es buit text)`). |
| 69 | - **`<`, `>`, `<=`, `>=`**: comparen tant nombres (`Int`, `Float`) com cadenes **`String` en ordre lexicogràfic UTF-8** (`"alfa" < "beta"`). |
| 70 | |
| 71 | Mira que net queda el clàssic problema dels anys de traspàs: |
| 72 | |
| 73 | ```ridgun |
| 74 | es de traspas Int => Bool |
| 75 | es de traspas a = |
| 76 | | (a % 400) == 0 = True |
| 77 | | (a % 4) == 0 and (a % 100) != 0 = True |
| 78 | | otherwise = False |
| 79 | ``` |
| 80 | |
| 81 | Zero símbols críptics. Ho llegeix algú que no sap Ridgun i ho entén a la primera. |
| 82 | |
| 83 | ### Variables locals condicionals (`x =` + `|`) |
| 84 | Com que en Ridgun tot `=` segueix la mateixa regla, **també pots fer servir `|` per assignar una variable local** sense haver de crear una funció auxiliar a part: |
| 85 | |
| 86 | ```ridgun |
| 87 | preu bitllet Int Bool => Int |
| 88 | preu bitllet base es_familia_nombrosa = |
| 89 | descompte = |
| 90 | | es_familia_nombrosa = 20 |
| 91 | | base > 100 = 10 |
| 92 | | otherwise = 0 |
| 93 | base - (base * descompte / 100) |
| 94 | ``` |
| 95 | |
| 96 | ### Arbres de decisió imbricats i el limitador compacte `|.` |
| 97 | *"I si dins d'una branca `| cond =` o `| otherwise =` vull obrir un altre subarbre de guardes `|` de 3 o 4 nivells de profunditat i després tornar a un nivell superior?"* |
| 98 | |
| 99 | Pots imbricar `|` dins de `|` tants nivells com vulguis. I quan un bloc `|` imbricat acaba però a sota continuen més branques d'un nivell pare (o més sentències del bloc), col·loques **`|.`** al final de la seva última línia (`| otherwise = valor |.`) per indicar sense cap ambigüitat que aquell subarbre es tanca i tornes al nivell anterior: |
| 100 | |
| 101 | ```ridgun |
| 102 | encaminar peticio Int Int Bool Bool Int Int => Int |
| 103 | encaminar peticio metode rol signada es_admin mida_kb limitat = |
| 104 | | metode == 0 = |
| 105 | | mida_kb == 0 = 204 |
| 106 | | otherwise = 200 |. |
| 107 | | metode == 1 = |
| 108 | | not signada = 401 |
| 109 | | otherwise = |
| 110 | -- Nivell 2: petició POST signada |
| 111 | | limitat > 0 = |
| 112 | -- Nivell 3: control de rate-limit |
| 113 | | es_admin = |
| 114 | -- Nivell 4: excepció per a administradors |
| 115 | | mida_kb > 10000 = 413 |
| 116 | | otherwise = 202 |. |
| 117 | | otherwise = 429 |. |
| 118 | | mida_kb > 1000 = |
| 119 | -- Nivell 3: hem tornat des del Nivell 4 gràcies a `|.` |
| 120 | | rol >= 2 and es_admin = 201 |
| 121 | | otherwise = 413 |. |
| 122 | | otherwise = 201 |. |
| 123 | | otherwise = 405 |
| 124 | ``` |
| 125 | |
| 126 | Fixa't en com n'és, de net: |
| 127 | - En un arbre simple d'1 nivell (o al final de tota la funció) **mai no cal posar `|.`** (seria soroll innecessari). |
| 128 | - Només escrius `|.` en tancar una branca imbricada quan a sota continua una branca d'un nivell superior. |
| 129 | |
| 130 | ### Blocs de diverses línies dins d'una guarda (`| cond =` + bloc sagnat) |
| 131 | Igual que després de l'`=` d'una funció normal pots obrir un bloc sagnat de diverses línies, **després de l'`=` de qualsevol guarda `| cond =` o `| otherwise =` també pots fer un salt de línia i obrir un bloc seqüencial**: |
| 132 | |
| 133 | ```ridgun |
| 134 | ajustar finestra Int Int => Int |
| 135 | ajustar finestra idx limit = |
| 136 | | idx >= limit = |
| 137 | topall = limit - 1 |
| 138 | escalat = topall * 2 |
| 139 | escalat + 10 |
| 140 | | otherwise = |
| 141 | seguent = idx + 1 |
| 142 | seguent * 2 |
| 143 | ``` |
| 144 | |
| 145 | Cada branca de la guarda té el seu propi bloc local net i la seva última línia és el valor que retorna aquella branca. |
| 146 | |
| 147 | --- |
| 148 | |
| 149 | ## 4.3. "Sense `while`? I no rebentarà la pila si recorro un fitxer gran?" |
| 150 | |
| 151 | Parlem de l'elefant a l'habitació. |
| 152 | |
| 153 | Si véns de Python, Java o JavaScript, t'han ensenyat des de ben petit que *"la recursió és elegant a la pissarra de la universitat, però al món real desborda la pila (`StackOverflowError`) a les 1.000 voltes; fes servir un bucle `while`"*. |
| 154 | |
| 155 | Això és veritat en llenguatges que **no** optimitzen les crides de cua. Però en un llenguatge funcional de sistemes com Ridgun, **una recursió de cua ÉS un bucle `while` disfressat de funció pura**. |
| 156 | |
| 157 | ### Quina diferència hi ha entre una recursió normal i una recursió de cua? |
| 158 | Fixa't en aquestes dues funcions que compten cap enrere des de `n` fins a `0` sumant els números: |
| 159 | |
| 160 | ```ridgun |
| 161 | -- Versió A: Recursió NORMAL (deixa feina pendent per a la tornada) |
| 162 | sumar lent Int => Int |
| 163 | sumar lent 0 = 0 |
| 164 | sumar lent n = n + (sumar lent (n - 1)) |
| 165 | |
| 166 | -- Versió B: Recursió DE CUA (porta el compte al dia a `acumulat`) |
| 167 | sumar rapid loop Int Int => Int |
| 168 | sumar rapid loop 0 acumulat = acumulat |
| 169 | sumar rapid loop n acumulat = sumar rapid loop (n - 1) (acumulat + n) |
| 170 | ``` |
| 171 | |
| 172 | - A la **Versió A**, abans de poder sumar `n + ...`, la funció ha d'esperar que acabi `sumar lent (n - 1)`. Això obliga l'ordinador a desar `n` a la pila en cada pas. |
| 173 | - A la **Versió B**, la crida a `sumar rapid loop` **és l'última cosa que passa a la línia**. No queda res pendent darrere seu. Ja li passes la suma actualitzada a `acumulat + n`. |
| 174 | |
| 175 | ### El truc de màgia de `ridgunc` (TCO: *Tail-Call Optimization*) |
| 176 | Quan `ridgunc` compila `sumar rapid loop`, veu que la funció es crida a si mateixa al final de la clàusula i diu: |
| 177 | |
| 178 | > *"Per a què crearé un marc de pila nou si l'actual ja no serveix per a res? Sobreescriuré `n` amb `n - 1`, sobreescriuré `acumulat` amb `acumulat + n` i saltaré al principi amb un `loop { ... continue; }` natiu."* |
| 179 | |
| 180 | Resultat: |
| 181 | 1. Pots cridar `sumar rapid loop 100_000_000 0` (cent milions de voltes) i **gastarà exactament 0 bytes extra de pila**. |
| 182 | 2. Va igual de ràpid que un bucle `while` escrit a mà en Rust o C, però sense haver hagut de declarar ni una sola variable mutable al teu codi Ridgun. |
| 183 | |
| 184 | --- |
| 185 | |
| 186 | ## 4.4. El patró mestre que faràs servir en tot programa Ridgun |
| 187 | |
| 188 | Cada vegada que en un altre llenguatge pensaries *"faré un bucle `while` amb un índex `i` i un acumulador"*, en Ridgun escrius una parella de funcions com aquesta (la funció principal neta i el seu ajudant `_loop` de cua): |
| 189 | |
| 190 | ```ridgun |
| 191 | cercar caracter loop String Int Int Int => Int |
| 192 | cercar caracter loop text pos fi cercat = |
| 193 | | pos >= fi = -1 |
| 194 | | (str char at text pos) == cercat = pos |
| 195 | | otherwise = cercar caracter loop text (pos + 1) fi cercat |
| 196 | |
| 197 | cercar caracter String Int => Int |
| 198 | cercar caracter text cercat = cercar caracter loop text 0 (str len text) cercat |
| 199 | ``` |
| 200 | |
| 201 | Mira les tres guardes de `cercar caracter loop`: |
| 202 | 1. He arribat al final (`pos >= fi`)? Retorno `-1` (fi del bucle). |
| 203 | 2. He trobat el caràcter a `pos`? Retorno `pos` (fi del bucle). |
| 204 | 3. No és aquest? Passo al següent (`pos + 1`) amb una crida de cua (que el compilador converteix en `continue;`). |
| 205 | |
| 206 | Net, impossible de tenir *bugs* de variables globals trepitjades i ràpid com un llamp. |
| 207 | |
| 208 | Al **Capítol 05** posarem fil a l'agulla amb les col·leccions de dades: **llistes, tuples, funcions que accepten infinits arguments (`...`) i les màscares de seqüències estil J/APL**. |