Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Kapitel 07: Högre ordningens funktioner och svettfri parallellism (Utan `Mutex`, utan `Deadlocks`) |
| 2 | |
| 3 | I nästan alla programmeringsspråk finns det ett dramatiskt ögonblick i ett projekts liv: dagen då någon på ett möte säger: |
| 4 | |
| 5 | > *"Det här går långsamt. Vi måste parallellisera det med trådar."* |
| 6 | |
| 7 | Det blir knäpptyst i rummet. Alla vet vad som väntar härnäst: veckor av jakt på spöklika *race conditions* som bara inträffar på tisdagar vid fullmåne, ömsesidiga låsningar (*deadlocks*) och fel som inte går att återskapa. |
| 8 | |
| 9 | När någon i Ridgun säger *"vi måste parallellisera det"*, räcker det med att sätta `||` framför dina listoperationer (**`||map`** och **`||filter`**) eller lägga dina beräkningar i ett parallellt `||`-block med pilar `->` och gå och ta en kaffe. |
| 10 | |
| 11 | Varför? Därför att **när dina funktioner är rena och dina data är oföränderliga (*immutable*) som standard, kan två processorkärnor som arbetar samtidigt aldrig trampa på varandras slangar**. |
| 12 | |
| 13 | Låt oss gå igenom det steg för steg. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## 7.1. Funktioner som tar emot funktioner: `map`, `||map`, `filter`, `||filter` och `fold` |
| 18 | |
| 19 | I Ridgun är en funktion ett värde som alla andra. Du kan skicka in den som parameter precis som du skickar in talet `7`. |
| 20 | |
| 21 | Språket har inbyggda samlingskombinatorer för att omvandla listor utan att skriva en enda manuell rekursion, både i sekventiellt läge (`map`, `filter`, `fold`) och i flerkärnigt parallellt läge med prefixet `||` (**`||map`**, **`||filter`**): |
| 22 | |
| 23 | ### 1. `map` (sekventiell) och `||map` (parallell över flera kärnor) |
| 24 | Anta att du har en funktion `tung Int => Int` och en lista med tal: |
| 25 | |
| 26 | ```ridgun |
| 27 | tung Int => Int |
| 28 | tung x = (x * x) + (x * 3) |
| 29 | |
| 30 | main => () |
| 31 | main = |
| 32 | data = [1, 2, 3, 4, 5, 6, 7, 8] |
| 33 | -- Strikt sekventiell från vänster till höger: |
| 34 | r1 = map tung data |
| 35 | -- Parallell över flera kärnor med prefixet `||`: |
| 36 | r2 = ||map tung data |
| 37 | echo r1 |
| 38 | echo r2 |
| 39 | ``` |
| 40 | |
| 41 | - Med **`map`** tillämpar Ridgun funktionen element för element från vänster till höger i den aktuella tråden (perfekt för små listor eller funktioner med ordnade in-/utmatningseffekter). |
| 42 | - Med **`||map`** fördelar Ridguns körtidsmiljö elementen i `data` mellan arbetstrådarna i din processor och bygger upp slutlistan **i exakt samma ordning**, vilket garanterar att `||map f xs == map f xs`. |
| 43 | |
| 44 | ### 2. `filter` (sekventiell) och `||filter` (parallell): Behåll bara dem som uppfyller ett villkor |
| 45 | Du skickar in en funktion som returnerar `Bool` (ett *predikat*) och en lista: |
| 46 | |
| 47 | ```ridgun |
| 48 | ar positiv Int => Bool |
| 49 | ar positiv n = n > 0 |
| 50 | |
| 51 | main => () |
| 52 | main = |
| 53 | rena = filter ar positiv [-5, 10, 0, -2, 42] |
| 54 | rena_par = ||filter ar positiv [-5, 10, 0, -2, 42] |
| 55 | echo rena -- Skriver ut [10, 42] |
| 56 | echo rena_par -- Skriver ut [10, 42] |
| 57 | ``` |
| 58 | |
| 59 | ### 3. `fold`: Reducera en hel lista till ett enda resultat |
| 60 | Du skickar in en kombinerande funktion med två argument `(Ackumulerat Element => Ackumulerat)`, startvärdet och listan: |
| 61 | |
| 62 | ```ridgun |
| 63 | ackumulera produkt Int Int => Int |
| 64 | ackumulera produkt acc x = acc * x |
| 65 | |
| 66 | main => () |
| 67 | main = |
| 68 | echo (fold ackumulera produkt 1 [2, 3, 4, 5]) -- 1 * 2 * 3 * 4 * 5 = 120 |
| 69 | ``` |
| 70 | |
| 71 | ### Hur skriver jag en egen funktion som tar emot en annan funktion? |
| 72 | Hur enkelt som helst: i typsignaturen sätter du den inbjudna funktionens signatur inom parentes `(Indata => Utdata)`: |
| 73 | |
| 74 | ```ridgun |
| 75 | kor tva ganger (A => A) A => A |
| 76 | kor tva ganger f x = f (f x) |
| 77 | ``` |
| 78 | |
| 79 | ### 4. Idiomatiska pipelines från vänster till höger (`->`) — 100 % Ridgun-stil! |
| 80 | När du kedjar ihop flera transformationer (`map`, `filter`, `fold` eller dina egna funktioner och `op ::`-operatorer) tvingar nästlade parenteser från höger till vänster (`compact l (filter g (map f xs))`) dig att läsa inifrån och ut. |
| 81 | |
| 82 | I Ridgun läses den **rekommenderade idiomatiska stilen (`2b`)** från vänster till höger med pipeline-operatorn **`->`**, där **det som kommer från vänster alltid går in som första argument** till nästa funktion eller `op ::`: |
| 83 | |
| 84 | ```ridgun |
| 85 | -- Rekommenderad idiomatisk stil i Ridgun (2b): |
| 86 | xs map (x => x * 2) -> filter (x => x > 10) -> fold (acc x => acc + x) 0 -> resultat |
| 87 | |
| 88 | -- Och om en funktion returnerar en tupel filtrerar du den med en mask och den packas upp automatiskt: |
| 89 | tolka paket "v0.9.9" -> (+ _ _ +) -> kombinera kod -> sammanfattning |
| 90 | ``` |
| 91 | |
| 92 | Dessutom kan varje uttryck med sidoeffekter (skriva en fil, anropa systemet) stå på en egen rad utan `echo` eller dummyvariabler `_ =`, eller avslutas med `-> ()` om du uttryckligen vill kasta returvärdet. |
| 93 | |
| 94 | ### 5. Lexikala closures och anonyma funktioner (`(x => expr)`, `(acc x => expr)`) |
| 95 | När du vill skicka in en kort funktion i farten eller fånga lokala variabler från det omgivande scopet (som en offset, ett prefix eller en aktiv session) utan att deklarera en separat toppnivåfunktion använder du en **lexikal closure** inom parentes med **`=>`**: |
| 96 | |
| 97 | ```ridgun |
| 98 | fn :: bearbeta med session => [Int] |
| 99 | bearbeta med session = |
| 100 | offset = 10 |
| 101 | bonus = 3 |
| 102 | adderade = map (x => x + offset) [1, 2, 3] |
| 103 | filtrerade = filter (x => x >= bonus) [1, 2, 3, 4] |
| 104 | total = fold (acc x => acc + x + bonus) 0 [1, 2, 3] |
| 105 | par = map ((a b) => a + b + offset) [(1, 2), (3, 4)] |
| 106 | adderade :: filtrerade :: [total] :: par |
| 107 | ``` |
| 108 | |
| 109 | Fem regler håller lambdauttryck rena och entydiga: |
| 110 | - **Obligatoriska parenteser**: varje lambda skrivs alltid inom parentes `(... => ...)`. |
| 111 | - **Lambda utan parametrar (`(=> expr)`)**: precis som `fn :: main => ()` inte har `()` före `=>`, skrivs en parameterlös lambda `(=> expr)` (och dess typ är `(=> Typ)`), aldrig `(() => expr)`. |
| 112 | - **Flera parametrar kontra tupel (`(a b => ...)` vs. `((a b) => ...)`)**: `(acc x => ...)` tar två separata parametrar, medan `((a b) => ...)` tar en enda tupel och destrukturerar den. |
| 113 | - **Flerradig kropp**: om du lägger en radbrytning efter `=>` kan du skriva ett indenterat flerstegsblock före det avslutande `)`: |
| 114 | ```ridgun |
| 115 | dubblade = map (x => |
| 116 | steg = x * 2 |
| 117 | steg + offset |
| 118 | ) [1, 2, 3] |
| 119 | ``` |
| 120 | - **Versaler kontra gemener**: `(Conn => Conn)` är en funktionstyp (stor begynnelsebokstav) och `(conn => conn)` är ett lambdavärde (liten begynnelsebokstav). |
| 121 | |
| 122 | Fångade variabler (`offset`, `bonus`) hanteras deterministiskt med `RMM1`/`RMM2` — noll minnesläckor och noll skräpsamlingspauser. |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | ## 7.2. Kronjuvelen: `||`-block med `->` som parallella grafer (DAG) |
| 127 | |
| 128 | Det är här Ridgun förändrar ditt sätt att tänka. |
| 129 | |
| 130 | När du i ett vanligt imperativt språk skriver tre rader efter varandra: |
| 131 | 1. Först körs rad 1. |
| 132 | 2. Sedan rad 2 (även om den inte har det minsta med rad 1 att göra). |
| 133 | 3. Sedan rad 3. |
| 134 | |
| 135 | Det är att tvinga datorn att arbeta med skygglappar. När du i Ridgun använder parallella streck **`||`** med pilar **`->`** (utan att behöva skriva något nyckelord som `where`), ger du inte sekventiella order: du ritar en **riktad acyklisk graf (DAG)** som anger vilken beräkning som matar vilken variabel: |
| 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 berakning => Int |
| 148 | super berakning = vanster_gren + hoger_gren |
| 149 | || fib 10 -> vanster_gren |
| 150 | || fact 6 -> hoger_gren |
| 151 | ``` |
| 152 | |
| 153 | Läs det högt: |
| 154 | - *"Jag vill beräkna `vanster_gren + hoger_gren`, medan `fib 10` parallellt (`||`) går in i `vanster_gren` (`->`) och `fact 6` går in i `hoger_gren` (`->`)."* |
| 155 | |
| 156 | ### Vad `ridgunc` gör bakom kulisserna med dina `||`-rader: |
| 157 | 1. Den tittar på pilarna `->` och bygger upp beroendeträdet (DAG). |
| 158 | 2. **Den fångar cykler statiskt**: om du av misstag skriver `|| a + 1 -> b` och `|| b + 1 -> a`, säger kompilatorn genast ifrån: *"Stopp där! Här finns ett cirkulärt beroende"* och vägrar kompilera en oändlig låsning. |
| 159 | 3. Den ser att `fib 10` och `fact 6` inte är beroende av varandra, så de får grönt ljus att köras parallellt innan de summeras i `vanster_gren + hoger_gren`. |
| 160 | |
| 161 | Du kan till och med bygga en pipeline i flera våningar inuti samma `||`-block — antingen under uttrycket eller direkt efter `=` — och kompilatorn räknar själv ut vilka våningar som kan köras samtidigt och vilka som måste vänta: |
| 162 | |
| 163 | ```ridgun |
| 164 | kvartalsbalans Int => Int |
| 165 | kvartalsbalans bas = |
| 166 | || bas * 100 -> bruttoforsaljning |
| 167 | || bas * 40 -> fasta_kostnader |
| 168 | || bruttoforsaljning - fasta_kostnader -> bruttovinst ||. |
| 169 | bruttovinst * 80 / 100 |
| 170 | ``` |
| 171 | |
| 172 | Lägg märke till detaljen **`||.`** i slutet av den sista `||`-raden: precis som `|.` stänger ett nästlat vaktträd, stänger **`||.`** det parallella `||`-blocket när vanliga sekventiella satser följer nedanför. Här startar `bruttoforsaljning` och `fasta_kostnader` parallellt; när båda är klara beräknas `bruttovinst`, och slutligen returneras `bruttovinst * 80 / 100`. Allt utan ett enda `async`, `Future`, `Promise` eller `Mutex`. |
| 173 | |
| 174 | --- |
| 175 | |
| 176 | ## 7.3. Genvägen `||` utan namn: Parallella rör som returnerar en tupel |
| 177 | |
| 178 | Men tänk om du bara vill starta två eller tre beräkningar parallellt och samla ihop dem i en tupel utan att ens behöva hitta på variabelnamn med `->`? |
| 179 | |
| 180 | Då använder du `||` direkt: |
| 181 | |
| 182 | ```ridgun |
| 183 | parallellt par Int => (Int, Int) |
| 184 | parallellt par n = |
| 185 | || fib n |
| 186 | || fact n |
| 187 | ``` |
| 188 | |
| 189 | Rakt, visuellt och returnerar tupeln `(fib n, fact n)`. |
| 190 | |
| 191 | --- |
| 192 | |
| 193 | ## 7.4. När du vill hålla i ratten själv: den inbyggda agentmodellen (`agent`, `send ... to`, `receive from`) och CSP-kanaler |
| 194 | |
| 195 | Den automatiska parallellismen i `||`-block och `||map` täcker 90 % av fallen. Men när du bygger en beständig server, isolerade användarsessioner, en jobbkö eller en producent–konsument-pipeline vill du ha **samtidiga agenter med isolerat minne och statiskt typad meddelandehantering**. |
| 196 | |
| 197 | I Ridgun behövs inget särskilt nyckelord som `actor ::`: **vilken vanlig funktion som helst (`fn ::` eller `op ::`) förvandlas till en samtidig agent på en enda rad med `agent`**: |
| 198 | |
| 199 | ### 1. Tillståndslösa och tillståndsbärande agenter (`agent`, `send ... to`, `receive from`, `stop`) |
| 200 | - `ag = agent (min funktion)`: gör om en funktion `M => R` till en tillståndslös agent av typen `Agent M R`. |
| 201 | - `ag = agent (funktion med tillstand) starttillstand`: gör om en funktion `S M => S` (eller `S M => S R`) till en agent med isolerat tillstånd som minns sitt tillstånd från meddelande till meddelande utan en enda `Mutex`. |
| 202 | - `send msg to ag` (eller med flera argument: `send a b to ag`): skickar ett typat meddelande till agentens inkorg. |
| 203 | - `res = receive from ag` (eller icke-blockerande `try receive from ag`): hämtar nästa svar ur agentens utkorg. |
| 204 | - `stop ag`: stoppar agenten och frigör dess tillstånd deterministiskt via `RMM1`/`RMM2`. |
| 205 | |
| 206 | ```ridgun |
| 207 | fn :: addera besok Int Int => Int |
| 208 | addera besok ack n = ack + n |
| 209 | |
| 210 | fn :: bearbeta parti Int Int => Int |
| 211 | bearbeta parti id t = (id * 100) + t |
| 212 | |
| 213 | fn :: main => () |
| 214 | main = |
| 215 | raknare = agent (addera besok) 10 |
| 216 | send 5 to raknare |
| 217 | send 7 to raknare |
| 218 | v1 = receive from raknare |
| 219 | v2 = receive from raknare |
| 220 | stop raknare |
| 221 | |
| 222 | arbetare = agent (bearbeta parti) |
| 223 | send 3 25 to arbetare |
| 224 | parti = receive from arbetare |
| 225 | stop arbetare |
| 226 | echo "Besök:" v1 "->" v2 "| Parti:" parti |
| 227 | ``` |
| 228 | |
| 229 | ### 2. Smart val vid kompilering (*Green Threads* vs. Riktiga Kärntrådar) |
| 230 | En vanlig fråga är: *"Skapar `||`-block, `||map` och `agent`-agenter alltid operativsystemtrådar, eller körs de som lätta uppgifter?"* |
| 231 | |
| 232 | I Ridgun **fattar kompilatorn det beslutet åt dig genom att statiskt analysera varje funktion**: |
| 233 | - **Lätta funktioner (`TierTrivial` / `StratInlineSequential`)**: Om funktionen du skickar till `||`, `||map`, `||filter` eller `agent` har litet djup (aritmetik, strängformatering, snabba tillståndsövergångar i en HTTP-session) skulle det kosta mer att värma upp operativsystemtrådar än själva beräkningen. Kompilatorn kör den som ett ultralätt *green-thread*-steg utan `SYS_clone`-overhead. |
| 234 | - **Tunga eller djupt rekursiva beräkningar (`TierDynamicRecursive` / `TierHeavyCPU`)**: Om funktionen har djup rekursion (som `fib` eller `fact`) eller tensoralgebra (`einsum`, `hadamard`, `@`) genererar kompilatorn automatiskt flerkärnig utskickning över **riktiga kärntrådar (`SYS_clone`)**, och bearbetar `||`-grenar eller `agent`-meddelandekön i äkta hårdvaruparallellism över alla dina CPU-kärnor. |
| 235 | |
| 236 | ### 3. Skicka meddelanden via begränsade kanaler (`new bounded channel`) |
| 237 | Precis som i Go eller Erlang kan du utöver agenter även skapa uttryckliga rör för att koppla ihop steg: |
| 238 | |
| 239 | - `new bounded channel kapacitet`: skapar en kanal med plats för `kapacitet` meddelanden. |
| 240 | - `send through varde kanal` (eller `chan send varde kanal`): lägger in ett meddelande i röret. |
| 241 | - `receive from kanal` (eller `chan recv kanal`): hämtar nästa meddelande ur röret. |
| 242 | - `try receive from kanal`: tittar efter om det finns ett meddelande utan att bli stående och vänta; returnerar `Some varde` eller `None`. |
| 243 | - `receive from timeout kanal ticks`: väntar upp till en tidsgräns och returnerar `Ok varde` eller `Err Timeout`. |
| 244 | |
| 245 | ```ridgun |
| 246 | fn :: main => () |
| 247 | main = |
| 248 | ror = new bounded channel 4 |
| 249 | _ = send through "Uppgift A slutförd" ror |
| 250 | _ = send through "Uppgift B slutförd" ror |
| 251 | forsta = receive from ror |
| 252 | andra = receive from ror |
| 253 | echo forsta "|" andra |
| 254 | ``` |
| 255 | |
| 256 | ### 4. Sänkagenter (*fire-and-forget* `agent sink`), globala brevlådor (`agent ::`) och deterministiska tester (`with agent`) |
| 257 | När du behöver asynkron granskning, mätvärden eller spårning utan att skräpa ner alla funktionssignaturer eller samla olästa svar i en utkö erbjuder Ridgun **sänkagenter (`agent sink`)** och **globala brevlådor på toppnivå (`agent ::`)** (`agent dropped`, `agent flush`, `agent state` och `with agent audit_log = spion do ...`), samt `Channel`- och `Agent`-deskriptorer som kan delas fritt mellan flera arbetare. |
| 258 | |
| 259 | Och spänn nu fast säkerhetsbältet inför **Kapitel 08**, den stora finalen: **tensorer med kompileringstidsverifierade dimensioner, räckviddsvariabler (`context`/`shared`), deterministiska minnesregioner, direkt hårdvaruåtkomst via MMIO och UNIX/POSIX-systemprogrammering**. |