Skip to content
Ridgun
English

Ridgun

The language itself: a compiler that compiles itself, the stable project tool, the specification and the book

07-orden-superior-y-paralelismo-dag.md288 lines · 16.6 KBRaw
1# Capítulo 07: Funciones de Orden Superior y Paralelismo sin Sudor (Ni `Mutex`, ni `Deadlocks`)
2
3En 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
7Se 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
9En 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
13Vamos a verlo paso a paso.
14
15---
16
17## 7.1. Funciones que reciben funciones: `map`, `||map`, `filter`, `||filter` y `fold`
18
19En Ridgun una función es un valor más. Puedes pasarla por parámetro igual que pasas el número `7`.
20
21El 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)
24Supón que tienes una función `costoso Int => Int` y una lista con números:
25
26```ridgun
27costoso Int => Int
28costoso x = (x * x) + (x * 3)
29
30main => ()
31main =
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
45Le pasas una función que devuelva `Bool` (un *predicado*) y una lista:
46
47```ridgun
48fn :: es positivo Int => Bool
49 es positivo n = n > 0
50
51fn :: 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
60Le pasas una función combinadora de dos argumentos `(Acumulado Elemento => Acumulado)`, el valor inicial y la lista:
61
62```ridgun
63fn :: acumular producto Int Int => Int
64 acumular producto acc x = acc * x
65
66fn :: 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?
72Muy fácil: en la firma de tipos pones la firma de la función invitada entre paréntesis `(Entrada => Salida)`:
73
74```ridgun
75fn :: 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)`)
80Cuando 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
83fn :: 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
94Cinco 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
107Las 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`)
110Escribir `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
113op :: [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
119fn :: 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
128Ridgun 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:
1291. **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).
1302. **Estilo `2` (`->` desde el valor inicial)**: `xs -> map f -> filter g -> compact limit 14 -> results`.
1313. **Estilo `1` (infijo continuo sin `->` intermedios)**: `xs map f filter g compact limit 14 -> results`.
1324. **Estilo `3` (arranque prefijo clásico + `->`)**: `map f xs -> filter g -> compact limit 14 -> results`.
133
134Y 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
143La 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
153Aquí es donde Ridgun cambia tu forma de pensar.
154
155En un lenguaje imperativo normal, cuando escribes tres líneas seguidas:
1561. Primero se ejecuta la línea 1.
1572. Luego la línea 2 (aunque no tenga nada que ver con la línea 1).
1583. Luego la línea 3.
159
160Eso 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
163fn :: fib Int => Int
164 fib 0 = 0
165 fib 1 = 1
166 fib n = (fib (n - 1)) + (fib (n - 2))
167
168fn :: fact Int => Int
169 fact 0 = 1
170 fact n = n * (fact (n - 1))
171
172fn :: super calculo => Int
173 super calculo = rama_izquierda + rama_derecha
174 || fib 10 -> rama_izquierda
175 || fact 6 -> rama_derecha
176```
177
178Lé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 `||`:
1821. Mira las flechas `->` y arma el árbol de dependencias (DAG).
1832. **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.
1843. 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
186Incluso 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
189fn :: 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
197Fí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
205Usas `||` directamente:
206
207```ridgun
208fn :: par paralelo Int => Int Int
209 par paralelo n =
210 || fib n
211 || fact n
212```
213
214Directo, 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
220El 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
222En 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
232fn :: sumar visita Int Int => Int
233 sumar visita acum n = acum + n
234
235fn :: procesar lote Int Int => Int
236 procesar lote id t = (id * 100) + t
237
238fn :: 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)
255Una pregunta habitual es: *"¿Los bloques `||`, `||map` y los agentes `agent` crean siempre hilos del sistema operativo o corren como tareas ligeras?"*
256
257En 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`)
262Igual 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
271fn :: 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`)
282Cuando 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
288Y 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**.