Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítol 07: Funcions d'Ordre Superior i Paral·lelisme sense Suor (Ni `Mutex`, ni `Deadlocks`) |
| 2 | |
| 3 | En gairebé tots els llenguatges de programació hi ha un moment dramàtic en la vida d'un projecte: el dia en què algú diu en una reunió: |
| 4 | |
| 5 | > *"Això va lent. Cal paral·lelitzar-ho amb fils."* |
| 6 | |
| 7 | Es fa el silenci a la sala. Tothom sap què ve després: setmanes caçant *race conditions* fantasma que només passen els dimarts amb lluna plena, bloquejos mutus (*deadlocks*) i errors irreproduïbles. |
| 8 | |
| 9 | En Ridgun, quan algú diu *"cal paral·lelitzar-ho"*, n'hi ha prou d'anteposar `||` a les teves operacions de llista (**`||map`** i **`||filter`**) o posar els teus càlculs en un bloc paral·lel `||` amb fletxes `->` i te'n vas a fer un cafè. |
| 10 | |
| 11 | Per què? Perquè **quan les teves funcions són pures i les teves dades són immutables per defecte, dos nuclis de CPU treballant alhora mai no es poden trepitjar la mànega**. |
| 12 | |
| 13 | Vegem-ho pas a pas. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## 7.1. Funcions que reben funcions: `map`, `||map`, `filter`, `||filter` i `fold` |
| 18 | |
| 19 | En Ridgun una funció és un valor més. Pots passar-la per paràmetre igual que passes el número `7`. |
| 20 | |
| 21 | El llenguatge porta de sèrie els combinadors de col·leccions per transformar llistes sense escriure ni una sola recursió manual, tant en mode seqüencial (`map`, `filter`, `fold`) com en mode paral·lel multicore amb el prefix `||` (**`||map`**, **`||filter`**): |
| 22 | |
| 23 | ### 1. `map` (seqüencial) i `||map` (paral·lel multicore) |
| 24 | Suposa que tens una funció `costos Int => Int` i una llista amb números: |
| 25 | |
| 26 | ```ridgun |
| 27 | costos Int => Int |
| 28 | costos x = (x * x) + (x * 3) |
| 29 | |
| 30 | main => () |
| 31 | main = |
| 32 | dades = [1, 2, 3, 4, 5, 6, 7, 8] |
| 33 | -- Seqüencial estricte d'esquerra a dreta: |
| 34 | r1 = map costos dades |
| 35 | -- Paral·lel multicore amb el prefix `||`: |
| 36 | r2 = ||map costos dades |
| 37 | echo r1 |
| 38 | echo r2 |
| 39 | ``` |
| 40 | |
| 41 | - Amb **`map`**, Ridgun aplica la funció element a element d'esquerra a dreta en el fil actual (ideal per a llistes petites o funcions amb efectes d'entrada/sortida ordenats). |
| 42 | - Amb **`||map`**, el runtime de Ridgun reparteix els elements de `dades` entre els fils treballadors del teu processador i reconstrueix la llista final **exactament en el mateix ordre**, garantint que `||map f xs == map f xs`. |
| 43 | |
| 44 | ### 2. `filter` (seqüencial) i `||filter` (paral·lel): Quedar-te només amb els que compleixin una condició |
| 45 | Li passes una funció que retorni `Bool` (un *predicat*) i una llista: |
| 46 | |
| 47 | ```ridgun |
| 48 | es positiu Int => Bool |
| 49 | es positiu n = n > 0 |
| 50 | |
| 51 | main => () |
| 52 | main = |
| 53 | nets = filter es positiu [-5, 10, 0, -2, 42] |
| 54 | nets_par = ||filter es positiu [-5, 10, 0, -2, 42] |
| 55 | echo nets -- Imprimeix [10, 42] |
| 56 | echo nets_par -- Imprimeix [10, 42] |
| 57 | ``` |
| 58 | |
| 59 | ### 3. `fold`: Reduir tota una llista a un sol resultat |
| 60 | Li passes una funció combinadora de dos arguments `(Acumulat Element => Acumulat)`, el valor inicial i la llista: |
| 61 | |
| 62 | ```ridgun |
| 63 | acumular producte Int Int => Int |
| 64 | acumular producte acc x = acc * x |
| 65 | |
| 66 | main => () |
| 67 | main = |
| 68 | echo (fold acumular producte 1 [2, 3, 4, 5]) -- 1 * 2 * 3 * 4 * 5 = 120 |
| 69 | ``` |
| 70 | |
| 71 | ### Com escric jo la meva pròpia funció que rebi una altra funció? |
| 72 | Molt fàcil: a la signatura de tipus poses la signatura de la funció convidada entre parèntesis `(Entrada => Sortida)`: |
| 73 | |
| 74 | ```ridgun |
| 75 | executar dues vegades (A => A) A => A |
| 76 | executar dues vegades f x = f (f x) |
| 77 | ``` |
| 78 | |
| 79 | ### 4. Canonades fluides d'esquerra a dreta (`->`) — L'estil 100% Ridguner! |
| 80 | Quan encadenes diverses transformacions (`map`, `filter`, `fold` o les teves pròpies funcions i operadors `op ::`), niar parèntesis de dreta a esquerra (`compact l (filter g (map f xs))`) t'obliga a llegir de dins cap a fora. |
| 81 | |
| 82 | En Ridgun, l'**estil idiomàtic recomanat (`2b`)** es llegeix d'esquerra a dreta amb l'operador de canonada **`->`**, on **el que ve de l'esquerra entra sempre com a primer argument** de la funció o `op ::` següent: |
| 83 | |
| 84 | ```ridgun |
| 85 | -- Estil idiomàtic recomanat en Ridgun (2b): |
| 86 | xs map (x => x * 2) -> filter (x => x > 10) -> fold (acc x => acc + x) 0 -> resultat |
| 87 | |
| 88 | -- I si una funció retorna una tupla, la filtres amb una màscara i es desempaqueta sola: |
| 89 | parsejar paquet "v0.9.9" -> (+ _ _ +) -> combinar codi -> resum |
| 90 | ``` |
| 91 | |
| 92 | A més, qualsevol expressió amb efectes secundaris (escriure un fitxer, invocar el sistema) es pot posar sola a la seva línia sense `echo` ni variables mudes `_ =`, o acabar en `-> ()` si vols descartar explícitament el seu retorn. |
| 93 | |
| 94 | ### 5. Clausures lèxiques i funcions anònimes (`(x => expr)`, `(acc x => expr)`) |
| 95 | Quan vols passar una funció curta al vol o capturar variables locals del teu àmbit actual (com un desplaçament, un prefix o una sessió activa) sense declarar una funció global auxiliar, fas servir una **clausura lèxica** entre parèntesis amb **`=>`**: |
| 96 | |
| 97 | ```ridgun |
| 98 | fn :: processar amb sessio => [Int] |
| 99 | processar amb sessio = |
| 100 | base = 10 |
| 101 | bo = 3 |
| 102 | sumats = map (x => x + base) [1, 2, 3] |
| 103 | filtrats = filter (x => x >= bo) [1, 2, 3, 4] |
| 104 | total = fold (acc x => acc + x + bo) 0 [1, 2, 3] |
| 105 | parells = map ((a b) => a + b + base) [(1, 2), (3, 4)] |
| 106 | sumats :: filtrats :: [total] :: parells |
| 107 | ``` |
| 108 | |
| 109 | Cinc regles mantenen les lambdes netes i sense ambigüitat: |
| 110 | - **Parèntesis obligatoris**: tota lambda va sempre entre parèntesis `(... => ...)`. |
| 111 | - **Lambda sense paràmetres (`(=> expr)`)**: igual que `fn :: main => ()` no porta `()` abans de `=>`, una lambda sense paràmetres s'escriu `(=> expr)` (i el seu tipus és `(=> Tipus)`), mai `(() => expr)`. |
| 112 | - **Diversos paràmetres enfront de tupla (`(a b => ...)` vs. `((a b) => ...)`)**: `(acc x => ...)` rep dos paràmetres separats; `((a b) => ...)` rep una sola tupla i la desestructura. |
| 113 | - **Cos de diverses línies**: si poses un salt de línia després de `=>`, pots escriure un bloc sagnat de diversos passos abans del `)` de tancament: |
| 114 | ```ridgun |
| 115 | dobles = map (x => |
| 116 | pas = x * 2 |
| 117 | pas + base |
| 118 | ) [1, 2, 3] |
| 119 | ``` |
| 120 | - **Majúscules enfront de minúscules**: `(Conn => Conn)` és un tipus de funció (inicial majúscula) i `(conn => conn)` és un valor lambda (inicial minúscula). |
| 121 | |
| 122 | Les variables capturades (`base`, `bo`) s'empaqueten automàticament amb gestió determinista de memòria (`RMM1`/`RMM2`), sense fuites i sense pauses de recol·lector de brossa. |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | ## 7.2. La joia de la corona: Blocs `||` amb `->` com a Grafs Paral·lels (DAGs) |
| 127 | |
| 128 | Aquí és on Ridgun canvia la teva manera de pensar. |
| 129 | |
| 130 | En un llenguatge imperatiu normal, quan escrius tres línies seguides: |
| 131 | 1. Primer s'executa la línia 1. |
| 132 | 2. Després la línia 2 (encara que no tingui res a veure amb la línia 1). |
| 133 | 3. Després la línia 3. |
| 134 | |
| 135 | Això és obligar l'ordinador a treballar amb clucales. En Ridgun, quan fas servir barres paral·leles **`||`** amb fletxes **`->`** (sense necessitat d'escriure cap paraula clau com `where`), no estàs donant ordres seqüencials: estàs dibuixant un **Graf Acíclic Dirigit (DAG)** que diu quin càlcul alimenta quina 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 calcul => Int |
| 148 | super calcul = branca_esquerra + branca_dreta |
| 149 | || fib 10 -> branca_esquerra |
| 150 | || fact 6 -> branca_dreta |
| 151 | ``` |
| 152 | |
| 153 | Llegeix-ho en veu alta: |
| 154 | - *"Vull calcular `branca_esquerra + branca_dreta`, mentre en paral·lel (`||`) `fib 10` va a `branca_esquerra` (`->`) i `fact 6` va a `branca_dreta` (`->`)."* |
| 155 | |
| 156 | ### Què fa `ridgunc` entre bastidors amb les teves línies `||`: |
| 157 | 1. Mira les fletxes `->` i munta l'arbre de dependències (DAG). |
| 158 | 2. **Caça cicles estàticament**: si per error escrius `|| a + 1 -> b` i `|| b + 1 -> a`, el compilador et diu a l'instant *"Quiets! Hi ha una dependència circular"* i no et deixa compilar un bloqueig infinit. |
| 159 | 3. Veu que `fib 10` i `fact 6` no depenen l'un de l'altre, així que tenen llum verda per córrer en paral·lel abans de sumar-se a `branca_esquerra + branca_dreta`. |
| 160 | |
| 161 | Fins i tot pots muntar una canonada de diverses plantes dins del mateix bloc `||` —ja sigui al peu de l'expressió o just després de l'`=`— i el compilador deduirà tot sol quines plantes poden anar alhora i quines s'han d'esperar: |
| 162 | |
| 163 | ```ridgun |
| 164 | balanc trimestral Int => Int |
| 165 | balanc trimestral base = |
| 166 | || base * 100 -> vendes_brutes |
| 167 | || base * 40 -> costos_fixos |
| 168 | || vendes_brutes - costos_fixos -> benefici_brut ||. |
| 169 | benefici_brut * 80 / 100 |
| 170 | ``` |
| 171 | |
| 172 | Fixa't en el detall **`||.`** al final de l'última línia `||`: igual que `|.` tanca un arbre de guardes imbricat, **`||.`** tanca el bloc paral·lel `||` quan a sota continuen sentències seqüencials normals. Aquí `vendes_brutes` i `costos_fixos` arrenquen en paral·lel; quan tots dos estan a punt es calcula `benefici_brut`, i finalment es retorna `benefici_brut * 80 / 100`. Tot sense un sol `async`, `Future`, `Promise` ni `Mutex`. |
| 173 | |
| 174 | --- |
| 175 | |
| 176 | ## 7.3. La drecera `||` sense noms: Canonades paral·leles que retornen una tupla |
| 177 | |
| 178 | I si només vols llançar dos o tres càlculs en paral·lel i recollir-los junts en una tupla sense ni tan sols inventar noms de variables amb `->`? |
| 179 | |
| 180 | Fas servir `||` directament: |
| 181 | |
| 182 | ```ridgun |
| 183 | parell parallel Int => (Int, Int) |
| 184 | parell parallel n = |
| 185 | || fib n |
| 186 | || fact n |
| 187 | ``` |
| 188 | |
| 189 | Directe, visual i retorna la tupla `(fib n, fact n)`. |
| 190 | |
| 191 | --- |
| 192 | |
| 193 | ## 7.4. Quan vols el volant manual: el Model Natiu d'Agents (`agent`, `send ... to`, `receive from`) i Canals CSP |
| 194 | |
| 195 | El paral·lelisme automàtic de `||` i `||map` cobreix el 90% dels casos. Però quan programes un servidor persistent, sessions d'usuari amb estat aïllat, cues de treballs o una canonada productor-consumidor, vols **agents concurrents amb memòria aïllada i pas de missatges tipat**. |
| 196 | |
| 197 | En Ridgun no cal inventar cap sintaxi estranya com `actor ::`: **qualsevol funció normal (`fn ::` o `op ::`) es converteix en un agent concurrent en una sola línia amb `agent`**: |
| 198 | |
| 199 | ### 1. Agents sense estat i amb estat aïllat (`agent`, `send ... to`, `receive from`, `stop`) |
| 200 | - `ag = agent (la meva funcio)`: converteix una funció `M => R` en un agent sense estat de tipus `Agent M R`. |
| 201 | - `ag = agent (funcio amb estat) estat_inicial`: converteix una funció `S M => S` (o `S M => S R`) en un agent amb estat aïllat que recorda el seu estat missatge rere missatge sense cap `Mutex`. |
| 202 | - `send msg to ag` (o amb diversos arguments: `send a b to ag`): envia un missatge tipat a la bústia de l'agent. |
| 203 | - `res = receive from ag` (o `try receive from ag` sense bloquejar): recull la següent resposta de la bústia de sortida de l'agent. |
| 204 | - `stop ag`: atura l'agent i allibera el seu estat automàticament mitjançant `RMM1`/`RMM2`. |
| 205 | |
| 206 | ```ridgun |
| 207 | fn :: sumar visita Int Int => Int |
| 208 | sumar visita acum n = acum + n |
| 209 | |
| 210 | fn :: processar lot Int Int => Int |
| 211 | processar lot id t = (id * 100) + t |
| 212 | |
| 213 | fn :: main => () |
| 214 | main = |
| 215 | comptador = agent (sumar visita) 10 |
| 216 | send 5 to comptador |
| 217 | send 7 to comptador |
| 218 | v1 = receive from comptador |
| 219 | v2 = receive from comptador |
| 220 | stop comptador |
| 221 | |
| 222 | treballador = agent (processar lot) |
| 223 | send 3 25 to treballador |
| 224 | lot = receive from treballador |
| 225 | stop treballador |
| 226 | echo "Visites:" v1 "->" v2 "| Lot:" lot |
| 227 | ``` |
| 228 | |
| 229 | ### 2. Elecció intel·ligent en temps de compilació (*Green Threads* vs. Fils Reals del Nucli) |
| 230 | Una pregunta habitual és: *"Els blocs `||`, `||map` i els agents `agent` creen sempre fils del sistema operatiu o corren com a tasques lleugeres?"* |
| 231 | |
| 232 | En Ridgun, **el compilador pren aquesta decisió per tu analitzant estàticament cada funció**: |
| 233 | - **Funcions lleugeres (`TierTrivial` / `StratInlineSequential`)**: Si la funció que passes a `||`, `||map`, `||filter` o `agent` té poca profunditat (aritmètica, formatat de cadenes, transició d'estat ràpida d'una sessió HTTP), escalfar fils del sistema operatiu costaria més que el propi càlcul. El compilador l'executa com un pas *green-thread* ultralleuger sense sobrecàrrega de `SYS_clone`. |
| 234 | - **Càlculs pesats o recursius profunds (`TierDynamicRecursive` / `TierHeavyCPU`)**: Si la funció té recursió profunda (com `fib` o `fact`) o àlgebra de tensors (`einsum`, `hadamard`, `@`), el compilador genera automàticament el despatx multinucli sobre **fils reals del nucli (`SYS_clone`)**, processant les branques `||` o la cua de missatges de l'`agent` en paral·lel real sobre tots els teus nuclis de CPU. |
| 235 | |
| 236 | ### 3. Passar missatges per canals afitats (`new bounded channel`) |
| 237 | Igual que en Go o Erlang, a més d'agents també pots crear tubs explícits per comunicar etapes: |
| 238 | |
| 239 | - `new bounded channel capacitat`: crea un canal amb espai per a `capacitat` missatges. |
| 240 | - `send through valor canal` (o `chan send valor canal`): fica un missatge al tub. |
| 241 | - `receive from canal` (o `chan recv canal`): treu el següent missatge del tub. |
| 242 | - `try receive from canal`: mira si hi ha missatge sense quedar-se esperant; retorna `Some valor` o `None`. |
| 243 | - `receive from timeout canal ticks`: espera un límit de temps i retorna `Ok valor` o `Err Timeout`. |
| 244 | |
| 245 | ```ridgun |
| 246 | fn :: main => () |
| 247 | main = |
| 248 | tub = new bounded channel 4 |
| 249 | _ = send through "Tasca A completada" tub |
| 250 | _ = send through "Tasca B completada" tub |
| 251 | primer = receive from tub |
| 252 | segon = receive from tub |
| 253 | echo primer "|" segon |
| 254 | ``` |
| 255 | |
| 256 | ### 4. Agents embornal (*fire-and-forget* `agent sink`), bústies globals (`agent ::`) i proves deterministes (`with agent`) |
| 257 | Quan necessites auditoria asíncrona, mètriques o traçabilitat transversal sense embrutar totes les signatures ni acumular respostes sense llegir en una cua de sortida, Ridgun ofereix **agents embornal (`agent sink`)** i **bústies globals de nivell superior (`agent ::`)** (`agent dropped`, `agent flush`, `agent state`, i `with agent audit_log = espia do ...`), a més de descriptors `Channel` i `Agent` compartibles entre múltiples treballadors. |
| 258 | |
| 259 | I ara, corda't el cinturó per al **Capítol 08**, el gran final: **tensors amb dimensions verificades en compilació, variables d'àmbit (`context`/`shared`), regions de memòria deterministes, accés directe a maquinari MMIO i programació de sistemes UNIX/POSIX**. |