Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítulo 07: Funciones de Orden Superior y Paralelismo sin Sudor (Ni `Mutex`, ni `Deadlocks`) |
| 2 | |
| 3 | En casi todos los lenguajes de programación hay un momento dramático en la vida de un proyecto: el día en que alguien dice en una reunión: |
| 4 | |
| 5 | > *"Esto va lento. Hay que paralelizarlo con hilos."* |
| 6 | |
| 7 | Se hace el silencio en la sala. Todo el mundo sabe lo que viene después: semanas cazando *race conditions* fantasma que solo ocurren los martes con luna llena, bloqueos mutuos (*deadlocks*) y errores irreproducibles. |
| 8 | |
| 9 | En Ridgun, cuando alguien dice *"hay que paralelizarlo"*, basta con anteponer `||` a tus operaciones de lista (**`||map`** y **`||filter`**) o poner tus cálculos en un bloque paralelo **`||`** con flechas `->` y te vas a tomar un café. |
| 10 | |
| 11 | ¿Por qué? Porque **cuando tus funciones son puras y tus datos son inmutables por defecto, dos núcleos de CPU trabajando a la vez jamás pueden pisarse la manguera**. |
| 12 | |
| 13 | Vamos a verlo paso a paso. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## 7.1. Funciones que reciben funciones: `map`, `||map`, `filter`, `||filter` y `fold` |
| 18 | |
| 19 | En Ridgun una función es un valor más. Puedes pasarla por parámetro igual que pasas el número `7`. |
| 20 | |
| 21 | El lenguaje trae de serie los combinadores de colecciones para transformar listas sin escribir ni una sola recursión manual, tanto en modo secuencial (`map`, `filter`, `fold`) como en modo paralelo multicore con el prefijo `||` (**`||map`**, **`||filter`**): |
| 22 | |
| 23 | ### 1. `map` (secuencial) y `||map` (paralelo multicore) |
| 24 | Supón que tienes una función `costoso Int => Int` y una lista con números: |
| 25 | |
| 26 | ```ridgun |
| 27 | costoso Int => Int |
| 28 | costoso x = (x * x) + (x * 3) |
| 29 | |
| 30 | main => () |
| 31 | main = |
| 32 | datos = [1, 2, 3, 4, 5, 6, 7, 8] |
| 33 | -- Secuencial estricto de izquierda a derecha: |
| 34 | r1 = map costoso datos |
| 35 | -- Paralelo multicore con el prefijo `||`: |
| 36 | r2 = ||map costoso datos |
| 37 | echo r1 |
| 38 | echo r2 |
| 39 | ``` |
| 40 | |
| 41 | - Con **`map`**, Ridgun aplica la función elemento a elemento de izquierda a derecha en el hilo actual (ideal para listas pequeñas o funciones con efectos de entrada/salida ordenados). |
| 42 | - Con **`||map`**, el runtime de Ridgun reparte los elementos de `datos` entre los hilos trabajadores de tu procesador y reconstruye la lista final **exactamente en el mismo orden**, garantizando que `||map f xs == map f xs`. |
| 43 | |
| 44 | ### 2. `filter` (secuencial) y `||filter` (paralelo): Quedarte solo con los que cumplan una condición |
| 45 | Le pasas una función que devuelva `Bool` (un *predicado*) y una lista: |
| 46 | |
| 47 | ```ridgun |
| 48 | fn :: es positivo Int => Bool |
| 49 | es positivo n = n > 0 |
| 50 | |
| 51 | fn :: main => () |
| 52 | main = |
| 53 | limpios = filter es positivo [-5, 10, 0, -2, 42] |
| 54 | limpios_par = ||filter es positivo [-5, 10, 0, -2, 42] |
| 55 | echo limpios -- Imprime [10, 42] |
| 56 | echo limpios_par -- Imprime [10, 42] |
| 57 | ``` |
| 58 | |
| 59 | ### 3. `fold`: Reducir toda una lista a un solo resultado |
| 60 | Le pasas una función combinadora de dos argumentos `(Acumulado Elemento => Acumulado)`, el valor inicial y la lista: |
| 61 | |
| 62 | ```ridgun |
| 63 | fn :: acumular producto Int Int => Int |
| 64 | acumular producto acc x = acc * x |
| 65 | |
| 66 | fn :: main => () |
| 67 | main = |
| 68 | echo (fold acumular producto 1 [2, 3, 4, 5]) -- 1 * 2 * 3 * 4 * 5 = 120 |
| 69 | ``` |
| 70 | |
| 71 | ### ¿Cómo escribo yo mi propia función que reciba otra función? |
| 72 | Muy fácil: en la firma de tipos pones la firma de la función invitada entre paréntesis `(Entrada => Salida)`: |
| 73 | |
| 74 | ```ridgun |
| 75 | fn :: ejecutar dos veces (A => A) A => A |
| 76 | ejecutar dos veces f x = f (f x) |
| 77 | ``` |
| 78 | |
| 79 | ### 4. Clausuras léxicas y funciones anónimas (`(x => expr)`, `(acc x => expr)`) |
| 80 | Cuando quieres pasar una función corta al vuelo o capturar variables locales de tu ámbito actual (como un desplazamiento, un prefijo o una sesión activa) sin declarar una función global auxiliar, usas una **clausura léxica** entre paréntesis con **`=>`**: |
| 81 | |
| 82 | ```ridgun |
| 83 | fn :: procesar con sesion => [Int] |
| 84 | procesar con sesion = |
| 85 | base = 10 |
| 86 | bono = 3 |
| 87 | sumados = map (x => x + base) [1, 2, 3] |
| 88 | filtrados = filter (x => x >= bono) [1, 2, 3, 4] |
| 89 | total = fold (acc x => acc + x + bono) 0 [1, 2, 3] |
| 90 | pares = map ((a b) => a + b + base) [(1, 2), (3, 4)] |
| 91 | sumados :: filtrados :: [total] :: pares |
| 92 | ``` |
| 93 | |
| 94 | Cinco reglas mantienen las lambdas limpias y sin ambigüedad: |
| 95 | - **Paréntesis obligatorios**: toda lambda va siempre entre paréntesis `(... => ...)`. |
| 96 | - **Lambda sin parámetros (`(=> expr)`)**: igual que `fn :: main => ()` no lleva `()` antes de `=>`, una lambda sin parámetros se escribe `(=> expr)` (y su tipo es `(=> Tipo)`), nunca `(() => expr)`. |
| 97 | - **Varios parámetros frente a tupla (`(a b => ...)` vs. `((a b) => ...)`)**: `(acc x => ...)` recibe dos parámetros separados; `((a b) => ...)` recibe una sola tupla y la desestructura. |
| 98 | - **Cuerpo de varias líneas**: si pones un salto de línea tras `=>`, puedes escribir un bloque sangrado de varios pasos antes del `)` de cierre: |
| 99 | ```ridgun |
| 100 | dobles = map (x => |
| 101 | paso = x * 2 |
| 102 | paso + base |
| 103 | ) [1, 2, 3] |
| 104 | ``` |
| 105 | - **Mayúsculas frente a minúsculas**: `(Conn => Conn)` es un tipo de función (inicial mayúscula) y `(conn => conn)` es un valor lambda (inicial minúscula). |
| 106 | |
| 107 | Las variables capturadas (`base`, `bono`) se empaquetan automáticamente con gestión determinista de memoria (`RMM1`/`RMM2`), sin fugas y sin pausas de recolector de basura. |
| 108 | |
| 109 | ### 5. El estilo más Ridgunero (recomendado): Tuberías fluidas con `->`, invocación por la izquierda y bloques sin `_ =` (`FR-COMP-014`) |
| 110 | Escribir `map f xs` o asignar cada paso intermedio a una variable (`r1 = ...`, `r2 = ...`) está muy bien, pero donde Ridgun se disfruta al 100% es encadenando transformaciones de izquierda a derecha con **`->`** y ligando el resultado final directamente con **`-> variable`**: |
| 111 | |
| 112 | ```ridgun |
| 113 | op :: [Int] compact limit Int => [Int] |
| 114 | [] compact limit _ = [] |
| 115 | (x:xs) compact limit lim |
| 116 | | x <= lim = [x] :: (xs compact limit lim) |
| 117 | | otherwise = xs compact limit lim |
| 118 | |
| 119 | fn :: main => () |
| 120 | main = |
| 121 | xs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
| 122 | |
| 123 | -- Estilo recomendado (2b): arranque infijo natural + `->` entre etapas + `-> results` |
| 124 | xs map (x => x * 2) -> filter (x => x > 6) -> compact limit 14 -> results |
| 125 | echo results -- Imprime [8, 10, 12, 14] |
| 126 | ``` |
| 127 | |
| 128 | Ridgun unifica las cuatro formas de escribir la cadena en el mismo árbol sintáctico (AST), para que elijas la que mejor se lea en cada caso: |
| 129 | 1. **Estilo `2b` (recomendado)**: `xs map f -> filter g -> compact limit 14 -> results` (arranque infijo natural con el sujeto a la izquierda, `->` separando cada etapa y `-> results` al final). |
| 130 | 2. **Estilo `2` (`->` desde el valor inicial)**: `xs -> map f -> filter g -> compact limit 14 -> results`. |
| 131 | 3. **Estilo `1` (infijo continuo sin `->` intermedios)**: `xs map f filter g compact limit 14 -> results`. |
| 132 | 4. **Estilo `3` (arranque prefijo clásico + `->`)**: `map f xs -> filter g -> compact limit 14 -> results`. |
| 133 | |
| 134 | Y si la tubería es larga, puedes partirla en varias líneas sangradas que empiezan por `->`: |
| 135 | |
| 136 | ```ridgun |
| 137 | xs map (x => x * 3) |
| 138 | -> filter (x => x >= 12) |
| 139 | -> fold 0 (acc x => acc + x) |
| 140 | -> total |
| 141 | ``` |
| 142 | |
| 143 | La regla universal de `->` es sencilla y consistente en todo el lenguaje: |
| 144 | - **El valor que viene por la izquierda entra como PRIMER argumento** de la función u operador de la derecha (`x -> f a b` $\equiv$ `x f a b` $\equiv$ `f x a b`), salvo en `map`/`filter`/`fold` (donde entra como la colección sobre la que iterar) o si colocas un comodín explícito **`_`** (`payload -> write file ruta_salida _`). |
| 145 | - **Envío directo a agentes**: `[5, 10, 15] -> sumar lista -> send to ag` y `[1, 2, 3] -> sumar lista -> ask to ag -> total`. |
| 146 | - **Máscaras de secuencia en tránsito (`-> (+ _ _ +) ->`)**: `parsear paquete "v0.9.9" -> (+ _ _ +) -> combinar codigo -> resumen`. |
| 147 | - **Bloques secuenciales limpios sin `_ =` y descarte explícito `-> ()`**: las llamadas con efectos laterales (`echo`, `write file`, `shell exec`, `log`, etc.) se escriben directamente sin `_ =`. Si la última línea de una función `=> ()` llama a una función que devuelve un valor (como `Int`), basta con terminar esa línea con **`-> ()`** para descartar el resultado explícitamente y devolver `()`. |
| 148 | |
| 149 | --- |
| 150 | |
| 151 | ## 7.2. La joya de la corona: Bloques `||` con `->` como Grafos Paralelos (DAGs) |
| 152 | |
| 153 | Aquí es donde Ridgun cambia tu forma de pensar. |
| 154 | |
| 155 | En un lenguaje imperativo normal, cuando escribes tres líneas seguidas: |
| 156 | 1. Primero se ejecuta la línea 1. |
| 157 | 2. Luego la línea 2 (aunque no tenga nada que ver con la línea 1). |
| 158 | 3. Luego la línea 3. |
| 159 | |
| 160 | Eso es obligar al ordenador a trabajar con orejeras. En Ridgun, cuando usas barras paralelas **`||`** con flechas **`->`** (sin necesidad de escribir ninguna palabra clave como `where`), no estás dando órdenes secuenciales: estás dibujando un **Grafo Acíclico Dirigido (DAG)** que dice qué cálculo alimenta a qué variable: |
| 161 | |
| 162 | ```ridgun |
| 163 | fn :: fib Int => Int |
| 164 | fib 0 = 0 |
| 165 | fib 1 = 1 |
| 166 | fib n = (fib (n - 1)) + (fib (n - 2)) |
| 167 | |
| 168 | fn :: fact Int => Int |
| 169 | fact 0 = 1 |
| 170 | fact n = n * (fact (n - 1)) |
| 171 | |
| 172 | fn :: super calculo => Int |
| 173 | super calculo = rama_izquierda + rama_derecha |
| 174 | || fib 10 -> rama_izquierda |
| 175 | || fact 6 -> rama_derecha |
| 176 | ``` |
| 177 | |
| 178 | Léelo en voz alta: |
| 179 | - *"Quiero calcular `rama_izquierda + rama_derecha`, mientras en paralelo (`||`) `fib 10` va a `rama_izquierda` (`->`) y `fact 6` va a `rama_derecha` (`->`)."* |
| 180 | |
| 181 | ### Lo que hace `ridgunc` entre bambalinas con tus líneas `||`: |
| 182 | 1. Mira las flechas `->` y arma el árbol de dependencias (DAG). |
| 183 | 2. **Caza ciclos estáticamente**: si por error escribes `|| a + 1 -> b` y `|| b + 1 -> a`, el compilador te dice en el acto *"¡Quietos! Hay una dependencia circular"* y no te deja compilar un cuelgue infinito. |
| 184 | 3. Ve que `fib 10` y `fact 6` no dependen el uno del otro, así que tienen luz verde para correr en paralelo antes de sumarse en `rama_izquierda + rama_derecha`. |
| 185 | |
| 186 | Incluso puedes montar un pipeline de varias plantas dentro del mismo bloque `||` —ya sea al pie de la expresión o justo después del `=`— y el compilador deducirá solito qué plantas pueden ir a la vez y cuáles tienen que esperar: |
| 187 | |
| 188 | ```ridgun |
| 189 | fn :: balance trimestral Int => Int |
| 190 | balance trimestral base = |
| 191 | || base * 100 -> ventas_brutas |
| 192 | || base * 40 -> costes_fijos |
| 193 | || ventas_brutas - costes_fijos -> beneficio_bruto ||. |
| 194 | beneficio_bruto * 80 / 100 |
| 195 | ``` |
| 196 | |
| 197 | Fíjate en el detalle **`||.`** al final de la última línea `||`: igual que `|.` cierra un árbol de guardas anidado, **`||.`** cierra el bloque paralelo `||` cuando debajo siguen sentencias secuenciales normales. Aquí `ventas_brutas` y `costes_fijos` arrancan en paralelo; cuando ambos están listos se calcula `beneficio_bruto`, y finalmente se devuelve `beneficio_bruto * 80 / 100`. Todo sin un solo `async`, `Future`, `Promise` ni `Mutex`. |
| 198 | |
| 199 | --- |
| 200 | |
| 201 | ## 7.3. El atajo `||` sin nombres: Tuberías paralelas que devuelven una tupla |
| 202 | |
| 203 | ¿Y si solo quieres lanzar dos o tres cálculos en paralelo y recogerlos juntos en una tupla sin siquiera inventar nombres de variables con `->`? |
| 204 | |
| 205 | Usas `||` directamente: |
| 206 | |
| 207 | ```ridgun |
| 208 | fn :: par paralelo Int => Int Int |
| 209 | par paralelo n = |
| 210 | || fib n |
| 211 | || fact n |
| 212 | ``` |
| 213 | |
| 214 | Directo, visual y devuelve la tupla `(fib n, fact n)`. |
| 215 | |
| 216 | --- |
| 217 | |
| 218 | ## 7.4. Cuando quieres el volante manual: el Modelo Nativo de Agentes (`agent`, `send ... to`, `receive from`) y Canales CSP |
| 219 | |
| 220 | El paralelismo automático de `||` y `||map` cubre el 90% de los casos. Pero cuando programas un servidor persistente, sesiones de usuario con estado aislado, colas de trabajo o un pipeline productor-consumidor, quieres **agentes concurrentes con memoria aislada y paso de mensajes tipado**. |
| 221 | |
| 222 | En Ridgun no hace falta inventar una sintaxis extraña como `actor ::`: **cualquier función normal (`fn ::` u `op ::`) se convierte en un agente concurrente en una línea con `agent`**: |
| 223 | |
| 224 | ### 1. Agentes sin estado y con estado aislado (`agent`, `send ... to`, `receive from`, `stop`) |
| 225 | - `ag = agent (mi funcion)`: convierte una función `M => R` en un agente sin estado de tipo `Agent M R`. |
| 226 | - `ag = agent (mi funcion con estado) estado_inicial`: convierte una función `S M => S` (o `S M => S R`) en un agente con estado aislado que recuerda su estado mensaje tras mensaje sin un solo `Mutex`. |
| 227 | - `send msg to ag` (o con varios argumentos: `send a b to ag`): envía un mensaje tipado al buzón del agente. |
| 228 | - `res = receive from ag` (o `try receive from ag` sin bloquear): recoge la siguiente respuesta del buzón de salida del agente. |
| 229 | - `stop ag`: detiene el agente y libera su estado automáticamente mediante `RMM1`/`RMM2`. |
| 230 | |
| 231 | ```ridgun |
| 232 | fn :: sumar visita Int Int => Int |
| 233 | sumar visita acum n = acum + n |
| 234 | |
| 235 | fn :: procesar lote Int Int => Int |
| 236 | procesar lote id t = (id * 100) + t |
| 237 | |
| 238 | fn :: main => () |
| 239 | main = |
| 240 | contador = agent (sumar visita) 10 |
| 241 | send 5 to contador |
| 242 | send 7 to contador |
| 243 | v1 = receive from contador |
| 244 | v2 = receive from contador |
| 245 | stop contador |
| 246 | |
| 247 | trabajador = agent (procesar lote) |
| 248 | send 3 25 to trabajador |
| 249 | lote = receive from trabajador |
| 250 | stop trabajador |
| 251 | echo "Visitas:" v1 "->" v2 "| Lote:" lote |
| 252 | ``` |
| 253 | |
| 254 | ### 2. Elección inteligente en tiempo de compilación (*Green Threads* vs. Hilos Reales del Kernel) |
| 255 | Una pregunta habitual es: *"¿Los bloques `||`, `||map` y los agentes `agent` crean siempre hilos del sistema operativo o corren como tareas ligeras?"* |
| 256 | |
| 257 | En Ridgun, **el compilador toma esa decisión por ti analizando estáticamente cada función**: |
| 258 | - **Funciones ligeras (`TierTrivial` / `StratInlineSequential`)**: Si la función que pasas a `||`, `||map`, `||filter` o `agent` tiene poca profundidad (aritmética, formateo de cadenas, transición de estado rápida de una sesión HTTP), calentar hilos del sistema operativo costaría más que el propio cálculo. El compilador la ejecuta como un paso *green-thread* ultraligero sin sobrecarga de `SYS_clone`. |
| 259 | - **Cálculos pesados o recursivos profundos (`TierDynamicRecursive` / `TierHeavyCPU`)**: Si la función tiene recursión profunda (como `fib` o `fact`) o álgebra de tensores (`einsum`, `hadamard`, `@`), el compilador genera automáticamente el despacho multinúcleo sobre **hilos reales del kernel (`SYS_clone`)**, procesando las ramas `||` o la cola de mensajes del `agent` en paralelo real sobre todos tus núcleos de CPU. |
| 260 | |
| 261 | ### 3. Pasar mensajes por canales acotados (`new bounded channel`) |
| 262 | Igual que en Go o Erlang, además de agentes también puedes crear tubos explícitos para comunicar etapas: |
| 263 | |
| 264 | - `new bounded channel capacidad`: crea un canal con espacio para `capacidad` mensajes. |
| 265 | - `send through valor canal` (o `chan send valor canal`): mete un mensaje en el tubo. |
| 266 | - `receive from canal` (o `chan recv canal`): saca el siguiente mensaje del tubo. |
| 267 | - `try receive from canal`: mira si hay mensaje sin quedarse esperando; devuelve `Some valor` o `None`. |
| 268 | - `receive from timeout canal ticks`: espera un límite de tiempo y devuelve `Ok valor` o `Err Timeout`. |
| 269 | |
| 270 | ```ridgun |
| 271 | fn :: main => () |
| 272 | main = |
| 273 | tubo = new bounded channel 4 |
| 274 | send through "Tarea A completada" tubo |
| 275 | send through "Tarea B completada" tubo |
| 276 | primero = receive from tubo |
| 277 | segundo = receive from tubo |
| 278 | echo primero "|" segundo |
| 279 | ``` |
| 280 | |
| 281 | ### 4. Agentes sumidero (*fire-and-forget* `agent sink`), buzones globales (`agent ::`) y pruebas deterministas (`with agent`) |
| 282 | Cuando necesitas auditoría asíncrona, métricas, trazas o *rate limiting* transversal sin ensuciar todas las firmas con un parámetro extra ni acumular respuestas sin leer en una cola de salida, Ridgun ofrece **agentes sumidero (`agent sink`)** y **buzones globales de nivel superior (`agent ::`)**: |
| 283 | - `rec = agent sink (paso) [estado_inicial], mailbox N drop|block, drain 2_s`: procesa cada mensaje en orden FIFO (`S M => S` o `M => ()`), libera el estado anterior con `RMM1`/`RMM2` y **no acumula cola de salida** (`receive from`, `try receive from` y `ask` sobre un `SinkAgent` son errores de compilación). |
| 284 | - `agent :: audit_log = sink (paso) [], mailbox 10_000 drop, drain 2_s`: declara un agente sumidero global inicializado antes de `main` y vaciado ordenadamente al salir del programa. |
| 285 | - `agent dropped ag`, `agent flush ag`, `agent state ag` y `with agent audit_log = espia do ...`: permiten observar mensajes descartados por contrapresión (`drop`), drenar el buzón, inspeccionar el estado y redirigir temporalmente un buzón global a un agente espía aislado en los tests. |
| 286 | - Además, los descriptores de `Channel`, `Agent` y `SinkAgent` pueden compartirse libremente entre múltiples trabajadores (`send ch 1 to w1`, `send ch 2 to w2`), mientras que los datos enviados por el canal mantienen su propiedad afín (`ErrUseAfterSend`). |
| 287 | |
| 288 | Y ahora, abróchate el cinturón para el **Capítulo 08**, el gran final: **tensores con dimensiones verificadas en compilación, variables de ámbito (`context`/`shared`), regiones de memoria deterministas, acceso directo a hardware MMIO y programación de sistemas UNIX/POSIX**. |