Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítulo 04: Adiós al `if/else`, adiós al `while` (Pattern Matching, Guardas y TCO) |
| 2 | |
| 3 | Cuando le cuentas a alguien que en Ridgun **no existe la palabra clave `if`, ni `else`, ni `switch`, ni `for`, ni `while`**, suele mirarte como si le hubieras dicho que vas a construir una casa sin martillo ni clavos: |
| 4 | |
| 5 | > *"¿Que no hay `if` ni `while`? ¿Y cómo narices tomo una decisión o recorro un millón de datos?"* |
| 6 | |
| 7 | Pues resulta que `if/else` y `while` son herramientas bastante toscas heredadas de cómo funcionaban los saltos (`GOTO`) en los años 50. En Ridgun los sustituimos por tres piezas que encajan como un guante: |
| 8 | 1. ***Pattern Matching*** (cuando decides según la **forma o valor exacto** del dato). |
| 9 | 2. **Guardas booleanas `|`** (cuando decides según una **condición o rango**). |
| 10 | 3. **Recursión de cola con TCO** (que el compilador convierte por debajo en un bucle `loop` nativo ultrarrápido). |
| 11 | |
| 12 | Vamos a verlas en acción. |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## 4.1. *Pattern Matching*: Cuando el argumento ya te da la respuesta |
| 17 | |
| 18 | En el capítulo anterior ya vimos un adelanto. En lugar de recibir una variable genérica `x` y abrir dentro de la función cinco `if (x == ...)`, en Ridgun pones los casos directamente en la puerta de entrada de la función: |
| 19 | |
| 20 | ```ridgun |
| 21 | fn :: codigo http Int => String |
| 22 | codigo http 200 = "OK: Todo fue sobre ruedas" |
| 23 | codigo http 301 = "Movido permanentemente" |
| 24 | codigo http 404 = "No encontrado (¿has mirado debajo del sofá?)" |
| 25 | codigo http 500 = "Fuego en el servidor" |
| 26 | codigo http n = "Código desconocido: {n}" |
| 27 | ``` |
| 28 | |
| 29 | Ridgun prueba las filas **de arriba abajo**: |
| 30 | - Si entra `404`, salta directo a la tercera fila. |
| 31 | - Si entra `418`, ninguna de las cuatro primeras filas coincide, así que cae en la última (`n`), que atrapa cualquier entero y lo nombra `n` para que puedas imprimirlo. |
| 32 | - ¿Y si en el último caso ni siquiera te importa qué número era? Pones un guion bajo `_` (*comodín*) y te olvidas: `codigo http _ = "Otro código"`. |
| 33 | |
| 34 | --- |
| 35 | |
| 36 | ## 4.2. Guardas booleanas (`|`): Tu bisturí para rangos, variables condicionales y árboles de decisión |
| 37 | |
| 38 | *"Vale, el pattern matching mola cuando busco el `200` o el `404`. ¿Pero qué hago si quiero comprobar si `nota >= 5` o si `edad > 18 and edad < 65`? ¡No voy a escribir 47 filas con todos los números del 18 al 65!"* |
| 39 | |
| 40 | ¡Claro que no! Para eso están las **guardas (`|`)**. |
| 41 | |
| 42 | Cuando el resultado depende de una desigualdad o de una combinación lógica, después del `=` de la función (o de cualquier variable local) abres una escalera de barras verticales `|`: |
| 43 | |
| 44 | ```ridgun |
| 45 | calificar Int => String |
| 46 | calificar 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 = "Te has pasado de la escala" |
| 53 | ``` |
| 54 | |
| 55 | Míralo bien: |
| 56 | - El signo `=` del encabezado (`calificar nota =`) se alinea debajo de `=>` igual que en cualquier otra función. |
| 57 | - Debajo se lee de arriba abajo igual que una tabla de tarifas: cada línea `| condicion = resultado` se comprueba en orden. En cuanto una da `True`, devuelve ese resultado y se acabó. |
| 58 | - Al final pones **`| otherwise = ...`**, que significa literalmente *"en cualquier otro caso"* (tu red de seguridad para que nunca quede un camino sin retorno). |
| 59 | |
| 60 | ### Lógica booleana en minúsculas claras (`and`, `or`, `xor`, `not`) |
| 61 | ¿Te has fijado en que muchos lenguajes usan `|` para bits, `||` para booleanos, `!` para negar... y cuando lees una condición compleja parece que un gato ha caminado sobre el teclado (`if (!a && (b || c))`)? |
| 62 | |
| 63 | En Ridgun, como `|` ya es el símbolo sagrado de las guardas y de los tipos algebraicos, y `||` es el operador de paralelismo, **los operadores booleanos se escriben con palabras claras en minúsculas**: |
| 64 | |
| 65 | - **`and`**: se cumplen las dos cosas con evaluación en cortocircuito (`edad >= 18 and tiene_carnet`: si el lado izquierdo es `False`, el derecho ni siquiera se evalúa). |
| 66 | - **`or`**: se cumple al menos una con evaluación en cortocircuito (`dia == "sabado" or dia == "domingo": si el lado izquierdo es `True`, el derecho no se evalúa). |
| 67 | - **`xor`**: se cumple una u otra, pero no las dos a la vez. |
| 68 | - **`not`**: invierte el booleano (`not (esta vacio texto)`). |
| 69 | - **`<`, `>`, `<=`, `>=`**: comparan tanto números (`Int`, `Float`) como cadenas de texto **`String` en orden lexicográfico UTF-8** (`"alfa" < "beta"`). |
| 70 | |
| 71 | Mira qué limpio queda el clásico problema de los años bisiestos: |
| 72 | |
| 73 | ```ridgun |
| 74 | fn :: es bisiesto Int => Bool |
| 75 | es bisiesto 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. Lo lee alguien que no sabe Ridgun y lo entiende a la primera. |
| 82 | |
| 83 | ### Variables locales condicionales (`x =` + `|`) |
| 84 | Como en Ridgun todo `=` sigue la misma regla, **también puedes usar `|` para asignar una variable local** sin tener que crear una función auxiliar aparte: |
| 85 | |
| 86 | ```ridgun |
| 87 | fn :: precio billete Int Bool => Int |
| 88 | precio billete base vip = |
| 89 | descuento = |
| 90 | | vip = 20 |
| 91 | | base > 100 = 10 |
| 92 | | otherwise = 0 |
| 93 | base - (base * descuento / 100) |
| 94 | ``` |
| 95 | |
| 96 | ### Árboles de decisión anidados y el limitador compacto `|.` |
| 97 | *"¿Y si dentro de una rama `| cond =` o `| otherwise =` quiero abrir otro sub-árbol de guardas `|` de 3 o 4 niveles de profundidad y luego volver a un nivel superior?"* |
| 98 | |
| 99 | Puedes anidar `|` dentro de `|` tantos niveles como quieras. Y cuando un bloque `|` anidado termina pero debajo siguen más ramas de un nivel padre (o más sentencias del bloque), colocas **`|.`** al final de su última línea (`| otherwise = valor |.`) para indicar sin ninguna ambigüedad que ese sub-árbol se cierra y vuelves al nivel anterior: |
| 100 | |
| 101 | ```ridgun |
| 102 | fn :: enrutar peticion Int Int Bool Bool Int Int => Int |
| 103 | enrutar peticion metodo rol firmada admin kb lim = |
| 104 | | metodo == 0 = |
| 105 | | kb == 0 = 204 |
| 106 | | otherwise = 200 |. |
| 107 | | metodo == 1 = |
| 108 | | not firmada = 401 |
| 109 | | otherwise = |
| 110 | -- Nivel 2: petición POST firmada |
| 111 | | lim > 0 = |
| 112 | -- Nivel 3: control de rate-limit |
| 113 | | admin = |
| 114 | -- Nivel 4: excepción para administradores |
| 115 | | kb > 10000 = 413 |
| 116 | | otherwise = 202 |. |
| 117 | | otherwise = 429 |. |
| 118 | | kb > 1000 = |
| 119 | -- Nivel 3: hemos vuelto desde el Nivel 4 gracias a `|.` |
| 120 | | rol >= 2 and admin = 201 |
| 121 | | otherwise = 413 |. |
| 122 | | otherwise = 201 |. |
| 123 | | otherwise = 405 |
| 124 | ``` |
| 125 | |
| 126 | Fíjate en lo limpio que es: |
| 127 | - En un árbol simple de 1 nivel (o al final de toda la función) **nunca hace falta poner `|.`** (sería ruido innecesario). |
| 128 | - Solo escribes `|.` al cerrar una rama anidada cuando debajo continúa una rama de un nivel superior. |
| 129 | |
| 130 | ### Bloques de varias líneas dentro de una guarda (`| cond =` + bloque indentado) |
| 131 | Igual que después del `=` de una función normal puedes abrir un bloque indentado de varias líneas, **después del `=` de cualquier guarda `| cond =` o `| otherwise =` también puedes hacer un salto de línea y abrir un bloque secuencial**: |
| 132 | |
| 133 | ```ridgun |
| 134 | fn :: ajustar ventana Int Int => Int |
| 135 | ajustar ventana idx limite = |
| 136 | | idx >= limite = |
| 137 | tope = limite - 1 |
| 138 | escalado = tope * 2 |
| 139 | escalado + 10 |
| 140 | | otherwise = |
| 141 | siguiente = idx + 1 |
| 142 | siguiente * 2 |
| 143 | ``` |
| 144 | |
| 145 | Cada rama de la guarda tiene su propio bloque local limpio y su última línea es el valor que devuelve esa rama. |
| 146 | |
| 147 | --- |
| 148 | |
| 149 | ## 4.3. "¿Sin `while`? ¿Y no va a reventar la pila si recorro un archivo grande?" |
| 150 | |
| 151 | Hablemos del elefante en la habitación. |
| 152 | |
| 153 | Si vienes de Python, Java o JavaScript, te han enseñado desde pequeñito que *"la recursión es elegante en la pizarra de la universidad, pero en el mundo real desborda la pila (`StackOverflowError`) a las 1.000 vueltas; usa un bucle `while`"*. |
| 154 | |
| 155 | Eso es verdad en lenguajes que **no** optimizan las llamadas de cola. Pero en un lenguaje funcional de sistemas como Ridgun, **una recursión de cola ES un bucle `while` disfrazado de función pura**. |
| 156 | |
| 157 | ### ¿Qué diferencia hay entre una recursión normal y una recursión de cola? |
| 158 | Fíjate en estas dos funciones que cuentan hacia atrás desde `n` hasta `0` sumando los números: |
| 159 | |
| 160 | ```ridgun |
| 161 | -- Versión A: Recursión NORMAL (deja trabajo pendiente para la vuelta) |
| 162 | fn :: 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 (lleva la cuenta al día en `acumulado`) |
| 167 | fn :: sumar rapido bucle Int Int => Int |
| 168 | sumar rapido bucle 0 acumulado = acumulado |
| 169 | sumar rapido bucle n acumulado = sumar rapido bucle (n - 1) (acumulado + n) |
| 170 | ``` |
| 171 | |
| 172 | - En la **Versión A**, antes de poder sumar `n + ...`, la función tiene que esperar a que termine `sumar lento (n - 1)`. Eso obliga al ordenador a guardar `n` en la pila en cada paso. |
| 173 | - En la **Versión B**, la llamada a `sumar rapido bucle` **es lo último que ocurre en la línea**. No queda nada pendiente detrás de ella. Ya le pasas la suma actualizada en `acumulado + n`. |
| 174 | |
| 175 | ### El truco de magia de `ridgunc` (TCO: *Tail-Call Optimization*) |
| 176 | Cuando `ridgunc` compila `sumar rapido bucle`, ve que la función se llama a sí misma al final de la cláusula y dice: |
| 177 | |
| 178 | > *"¿Para qué voy a crear un marco de pila nuevo si el actual ya no sirve para nada? Voy a sobrescribir `n` con `n - 1`, sobrescribir `acumulado` con `acumulado + n` y saltar al principio con un `loop { ... continue; }` nativo."* |
| 179 | |
| 180 | Resultado: |
| 181 | 1. Puedes llamar a `sumar rapido bucle 100_000_000 0` (cien millones de vueltas) y **gastará exactamente 0 bytes extra de pila**. |
| 182 | 2. Va igual de rápido que un bucle `while` escrito a mano en Rust o C, pero sin haber tenido que declarar ni una sola variable mutable en tu código Ridgun. |
| 183 | |
| 184 | --- |
| 185 | |
| 186 | ## 4.4. El patrón maestro que usarás en todo programa Ridgun |
| 187 | |
| 188 | Cada vez que en otro lenguaje pensarías *"voy a hacer un bucle `while` con un índice `i` y un acumulador"*, en Ridgun escribes una pareja de funciones como esta (la función principal limpia y su ayudante de cola): |
| 189 | |
| 190 | ```ridgun |
| 191 | fn :: buscar caracter bucle String Int Int Int => Int |
| 192 | buscar caracter bucle texto pos fin buscado = |
| 193 | | pos >= fin = -1 |
| 194 | | (str char at texto pos) == buscado = pos |
| 195 | | otherwise = buscar caracter bucle texto (pos + 1) fin buscado |
| 196 | |
| 197 | fn :: buscar caracter String Int => Int |
| 198 | buscar caracter texto buscado = buscar caracter bucle texto 0 (str len texto) buscado |
| 199 | ``` |
| 200 | |
| 201 | Mira las tres guardas de `buscar caracter bucle`: |
| 202 | 1. ¿Llegué al final (`pos >= fin`)? Devuelvo `-1` (fin del bucle). |
| 203 | 2. ¿Encontré el carácter en `pos`? Devuelvo `pos` (fin del bucle). |
| 204 | 3. ¿No es este? Paso al siguiente (`pos + 1`) con una llamada de cola (que el compilador convierte en `continue;`). |
| 205 | |
| 206 | Limpio, imposible de tener *bugs* de variables globales pisadas y rápido como un rayo. |
| 207 | |
| 208 | En el **Capítulo 05** vamos a meterle mano a las colecciones de datos: **listas, tuplas, funciones que aceptan infinitos argumentos (`...`) y las máscaras de secuencias estilo J/APL**. |