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-y-guardas.md208 lines · 11.0 KBRaw
1# Capítulo 04: Adiós al `if/else`, adiós al `while` (Pattern Matching, Guardas y TCO)
2
3Cuando 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
7Pues 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:
81. ***Pattern Matching*** (cuando decides según la **forma o valor exacto** del dato).
92. **Guardas booleanas `|`** (cuando decides según una **condición o rango**).
103. **Recursión de cola con TCO** (que el compilador convierte por debajo en un bucle `loop` nativo ultrarrápido).
11
12Vamos a verlas en acción.
13
14---
15
16## 4.1. *Pattern Matching*: Cuando el argumento ya te da la respuesta
17
18En 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
21fn :: 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
29Ridgun 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
42Cuando 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
45calificar Int => String
46calificar 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
55Mí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
63En 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
71Mira qué limpio queda el clásico problema de los años bisiestos:
72
73```ridgun
74fn :: 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
81Cero símbolos crípticos. Lo lee alguien que no sabe Ridgun y lo entiende a la primera.
82
83### Variables locales condicionales (`x =` + `|`)
84Como 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
87fn :: 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
99Puedes 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
102fn :: 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
126Fí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)
131Igual 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
134fn :: 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
145Cada 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
151Hablemos del elefante en la habitación.
152
153Si 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
155Eso 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?
158Fí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)
162fn :: 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`)
167fn :: 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*)
176Cuando `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
180Resultado:
1811. Puedes llamar a `sumar rapido bucle 100_000_000 0` (cien millones de vueltas) y **gastará exactamente 0 bytes extra de pila**.
1822. 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
188Cada 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
191fn :: 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
197fn :: buscar caracter String Int => Int
198 buscar caracter texto buscado = buscar caracter bucle texto 0 (str len texto) buscado
199```
200
201Mira las tres guardas de `buscar caracter bucle`:
2021. ¿Llegué al final (`pos >= fin`)? Devuelvo `-1` (fin del bucle).
2032. ¿Encontré el carácter en `pos`? Devuelvo `pos` (fin del bucle).
2043. ¿No es este? Paso al siguiente (`pos + 1`) con una llamada de cola (que el compilador convierte en `continue;`).
205
206Limpio, imposible de tener *bugs* de variables globales pisadas y rápido como un rayo.
207
208En 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**.