Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítulo 04: Adeus ao `if/else`, adeus ao `while` (Pattern Matching, Gardas e TCO) |
| 2 | |
| 3 | Cando lle contas a alguén que en Ridgun **non existe a palabra chave `if`, nin `else`, nin `switch`, nin `for`, nin `while`**, adoita mirarte coma se lle dixeses que vas construír unha casa sen martelo nin cravos: |
| 4 | |
| 5 | > *"Que non hai `if` nin `while`? E como demo tomo unha decisión ou percorro un millón de datos?"* |
| 6 | |
| 7 | Pois resulta que `if/else` e `while` son ferramentas bastante toscas herdadas de como funcionaban os saltos (`GOTO`) nos anos 50. En Ridgun substituímolos por tres pezas que encaixan coma unha luva: |
| 8 | 1. ***Pattern Matching*** (cando decides segundo a **forma ou valor exacto** do dato). |
| 9 | 2. **Gardas booleanas `|`** (cando decides segundo unha **condición ou rango**). |
| 10 | 3. **Recursión de cola con TCO** (que o compilador converte por baixo nun bucle `loop` nativo ultrarrápido). |
| 11 | |
| 12 | Imos velas en acción. |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## 4.1. *Pattern Matching*: Cando o argumento xa che dá a resposta |
| 17 | |
| 18 | No capítulo anterior xa vimos un adianto. En lugar de recibir unha variable xenérica `x` e abrir dentro da función cinco `if (x == ...)`, en Ridgun pos os casos directamente na porta de entrada da función: |
| 19 | |
| 20 | ```ridgun |
| 21 | codigo http Int => String |
| 22 | codigo http 200 = "OK: Todo foi sobre rodas" |
| 23 | codigo http 301 = "Movido permanentemente" |
| 24 | codigo http 404 = "Non atopado (miraches debaixo do sofá?)" |
| 25 | codigo http 500 = "Lume no servidor" |
| 26 | codigo http n = "Código descoñecido: {n}" |
| 27 | ``` |
| 28 | |
| 29 | Ridgun proba as filas **de arriba abaixo**: |
| 30 | - Se entra `404`, salta directo á terceira fila. |
| 31 | - Se entra `418`, ningunha das catro primeiras filas coincide, así que cae na última (`n`), que atrapa calquera enteiro e o nomea `n` para que poidas imprimilo. |
| 32 | - E se no último caso nin sequera che importa que número era? Pos un guión baixo `_` (*comodín*) e esquéceste: `codigo http _ = "Outro código"`. |
| 33 | |
| 34 | --- |
| 35 | |
| 36 | ## 4.2. Gardas booleanas (`|`): O teu bisturí para rangos, variables condicionais e árbores de decisión |
| 37 | |
| 38 | *"Vale, o pattern matching mola cando busco o `200` ou o `404`. Pero que fago se quero comprobar se `nota >= 5` ou se `idade > 18 and idade < 65`? Non vou escribir 47 filas con todos os números do 18 ao 65!"* |
| 39 | |
| 40 | Claro que non! Para iso están as **gardas (`|`)**. |
| 41 | |
| 42 | Cando o resultado depende dunha desigualdade ou dunha combinación lóxica, despois do `=` da función (ou de calquera variable local) abres unha escaleira de barras verticais `|`: |
| 43 | |
| 44 | ```ridgun |
| 45 | cualificar Int => String |
| 46 | cualificar nota = |
| 47 | | nota < 0 = "Nota imposible (puntos negativos?)" |
| 48 | | nota < 5 = "Suspenso" |
| 49 | | nota < 7 = "Aprobado" |
| 50 | | nota < 9 = "Notable" |
| 51 | | nota <= 10 = "Sobresaliente" |
| 52 | | otherwise = "Pasácheste da escala" |
| 53 | ``` |
| 54 | |
| 55 | Mírao ben: |
| 56 | - O signo `=` da cabeceira (`cualificar nota =`) alíñase debaixo de `=>` igual ca en calquera outra función. |
| 57 | - Debaixo lese de arriba abaixo igual ca unha táboa de tarifas: cada liña `| condicion = resultado` compróbase en orde. En canto unha dá `True`, devolve ese resultado e rematou. |
| 58 | - Ao final pos **`| otherwise = ...`**, que significa literalmente *"en calquera outro caso"* (a túa rede de seguridade para que nunca quede un camiño sen retorno). |
| 59 | |
| 60 | ### Lóxica booleana en minúsculas claras (`and`, `or`, `xor`, `not`) |
| 61 | Fixácheste en que moitas linguaxes usan `|` para bits, `||` para booleanos, `!` para negar... e cando les unha condición complexa parece que un gato camiñou sobre o teclado (`if (!a && (b || c))`)? |
| 62 | |
| 63 | En Ridgun, como `|` xa é o símbolo sagrado das gardas e dos tipos alxébricos, e `||` é o operador de paralelismo, **os operadores booleanos escríbense con palabras claras en minúsculas**: |
| 64 | |
| 65 | - **`and`**: cúmprense as dúas cousas con avaliación en curtocircuíto (`idade >= 18 and ten_carne`). |
| 66 | - **`or`**: cúmprese polo menos unha con avaliación en curtocircuíto (`dia == "sabado" or dia == "domingo"`). |
| 67 | - **`xor`**: cúmprese unha ou outra, pero non as dúas á vez. |
| 68 | - **`not`**: inverte o booleano (`not (esta baleiro texto)`). |
| 69 | - **`<`, `>`, `<=`, `>=`**: comparan tanto números (`Int`, `Float`) como cadeas **`String` en orde lexicográfica UTF-8** (`"alfa" < "beta"`). |
| 70 | |
| 71 | Mira que limpo queda o clásico problema dos anos bisestos: |
| 72 | |
| 73 | ```ridgun |
| 74 | e bisesto Int => Bool |
| 75 | e bisesto a = |
| 76 | | (a % 400) == 0 = True |
| 77 | | (a % 4) == 0 and (a % 100) != 0 = True |
| 78 | | otherwise = False |
| 79 | ``` |
| 80 | |
| 81 | Cero símbolos crípticos. Leo alguén que non sabe Ridgun e enténdeo á primeira. |
| 82 | |
| 83 | ### Variables locais condicionais (`x =` + `|`) |
| 84 | Como en Ridgun todo `=` segue a mesma regra, **tamén podes usar `|` para asignar unha variable local** sen ter que crear unha función auxiliar á parte: |
| 85 | |
| 86 | ```ridgun |
| 87 | prezo billete Int Bool => Int |
| 88 | prezo billete base e_familia_numerosa = |
| 89 | desconto = |
| 90 | | e_familia_numerosa = 20 |
| 91 | | base > 100 = 10 |
| 92 | | otherwise = 0 |
| 93 | base - (base * desconto / 100) |
| 94 | ``` |
| 95 | |
| 96 | ### Árbores de decisión aniñadas e o limitador compacto `|.` |
| 97 | *"E se dentro dunha rama `| cond =` ou `| otherwise =` quero abrir outra sub-árbore de gardas `|` de 3 ou 4 niveis de profundidade e logo volver a un nivel superior?"* |
| 98 | |
| 99 | Podes aniñar `|` dentro de `|` tantos niveis como queiras. E cando un bloque `|` aniñado remata pero debaixo seguen máis ramas dun nivel pai (ou máis sentenzas do bloque), colocas **`|.`** ao final da súa última liña (`| otherwise = valor |.`) para indicar sen ningunha ambigüidade que esa sub-árbore se pecha e volves ao nivel anterior: |
| 100 | |
| 101 | ```ridgun |
| 102 | encaminar peticion Int Int Bool Bool Int Int => Int |
| 103 | encaminar peticion metodo rol asinada e_admin tamano_kb limitado = |
| 104 | | metodo == 0 = |
| 105 | | tamano_kb == 0 = 204 |
| 106 | | otherwise = 200 |. |
| 107 | | metodo == 1 = |
| 108 | | not asinada = 401 |
| 109 | | otherwise = |
| 110 | -- Nivel 2: petición POST asinada |
| 111 | | limitado > 0 = |
| 112 | -- Nivel 3: control de rate-limit |
| 113 | | e_admin = |
| 114 | -- Nivel 4: excepción para administradores |
| 115 | | tamano_kb > 10000 = 413 |
| 116 | | otherwise = 202 |. |
| 117 | | otherwise = 429 |. |
| 118 | | tamano_kb > 1000 = |
| 119 | -- Nivel 3: volvemos desde o Nivel 4 grazas a `|.` |
| 120 | | rol >= 2 and e_admin = 201 |
| 121 | | otherwise = 413 |. |
| 122 | | otherwise = 201 |. |
| 123 | | otherwise = 405 |
| 124 | ``` |
| 125 | |
| 126 | Fíxate no limpo que é: |
| 127 | - Nunha árbore simple de 1 nivel (ou ao final de toda a función) **nunca fai falta poñer `|.`** (sería ruído innecesario). |
| 128 | - Só escribes `|.` ao pechar unha rama aniñada cando debaixo continúa unha rama dun nivel superior. |
| 129 | |
| 130 | ### Bloques de varias liñas dentro dunha garda (`| cond =` + bloque sangrado) |
| 131 | Igual que despois do `=` dunha función normal podes abrir un bloque sangrado de varias liñas, **despois do `=` de calquera garda `| cond =` ou `| otherwise =` tamén podes facer un salto de liña e abrir un bloque secuencial**: |
| 132 | |
| 133 | ```ridgun |
| 134 | axustar fiestra Int Int => Int |
| 135 | axustar fiestra idx limite = |
| 136 | | idx >= limite = |
| 137 | tope = limite - 1 |
| 138 | escalado = tope * 2 |
| 139 | escalado + 10 |
| 140 | | otherwise = |
| 141 | seguinte = idx + 1 |
| 142 | seguinte * 2 |
| 143 | ``` |
| 144 | |
| 145 | Cada rama da garda ten o seu propio bloque local limpo e a súa última liña é o valor que devolve esa rama. |
| 146 | |
| 147 | --- |
| 148 | |
| 149 | ## 4.3. "Sen `while`? E non vai rebentar a pila se percorro un ficheiro grande?" |
| 150 | |
| 151 | Falemos do elefante na sala. |
| 152 | |
| 153 | Se vés de Python, Java ou JavaScript, ensináronche desde pequeniño que *"a recursión é elegante no encerado da universidade, pero no mundo real desborda a pila (`StackOverflowError`) ás 1.000 voltas; usa un bucle `while`"*. |
| 154 | |
| 155 | Iso é verdade en linguaxes que **non** optimizan as chamadas de cola. Pero nunha linguaxe funcional de sistemas como Ridgun, **unha recursión de cola É un bucle `while` disfrazado de función pura**. |
| 156 | |
| 157 | ### Que diferenza hai entre unha recursión normal e unha recursión de cola? |
| 158 | Fíxate nestas dúas funcións que contan cara a atrás desde `n` ata `0` sumando os números: |
| 159 | |
| 160 | ```ridgun |
| 161 | -- Versión A: Recursión NORMAL (deixa traballo pendente para a volta) |
| 162 | sumar lento Int => Int |
| 163 | sumar lento 0 = 0 |
| 164 | sumar lento n = n + (sumar lento (n - 1)) |
| 165 | |
| 166 | -- Versión B: Recursión DE COLA (leva a conta ao día en `acumulado`) |
| 167 | sumar rapido loop Int Int => Int |
| 168 | sumar rapido loop 0 acumulado = acumulado |
| 169 | sumar rapido loop n acumulado = sumar rapido loop (n - 1) (acumulado + n) |
| 170 | ``` |
| 171 | |
| 172 | - Na **Versión A**, antes de poder sumar `n + ...`, a función ten que agardar a que remate `sumar lento (n - 1)`. Iso obriga ao ordenador a gardar `n` na pila en cada paso. |
| 173 | - Na **Versión B**, a chamada a `sumar rapido loop` **é o último que ocorre na liña**. Non queda nada pendente detrás dela. Xa lle pasas a suma actualizada en `acumulado + n`. |
| 174 | |
| 175 | ### O truco de maxia de `ridgunc` (TCO: *Tail-Call Optimization*) |
| 176 | Cando `ridgunc` compila `sumar rapido loop`, ve que a función se chama a si mesma ao final da cláusula e di: |
| 177 | |
| 178 | > *"Para que vou crear un marco de pila novo se o actual xa non serve para nada? Vou sobrescribir `n` con `n - 1`, sobrescribir `acumulado` con `acumulado + n` e saltar ao principio cun `loop { ... continue; }` nativo."* |
| 179 | |
| 180 | Resultado: |
| 181 | 1. Podes chamar a `sumar rapido loop 100_000_000 0` (cen millóns de voltas) e **gastará exactamente 0 bytes extra de pila**. |
| 182 | 2. Vai igual de rápido ca un bucle `while` escrito á man en Rust ou C, pero sen ter que declarar nin unha soa variable mutable no teu código Ridgun. |
| 183 | |
| 184 | --- |
| 185 | |
| 186 | ## 4.4. O patrón mestre que usarás en todo programa Ridgun |
| 187 | |
| 188 | Cada vez que noutra linguaxe pensarías *"vou facer un bucle `while` cun índice `i` e un acumulador"*, en Ridgun escribes unha parella de funcións coma esta (a función principal limpa e a súa axudante `_loop` de cola): |
| 189 | |
| 190 | ```ridgun |
| 191 | buscar caracter loop String Int Int Int => Int |
| 192 | buscar caracter loop texto pos fin buscado = |
| 193 | | pos >= fin = -1 |
| 194 | | (str char at texto pos) == buscado = pos |
| 195 | | otherwise = buscar caracter loop texto (pos + 1) fin buscado |
| 196 | |
| 197 | buscar caracter String Int => Int |
| 198 | buscar caracter texto buscado = buscar caracter loop texto 0 (str len texto) buscado |
| 199 | ``` |
| 200 | |
| 201 | Mira as tres gardas de `buscar caracter loop`: |
| 202 | 1. Cheguei ao final (`pos >= fin`)? Devolvo `-1` (fin do bucle). |
| 203 | 2. Atopei o carácter en `pos`? Devolvo `pos` (fin do bucle). |
| 204 | 3. Non é este? Paso ao seguinte (`pos + 1`) cunha chamada de cola (que o compilador converte en `continue;`). |
| 205 | |
| 206 | Limpo, imposible de ter *bugs* de variables globais pisadas e rápido coma un raio. |
| 207 | |
| 208 | No **Capítulo 05** ímoslle meter man ás coleccións de datos: **listas, tuplas, funcións que aceptan infinitos argumentos (`...`) e as máscaras de secuencias estilo J/APL**. |