Skip to content
Ridgun
English

Ridgun

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

07-hogre-ordningen-och-dag-parallellism.md259 lines · 13.9 KBRaw
1# Kapitel 07: Högre ordningens funktioner och svettfri parallellism (Utan `Mutex`, utan `Deadlocks`)
2
3I 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
7Det 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
9Nä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
11Varfö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
13Lå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
19I 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
21Språ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)
24Anta att du har en funktion `tung Int => Int` och en lista med tal:
25
26```ridgun
27tung Int => Int
28tung x = (x * x) + (x * 3)
29
30main => ()
31main =
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
45Du skickar in en funktion som returnerar `Bool` (ett *predikat*) och en lista:
46
47```ridgun
48ar positiv Int => Bool
49ar positiv n = n > 0
50
51main => ()
52main =
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
60Du skickar in en kombinerande funktion med två argument `(Ackumulerat Element => Ackumulerat)`, startvärdet och listan:
61
62```ridgun
63ackumulera produkt Int Int => Int
64ackumulera produkt acc x = acc * x
65
66main => ()
67main =
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?
72Hur enkelt som helst: i typsignaturen sätter du den inbjudna funktionens signatur inom parentes `(Indata => Utdata)`:
73
74```ridgun
75kor tva ganger (A => A) A => A
76kor tva ganger f x = f (f x)
77```
78
79### 4. Idiomatiska pipelines från vänster till höger (`->`) — 100 % Ridgun-stil!
80Nä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
82I 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):
86xs 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:
89tolka paket "v0.9.9" -> (+ _ _ +) -> kombinera kod -> sammanfattning
90```
91
92Dessutom 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)`)
95Nä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
98fn :: 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
109Fem 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
122Få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
128Det är här Ridgun förändrar ditt sätt att tänka.
129
130När du i ett vanligt imperativt språk skriver tre rader efter varandra:
1311. Först körs rad 1.
1322. Sedan rad 2 (även om den inte har det minsta med rad 1 att göra).
1333. Sedan rad 3.
134
135Det ä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
138fib Int => Int
139fib 0 = 0
140fib 1 = 1
141fib n = (fib (n - 1)) + (fib (n - 2))
142
143fact Int => Int
144fact 0 = 1
145fact n = n * (fact (n - 1))
146
147super berakning => Int
148super berakning = vanster_gren + hoger_gren
149 || fib 10 -> vanster_gren
150 || fact 6 -> hoger_gren
151```
152
153Lä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:
1571. Den tittar på pilarna `->` och bygger upp beroendeträdet (DAG).
1582. **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.
1593. 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
161Du 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
164kvartalsbalans Int => Int
165kvartalsbalans bas =
166 || bas * 100 -> bruttoforsaljning
167 || bas * 40 -> fasta_kostnader
168 || bruttoforsaljning - fasta_kostnader -> bruttovinst ||.
169 bruttovinst * 80 / 100
170```
171
172Lä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
178Men 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
180Då använder du `||` direkt:
181
182```ridgun
183parallellt par Int => (Int, Int)
184parallellt par n =
185 || fib n
186 || fact n
187```
188
189Rakt, 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
195Den 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
197I 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
207fn :: addera besok Int Int => Int
208 addera besok ack n = ack + n
209
210fn :: bearbeta parti Int Int => Int
211 bearbeta parti id t = (id * 100) + t
212
213fn :: 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)
230En vanlig fråga är: *"Skapar `||`-block, `||map` och `agent`-agenter alltid operativsystemtrådar, eller körs de som lätta uppgifter?"*
231
232I 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`)
237Precis 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
246fn :: 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`)
257Nä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
259Och 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**.