Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítulo 07: Funcións de Orde Superior e Paralelismo sen Suor (Nin `Mutex`, nin `Deadlocks`) |
| 2 | |
| 3 | En case todas as linguaxes de programación hai un momento dramático na vida dun proxecto: o día en que alguén di nunha xuntanza: |
| 4 | |
| 5 | > *"Isto vai lento. Hai que paralelizalo con fíos."* |
| 6 | |
| 7 | Faise o silencio na sala. Todo o mundo sabe o que vén despois: semanas cazando *race conditions* pantasma que só ocorren os martes con lúa chea, bloqueos mutuos (*deadlocks*) e erros irreproducibles. |
| 8 | |
| 9 | En Ridgun, cando alguén di *"hai que paralelizalo"*, abonda con antepoñer `||` ás túas operacións de lista (**`||map`** e **`||filter`**) ou poñer os teus cálculos nun bloque paralelo `||` con frechas `->` e vas tomar un café. |
| 10 | |
| 11 | Por que? Porque **cando as túas funcións son puras e os teus datos son inmutables por defecto, dous núcleos de CPU traballando á vez xamais poden pisarse a mangueira**. |
| 12 | |
| 13 | Imos velo paso a paso. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## 7.1. Funcións que reciben funcións: `map`, `||map`, `filter`, `||filter` e `fold` |
| 18 | |
| 19 | En Ridgun unha función é un valor máis. Podes pasala por parámetro igual que pasas o número `7`. |
| 20 | |
| 21 | A linguaxe trae de serie os combinadores de coleccións para transformar listas sen escribir nin unha soa recursión manual, tanto en modo secuencial (`map`, `filter`, `fold`) como en modo paralelo multicore co prefixo `||` (**`||map`**, **`||filter`**): |
| 22 | |
| 23 | ### 1. `map` (secuencial) e `||map` (paralelo multicore) |
| 24 | Supón que tes unha función `custoso Int => Int` e unha lista con números: |
| 25 | |
| 26 | ```ridgun |
| 27 | custoso Int => Int |
| 28 | custoso x = (x * x) + (x * 3) |
| 29 | |
| 30 | main => () |
| 31 | main = |
| 32 | datos = [1, 2, 3, 4, 5, 6, 7, 8] |
| 33 | -- Secuencial estrito de esquerda a dereita: |
| 34 | r1 = map custoso datos |
| 35 | -- Paralelo multicore co prefixo `||`: |
| 36 | r2 = ||map custoso datos |
| 37 | echo r1 |
| 38 | echo r2 |
| 39 | ``` |
| 40 | |
| 41 | - Con **`map`**, Ridgun aplica a función elemento a elemento de esquerda a dereita no fío actual (ideal para listas pequenas ou funcións con efectos de entrada/saída ordenados). |
| 42 | - Con **`||map`**, o runtime de Ridgun reparte os elementos de `datos` entre os fíos traballadores do teu procesador e reconstrúe a lista final **exactamente na mesma orde**, garantindo que `||map f xs == map f xs`. |
| 43 | |
| 44 | ### 2. `filter` (secuencial) e `||filter` (paralelo): Quedar só cos que cumpran unha condición |
| 45 | Pásaslle unha función que devolva `Bool` (un *predicado*) e unha lista: |
| 46 | |
| 47 | ```ridgun |
| 48 | e positivo Int => Bool |
| 49 | e positivo n = n > 0 |
| 50 | |
| 51 | main => () |
| 52 | main = |
| 53 | limpos = filter e positivo [-5, 10, 0, -2, 42] |
| 54 | limpos_par = ||filter e positivo [-5, 10, 0, -2, 42] |
| 55 | echo limpos -- Imprime [10, 42] |
| 56 | echo limpos_par -- Imprime [10, 42] |
| 57 | ``` |
| 58 | |
| 59 | ### 3. `fold`: Reducir toda unha lista a un só resultado |
| 60 | Pásaslle unha función combinadora de dous argumentos `(Acumulado Elemento => Acumulado)`, o valor inicial e a lista: |
| 61 | |
| 62 | ```ridgun |
| 63 | acumular produto Int Int => Int |
| 64 | acumular produto acc x = acc * x |
| 65 | |
| 66 | main => () |
| 67 | main = |
| 68 | echo (fold acumular produto 1 [2, 3, 4, 5]) -- 1 * 2 * 3 * 4 * 5 = 120 |
| 69 | ``` |
| 70 | |
| 71 | ### Como escribo eu a miña propia función que reciba outra función? |
| 72 | Moi doado: na sinatura de tipos pos a sinatura da función convidada entre parénteses `(Entrada => Saida)`: |
| 73 | |
| 74 | ```ridgun |
| 75 | executar duas veces (A => A) A => A |
| 76 | executar duas veces f x = f (f x) |
| 77 | ``` |
| 78 | |
| 79 | ### 4. Tubaxes fluídas de esquerda a dereita (`->`) — O estilo 100% Ridgun! |
| 80 | Cando encadeas varias transformacións (`map`, `filter`, `fold` ou as túas propias funcións e operadores `op ::`), aniñar parénteses de dereita a esquerda (`compact l (filter g (map f xs))`) obrígache a ler de dentro cara a fóra. |
| 81 | |
| 82 | En Ridgun, o **estilo idiomático recomendado (`2b`)** lese de esquerda a dereita co operador de tubaxe **`->`**, onde **o que vén da esquerda entra sempre como primeiro argumento** da función ou `op ::` seguinte: |
| 83 | |
| 84 | ```ridgun |
| 85 | -- Estilo idiomático recomendado en Ridgun (2b): |
| 86 | xs map (x => x * 2) -> filter (x => x > 10) -> fold (acc x => acc + x) 0 -> resultado |
| 87 | |
| 88 | -- E se unha función devolve unha tupla, fíltrala cunha máscara e desempaquétase soa: |
| 89 | parsear paquete "v0.9.9" -> (+ _ _ +) -> combinar codigo -> resumo |
| 90 | ``` |
| 91 | |
| 92 | Ademais, calquera expresión con efectos secundarios (escribir un ficheiro, invocar o sistema) pódese poñer soa na súa liña sen `echo` nin variables mudas `_ =`, ou rematar en `-> ()` se queres descartar explicitamente o seu retorno. |
| 93 | |
| 94 | ### 5. Clausuras léxicas e funcións anónimas (`(x => expr)`, `(acc x => expr)`) |
| 95 | Cando queres pasar unha función curta ao voo ou capturar variables locais do teu ámbito actual (como un desprazamento, un prefixo ou unha sesión activa) sen declarar unha función global auxiliar, usas unha **clausura léxica** entre parénteses con **`=>`**: |
| 96 | |
| 97 | ```ridgun |
| 98 | fn :: procesar con sesion => [Int] |
| 99 | procesar con sesion = |
| 100 | base = 10 |
| 101 | bono = 3 |
| 102 | sumados = map (x => x + base) [1, 2, 3] |
| 103 | filtrados = filter (x => x >= bono) [1, 2, 3, 4] |
| 104 | total = fold (acc x => acc + x + bono) 0 [1, 2, 3] |
| 105 | pares = map ((a b) => a + b + base) [(1, 2), (3, 4)] |
| 106 | sumados :: filtrados :: [total] :: pares |
| 107 | ``` |
| 108 | |
| 109 | Cinco regras manteñen as lambdas limpas e sen ambigüidade: |
| 110 | - **Parénteses obrigatorias**: toda lambda vai sempre entre parénteses `(... => ...)`. |
| 111 | - **Lambda sen parámetros (`(=> expr)`)**: igual que `fn :: main => ()` non leva `()` antes de `=>`, unha lambda sen parámetros escríbese `(=> expr)` (e o seu tipo é `(=> Tipo)`), nunca `(() => expr)`. |
| 112 | - **Varios parámetros fronte a tupla (`(a b => ...)` vs. `((a b) => ...)`)**: `(acc x => ...)` recibe dous parámetros separados; `((a b) => ...)` recibe unha soa tupla e desestrutúraa. |
| 113 | - **Corpo de varias liñas**: se pos un salto de liña tras `=>`, podes escribir un bloque sangrado de varios pasos antes do `)` de peche: |
| 114 | ```ridgun |
| 115 | dobres = map (x => |
| 116 | paso = x * 2 |
| 117 | paso + base |
| 118 | ) [1, 2, 3] |
| 119 | ``` |
| 120 | - **Maiúsculas fronte a minúsculas**: `(Conn => Conn)` é un tipo de función (inicial maiúscula) e `(conn => conn)` é un valor lambda (inicial minúscula). |
| 121 | |
| 122 | As variables capturadas (`base`, `bono`) empaquétanse automaticamente con xestión determinista de memoria (`RMM1`/`RMM2`), sen fugas e sen pausas de colector de lixo. |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | ## 7.2. A xoia da coroa: Bloques `||` con `->` como Grafos Paralelos (DAGs) |
| 127 | |
| 128 | Aquí é onde Ridgun muda a túa forma de pensar. |
| 129 | |
| 130 | Nunha linguaxe imperativa normal, cando escribes tres liñas seguidas: |
| 131 | 1. Primeiro execútase a liña 1. |
| 132 | 2. Logo a liña 2 (aínda que non teña nada que ver coa liña 1). |
| 133 | 3. Logo a liña 3. |
| 134 | |
| 135 | Iso é obrigar o ordenador a traballar con anteolleiras. En Ridgun, cando usas barras paralelas **`||`** con frechas **`->`** (sen necesidade de escribir ningunha palabra chave como `where`), non estás dando ordes secuenciais: estás debuxando un **Grafo Acíclico Dirixido (DAG)** que di que cálculo alimenta a que variable: |
| 136 | |
| 137 | ```ridgun |
| 138 | fib Int => Int |
| 139 | fib 0 = 0 |
| 140 | fib 1 = 1 |
| 141 | fib n = (fib (n - 1)) + (fib (n - 2)) |
| 142 | |
| 143 | fact Int => Int |
| 144 | fact 0 = 1 |
| 145 | fact n = n * (fact (n - 1)) |
| 146 | |
| 147 | super calculo => Int |
| 148 | super calculo = rama_esquerda + rama_dereita |
| 149 | || fib 10 -> rama_esquerda |
| 150 | || fact 6 -> rama_dereita |
| 151 | ``` |
| 152 | |
| 153 | Leo en voz alta: |
| 154 | - *"Quero calcular `rama_esquerda + rama_dereita`, mentres en paralelo (`||`) `fib 10` vai a `rama_esquerda` (`->`) e `fact 6` vai a `rama_dereita` (`->`)."* |
| 155 | |
| 156 | ### O que fai `ridgunc` entre bastidores coas túas liñas `||`: |
| 157 | 1. Mira as frechas `->` e arma a árbore de dependencias (DAG). |
| 158 | 2. **Caza ciclos estaticamente**: se por erro escribes `|| a + 1 -> b` e `|| b + 1 -> a`, o compilador diche no acto *"Quietos! Hai unha dependencia circular"* e non che deixa compilar un colgue infinito. |
| 159 | 3. Ve que `fib 10` e `fact 6` non dependen o un do outro, así que teñen luz verde para correr en paralelo antes de sumarse en `rama_esquerda + rama_dereita`. |
| 160 | |
| 161 | Mesmo podes montar unha tubaxe de varias plantas dentro do mesmo bloque `||` —xa sexa ao pé da expresión ou xusto despois do `=`— e o compilador deducirá el só que plantas poden ir á vez e cales teñen que agardar: |
| 162 | |
| 163 | ```ridgun |
| 164 | balance trimestral Int => Int |
| 165 | balance trimestral base = |
| 166 | || base * 100 -> vendas_brutas |
| 167 | || base * 40 -> custos_fixos |
| 168 | || vendas_brutas - custos_fixos -> beneficio_bruto ||. |
| 169 | beneficio_bruto * 80 / 100 |
| 170 | ``` |
| 171 | |
| 172 | Fíxate no detalle **`||.`** ao final da última liña `||`: igual que `|.` pecha unha árbore de gardas aniñada, **`||.`** pecha o bloque paralelo `||` cando debaixo seguen sentenzas secuenciais normais. Aquí `vendas_brutas` e `custos_fixos` arrincan en paralelo; cando ambos están listos calcúlase `beneficio_bruto`, e finalmente devólvese `beneficio_bruto * 80 / 100`. Todo sen un só `async`, `Future`, `Promise` nin `Mutex`. |
| 173 | |
| 174 | --- |
| 175 | |
| 176 | ## 7.3. O atallo `||` sen nomes: Tubaxes paralelas que devolven unha tupla |
| 177 | |
| 178 | E se só queres lanzar dous ou tres cálculos en paralelo e recollelos xuntos nunha tupla sen sequera inventar nomes de variables con `->`? |
| 179 | |
| 180 | Usas `||` directamente: |
| 181 | |
| 182 | ```ridgun |
| 183 | par paralelo Int => (Int, Int) |
| 184 | par paralelo n = |
| 185 | || fib n |
| 186 | || fact n |
| 187 | ``` |
| 188 | |
| 189 | Directo, visual e devolve a tupla `(fib n, fact n)`. |
| 190 | |
| 191 | --- |
| 192 | |
| 193 | ## 7.4. Cando queres o volante manual: o Modelo Nativo de Axentes (`agent`, `send ... to`, `receive from`) e Canles CSP |
| 194 | |
| 195 | O paralelismo automático de `||` e `||map` cobre o 90% dos casos. Pero cando programas un servidor persistente, sesións de usuario con estado illado, filas de traballos ou unha tubaxe produtor-consumidor, queres **axentes concorrentes con memoria illada e paso de mensaxes tipado**. |
| 196 | |
| 197 | En Ridgun non fai falta inventar unha sintaxe estraña como `actor ::`: **calquera función normal (`fn ::` ou `op ::`) convértese nun axente concorrente nunha liña con `agent`**: |
| 198 | |
| 199 | ### 1. Axentes sen estado e con estado illado (`agent`, `send ... to`, `receive from`, `stop`) |
| 200 | - `ag = agent (miña funcion)`: converte unha función `M => R` nun axente sen estado de tipo `Agent M R`. |
| 201 | - `ag = agent (funcion con estado) estado_inicial`: converte unha función `S M => S` (ou `S M => S R`) nun axente con estado illado que lembra o seu estado mensaxe tras mensaxe sen un só `Mutex`. |
| 202 | - `send msg to ag` (ou con varios argumentos: `send a b to ag`): envía unha mensaxe tipada á caixa de correo do axente. |
| 203 | - `res = receive from ag` (ou `try receive from ag` sen bloquear): recolle a seguinte resposta da caixa de saída do axente. |
| 204 | - `stop ag`: detén o axente e libera o seu estado automaticamente mediante `RMM1`/`RMM2`. |
| 205 | |
| 206 | ```ridgun |
| 207 | fn :: sumar visita Int Int => Int |
| 208 | sumar visita acum n = acum + n |
| 209 | |
| 210 | fn :: procesar lote Int Int => Int |
| 211 | procesar lote id t = (id * 100) + t |
| 212 | |
| 213 | fn :: main => () |
| 214 | main = |
| 215 | contador = agent (sumar visita) 10 |
| 216 | send 5 to contador |
| 217 | send 7 to contador |
| 218 | v1 = receive from contador |
| 219 | v2 = receive from contador |
| 220 | stop contador |
| 221 | |
| 222 | traballador = agent (procesar lote) |
| 223 | send 3 25 to traballador |
| 224 | lote = receive from traballador |
| 225 | stop traballador |
| 226 | echo "Visitas:" v1 "->" v2 "| Lote:" lote |
| 227 | ``` |
| 228 | |
| 229 | ### 2. Elección intelixente en tempo de compilación (*Green Threads* vs. Fíos Reais do Kernel) |
| 230 | Unha pregunta habitual é: *"Os bloques `||`, `||map` e os axentes `agent` crean sempre fíos do sistema operativo ou corren como tarefas lixeiras?"* |
| 231 | |
| 232 | En Ridgun, **o compilador toma esa decisión por ti analizando estaticamente cada función**: |
| 233 | - **Funcións lixeiras (`TierTrivial` / `StratInlineSequential`)**: Se a función que pasas a `||`, `||map`, `||filter` ou `agent` ten pouca profundidade (aritmética, formatado de cadeas, transición de estado rápida dunha sesión HTTP), quentar fíos do sistema operativo custaría máis có propio cálculo. O compilador execútaa como un paso *green-thread* ultralixeiro sen sobrecarga de `SYS_clone`. |
| 234 | - **Cálculos pesados ou recursivos profundos (`TierDynamicRecursive` / `TierHeavyCPU`)**: Se a función ten recursión profunda (como `fib` ou `fact`) ou álxebra de tensores (`einsum`, `hadamard`, `@`), o compilador xera automaticamente o despacho multinúcleo sobre **fíos reais do kernel (`SYS_clone`)**, procesando as ramas `||` ou a cola de mensaxes do `agent` en paralelo real sobre todos os teus núcleos de CPU. |
| 235 | |
| 236 | ### 3. Pasar mensaxes por canles acoutadas (`new bounded channel`) |
| 237 | Igual ca en Go ou Erlang, ademais de axentes tamén podes crear tubos explícitos para comunicar etapas: |
| 238 | |
| 239 | - `new bounded channel capacidade`: crea unha canle con espazo para `capacidade` mensaxes. |
| 240 | - `send through valor canle` (ou `chan send valor canle`): mete unha mensaxe no tubo. |
| 241 | - `receive from canle` (ou `chan recv canle`): saca a seguinte mensaxe do tubo. |
| 242 | - `try receive from canle`: mira se hai mensaxe sen quedar agardando; devolve `Some valor` ou `None`. |
| 243 | - `receive from timeout canle ticks`: agarda un límite de tempo e devolve `Ok valor` ou `Err Timeout`. |
| 244 | |
| 245 | ```ridgun |
| 246 | fn :: main => () |
| 247 | main = |
| 248 | tubo = new bounded channel 4 |
| 249 | _ = send through "Tarefa A completada" tubo |
| 250 | _ = send through "Tarefa B completada" tubo |
| 251 | primeiro = receive from tubo |
| 252 | segundo = receive from tubo |
| 253 | echo primeiro "|" segundo |
| 254 | ``` |
| 255 | |
| 256 | ### 4. Axentes sumidoiro (*fire-and-forget* `agent sink`), caixas de correo globais (`agent ::`) e probas deterministas (`with agent`) |
| 257 | Cando precisas auditoría asíncrona, métricas ou trazabilidade transversal sen ensuciar todas as sinaturas nin acumular respostas sen ler nunha cola de saída, Ridgun ofrece **axentes sumidoiro (`agent sink`)** e **caixas de correo globais de nivel superior (`agent ::`)** (`agent dropped`, `agent flush`, `agent state` e `with agent audit_log = espia do ...`), ademais de descritores `Channel` e `Agent` compartibles entre múltiples traballadores. |
| 258 | |
| 259 | E agora, abrocha o cinto para o **Capítulo 08**, o gran final: **tensores con dimensións verificadas en compilación, variables de ámbito (`context`/`shared`), rexións de memoria deterministas, acceso directo a hardware MMIO e programación de sistemas UNIX/POSIX**. |