Skip to content
Ridgun
English

Ridgun

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

07-ordine-superiore-e-parallelismo-dag.md288 lines · 16.7 KBRaw
1# Capitolo 07: Funzioni di Ordine Superiore e Parallelismo senza Sudore (Né `Mutex`, né `Deadlock`)
2
3In quasi tutti i linguaggi di programmazione c'è un momento drammatico nella vita di un progetto: il giorno in cui qualcuno dice durante una riunione:
4
5> *"Questo va lento. Bisogna parallelizzarlo con i thread."*
6
7Cala il silenzio nella sala. Tutti sanno cosa viene dopo: settimane a dare la caccia a *race condition* fantasma che accadono solo il martedì di luna piena, blocchi reciproci (*deadlock*) ed errori irriproducibili.
8
9In Ridgun, quando qualcuno dice *"bisogna parallelizzarlo"*, basta anteporre `||` alle tue operazioni sulle liste (**`||map`** e **`||filter`**) o mettere i tuoi calcoli in un blocco parallelo **`||`** con frecce `->` e vai a prenderti un caffè.
10
11Perché? Perché **quando le tue funzioni sono pure e i tuoi dati sono immutabili per impostazione predefinita, due core della CPU che lavorano contemporaneamente non possono mai pestarsi i piedi**.
12
13Vediamolo passo dopo passo.
14
15---
16
17## 7.1. Funzioni che ricevono funzioni: `map`, `||map`, `filter`, `||filter` e `fold`
18
19In Ridgun una funzione è un valore come gli altri. Puoi passarla come parametro proprio come passi il numero `7`.
20
21Il linguaggio offre di serie i combinatori di collezioni per trasformare liste senza scrivere nemmeno una singola ricorsione manuale, sia in modalità sequenziale (`map`, `filter`, `fold`) sia in modalità parallela multicore con il prefisso `||` (**`||map`**, **`||filter`**):
22
23### 1. `map` (sequenziale) e `||map` (parallelo multicore)
24Supponi di avere una funzione `costoso Int => Int` e una lista di numeri:
25
26```ridgun
27costoso Int => Int
28costoso x = (x * x) + (x * 3)
29
30main => ()
31main =
32 dati = [1, 2, 3, 4, 5, 6, 7, 8]
33 -- Sequenziale stretto da sinistra a destra:
34 r1 = map costoso dati
35 -- Parallelo multicore con il prefisso `||`:
36 r2 = ||map costoso dati
37 echo r1
38 echo r2
39```
40
41- Con **`map`**, Ridgun applica la funzione elemento per elemento da sinistra a destra nel thread attuale (ideale per liste piccole o funzioni con effetti di input/output ordinati).
42- Con **`||map`**, il runtime di Ridgun distribuisce gli elementi di `dati` tra i thread worker del tuo processore e ricostruisce la lista finale **esattamente nello stesso ordine**, garantendo che `||map f xs == map f xs`.
43
44### 2. `filter` (sequenziale) e `||filter` (parallelo): Tenere solo quelli che soddisfano una condizione
45Le passi una funzione che restituisca `Bool` (un *predicato*) e una lista:
46
47```ridgun
48fn :: e positivo Int => Bool
49 e positivo n = n > 0
50
51fn :: main => ()
52 main =
53 puliti = filter e positivo [-5, 10, 0, -2, 42]
54 puliti_par = ||filter e positivo [-5, 10, 0, -2, 42]
55 echo puliti -- Stampa [10, 42]
56 echo puliti_par -- Stampa [10, 42]
57```
58
59### 3. `fold`: Ridurre un'intera lista a un solo risultato
60Le passi una funzione combinatrice a due argomenti `(Accumulato Elemento => Accumulato)`, il valore iniziale e la lista:
61
62```ridgun
63fn :: accumula prodotto Int Int => Int
64 accumula prodotto acc x = acc * x
65
66fn :: main => ()
67 main =
68 echo (fold accumula prodotto 1 [2, 3, 4, 5]) -- 1 * 2 * 3 * 4 * 5 = 120
69```
70
71### Come scrivo io la mia funzione che riceve un'altra funzione?
72Facilissimo: nella firma dei tipi metti la firma della funzione ospite tra parentesi `(Ingresso => Uscita)`:
73
74```ridgun
75fn :: esegui due volte (A => A) A => A
76 esegui due volte f x = f (f x)
77```
78
79### 4. Chiusure lessicali e funzioni anonime (`(x => expr)`, `(acc x => expr)`)
80Quando vuoi passare una funzione breve al volo o catturare variabili locali del tuo ambito attuale (come uno scostamento, un prefisso o una sessione attiva) senza dichiarare una funzione globale ausiliaria, usi una **chiusura lessicale** tra parentesi con **`=>`**:
81
82```ridgun
83fn :: elabora con sessione => [Int]
84 elabora con sessione =
85 base = 10
86 bonus = 3
87 sommati = map (x => x + base) [1, 2, 3]
88 filtrati = filter (x => x >= bonus) [1, 2, 3, 4]
89 totale = fold (acc x => acc + x + bonus) 0 [1, 2, 3]
90 coppie = map ((a b) => a + b + base) [(1, 2), (3, 4)]
91 sommati :: filtrati :: [totale] :: coppie
92```
93
94Cinque regole mantengono le lambda pulite e prive di ambiguità:
95- **Parentesi obbligatorie**: ogni lambda va sempre tra parentesi `(... => ...)`.
96- **Lambda senza parametri (`(=> expr)`)**: proprio come `fn :: main => ()` non porta `()` prima di `=>`, una lambda senza parametri si scrive `(=> expr)` (e il suo tipo è `(=> Tipo)`), mai `(() => expr)`.
97- **Più parametri rispetto a una tupla (`(a b => ...)` vs. `((a b) => ...)`)**: `(acc x => ...)` riceve due parametri separati; `((a b) => ...)` riceve un'unica tupla e la destruttura.
98- **Corpo su più righe**: se vai a capo dopo `=>`, puoi scrivere un blocco indentato di più passi prima della `)` di chiusura:
99 ```ridgun
100 doppi = map (x =>
101 passo = x * 2
102 passo + base
103 ) [1, 2, 3]
104 ```
105- **Maiuscole rispetto a minuscole**: `(Conn => Conn)` è un tipo di funzione (iniziale maiuscola) e `(conn => conn)` è un valore lambda (iniziale minuscola).
106
107Le variabili catturate (`base`, `bonus`) vengono impacchettate automaticamente con gestione deterministica della memoria (`RMM1`/`RMM2`), senza perdite e senza pause del garbage collector.
108
109### 5. Lo stile più idiomatico di Ridgun (consigliato): Pipeline fluide con `->`, invocazione da sinistra e blocchi senza `_ =` (`FR-COMP-014`)
110Scrivere `map f xs` o assegnare ogni passo intermedio a una variabile (`r1 = ...`, `r2 = ...`) va benissimo, ma dove Ridgun si gode al 100% è concatenando trasformazioni da sinistra a destra con **`->`** e legando il risultato finale direttamente con **`-> variabile`**:
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 -- Stile consigliato (2b): avvio infisso naturale + `->` tra le tappe + `-> results`
124 xs map (x => x * 2) -> filter (x => x > 6) -> compact limit 14 -> results
125 echo results -- Stampa [8, 10, 12, 14]
126```
127
128Ridgun unifica i quattro modi di scrivere la catena nello stesso albero sintattico (AST), così puoi scegliere quello che si legge meglio in ogni caso:
1291. **Stile `2b` (consigliato)**: `xs map f -> filter g -> compact limit 14 -> results` (avvio infisso naturale con il soggetto a sinistra, `->` a separare ogni tappa e `-> results` alla fine).
1302. **Stile `2` (`->` dal valore iniziale)**: `xs -> map f -> filter g -> compact limit 14 -> results`.
1313. **Stile `1` (infisso continuo senza `->` intermedi)**: `xs map f filter g compact limit 14 -> results`.
1324. **Stile `3` (avvio prefisso classico + `->`)**: `map f xs -> filter g -> compact limit 14 -> results`.
133
134E se la pipeline è lunga, puoi spezzarla su più righe indentate che iniziano con `->`:
135
136```ridgun
137 xs map (x => x * 3)
138 -> filter (x => x >= 12)
139 -> fold 0 (acc x => acc + x)
140 -> totale
141```
142
143La regola universale di `->` è semplice e coerente in tutto il linguaggio:
144- **Il valore che arriva da sinistra entra come PRIMO argomento** della funzione o dell'operatore di destra (`x -> f a b` $\equiv$ `x f a b` $\equiv$ `f x a b`), tranne in `map`/`filter`/`fold` (dove entra come la collezione su cui iterare) o se posizioni un jolly esplicito **`_`** (`payload -> write file percorso_uscita _`).
145- **Invio diretto agli agenti**: `[5, 10, 15] -> somma lista -> send to ag` e `[1, 2, 3] -> somma lista -> ask to ag -> totale`.
146- **Maschere di sequenza in transito (`-> (+ _ _ +) ->`)**: `analizza pacchetto "v0.9.9" -> (+ _ _ +) -> combina codice -> riepilogo`.
147- **Blocchi sequenziali puliti senza `_ =` e scarto esplicito `-> ()`**: le chiamate con effetti collaterali (`echo`, `write file`, `shell exec`, `log`, ecc.) si scrivono direttamente senza `_ =`. Se l'ultima riga di una funzione `=> ()` chiama una funzione che restituisce un valore (come `Int`), basta terminare quella riga con **`-> ()`** per scartare esplicitamente il risultato e restituire `()`.
148
149---
150
151## 7.2. Il fiore all'occhiello: Blocchi `||` con `->` come Grafi Paralleli (DAG)
152
153Qui è dove Ridgun cambia il tuo modo di pensare.
154
155In un normale linguaggio imperativo, quando scrivi tre righe di fila:
1561. Prima viene eseguita la riga 1.
1572. Poi la riga 2 (anche se non ha nulla a che vedere con la riga 1).
1583. Poi la riga 3.
159
160Questo significa costringere il computer a lavorare con i paraocchi. In Ridgun, quando usi le barre parallele **`||`** con le frecce **`->`** (senza bisogno di scrivere alcuna parola chiave come `where`), non stai dando ordini sequenziali: stai disegnando un **Grafo Aciclico Diretto (DAG)** che dice quale calcolo alimenta quale variabile:
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 calcolo => Int
173 super calcolo = ramo_sinistro + ramo_destro
174 || fib 10 -> ramo_sinistro
175 || fact 6 -> ramo_destro
176```
177
178Leggilo ad alta voce:
179- *"Voglio calcolare `ramo_sinistro + ramo_destro`, mentre in parallelo (`||`) `fib 10` va in `ramo_sinistro` (`->`) e `fact 6` va in `ramo_destro` (`->`)."*
180
181### Cosa fa `ridgunc` dietro le quinte con le tue righe `||`:
1821. Guarda le frecce `->` e costruisce l'albero delle dipendenze (DAG).
1832. **Cattura i cicli staticamente**: se per errore scrivi `|| a + 1 -> b` e `|| b + 1 -> a`, il compilatore ti dice all'istante *"Fermi tutti! C'è una dipendenza circolare"* e non ti lascia compilare un blocco infinito.
1843. Vede che `fib 10` e `fact 6` non dipendono l'uno dall'altro, quindi hanno via libera per correre in parallelo prima di sommarsi in `ramo_sinistro + ramo_destro`.
185
186Puoi persino montare una pipeline a più piani dentro lo stesso blocco `||` — sia in fondo all'espressione sia subito dopo l'`=` — e il compilatore dedurrà da solo quali piani possono andare contemporaneamente e quali devono aspettare:
187
188```ridgun
189fn :: bilancio trimestrale Int => Int
190 bilancio trimestrale base =
191 || base * 100 -> vendite_lorde
192 || base * 40 -> costi_fissi
193 || vendite_lorde - costi_fissi -> utile_lordo ||.
194 utile_lordo * 80 / 100
195```
196
197Nota il dettaglio **`||.`** alla fine dell'ultima riga `||`: proprio come `|.` chiude un albero di guardie annidato, **`||.`** chiude il blocco parallelo `||` quando sotto seguono normali istruzioni sequenziali. Qui `vendite_lorde` e `costi_fissi` partono in parallelo; quando entrambi sono pronti si calcola `utile_lordo`, e infine si restituisce `utile_lordo * 80 / 100`. Tutto senza un singolo `async`, `Future`, `Promise` o `Mutex`.
198
199---
200
201## 7.3. La scorciatoia `||` senza nomi: Pipeline parallele che restituiscono una tupla
202
203E se vuoi solo lanciare due o tre calcoli in parallelo e raccoglierli insieme in una tupla senza nemmeno inventare nomi di variabili con `->`?
204
205Usi `||` direttamente:
206
207```ridgun
208fn :: coppia parallela Int => Int Int
209 coppia parallela n =
210 || fib n
211 || fact n
212```
213
214Diretto, visivo e restituisce la tupla `(fib n, fact n)`.
215
216---
217
218## 7.4. Quando vuoi il volante manuale: il Modello Nativo di Agenti (`agent`, `send ... to`, `receive from`) e Canali CSP
219
220Il parallelismo automatico di `||` e `||map` copre il 90% dei casi. Ma quando programmi un server persistente, sessioni utente con stato isolato, code di lavoro o una pipeline produttore-consumatore, vuoi **agenti concorrenti con memoria isolata e passaggio di messaggi tipizzato**.
221
222In Ridgun non serve inventare una sintassi strana come `actor ::`: **qualsiasi funzione normale (`fn ::` o `op ::`) si trasforma in un agente concorrente in una riga con `agent`**:
223
224### 1. Agenti senza stato e con stato isolato (`agent`, `send ... to`, `receive from`, `stop`)
225- `ag = agent (mia funzione)`: converte una funzione `M => R` in un agente senza stato di tipo `Agent M R`.
226- `ag = agent (mia funzione con stato) stato_iniziale`: converte una funzione `S M => S` (o `S M => S R`) in un agente con stato isolato che ricorda il suo stato messaggio dopo messaggio senza un singolo `Mutex`.
227- `send msg to ag` (o con più argomenti: `send a b to ag`): invia un messaggio tipizzato alla casella di posta dell'agente.
228- `res = receive from ag` (o `try receive from ag` senza bloccare): raccoglie la risposta successiva dalla casella di uscita dell'agente.
229- `stop ag`: ferma l'agente e libera automaticamente il suo stato mediante `RMM1`/`RMM2`.
230
231```ridgun
232fn :: somma visita Int Int => Int
233 somma visita acum n = acum + n
234
235fn :: elabora lotto Int Int => Int
236 elabora lotto id t = (id * 100) + t
237
238fn :: main => ()
239 main =
240 contatore = agent (somma visita) 10
241 send 5 to contatore
242 send 7 to contatore
243 v1 = receive from contatore
244 v2 = receive from contatore
245 stop contatore
246
247 lavoratore = agent (elabora lotto)
248 send 3 25 to lavoratore
249 lotto = receive from lavoratore
250 stop lavoratore
251 echo "Visite:" v1 "->" v2 "| Lotto:" lotto
252```
253
254### 2. Scelta intelligente a tempo di compilazione (*Green Thread* vs. Thread Reali del Kernel)
255Una domanda frequente è: *"I blocchi `||`, `||map` e gli agenti `agent` creano sempre thread del sistema operativo o girano come task leggeri?"*
256
257In Ridgun, **il compilatore prende questa decisione per te analizzando staticamente ogni funzione**:
258- **Funzioni leggere (`TierTrivial` / `StratInlineSequential`)**: Se la funzione che passi a `||`, `||map`, `||filter` o `agent` ha poca profondità (aritmetica, formattazione di stringhe, transizione di stato rapida di una sessione HTTP), riscaldare thread del sistema operativo costerebbe più del calcolo stesso. Il compilatore la esegue come un passo *green-thread* ultraleggero senza sovraccarico di `SYS_clone`.
259- **Calcoli pesanti o ricorsivi profondi (`TierDynamicRecursive` / `TierHeavyCPU`)**: Se la funzione ha ricorsione profonda (come `fib` o `fact`) o algebra dei tensori (`einsum`, `hadamard`, `@`), il compilatore genera automaticamente lo smistamento multicore su **thread reali del kernel (`SYS_clone`)**, elaborando i rami `||` o la coda di messaggi dell'`agent` in vero parallelo su tutti i core della tua CPU.
260
261### 3. Passare messaggi tramite canali limitati (`new bounded channel`)
262Proprio come in Go o Erlang, oltre agli agenti puoi anche creare tubi espliciti per far comunicare le tappe:
263
264- `new bounded channel capacita`: crea un canale con spazio per `capacita` messaggi.
265- `send through valore canale` (o `chan send valore canale`): mette un messaggio nel tubo.
266- `receive from canale` (o `chan recv canale`): estrae il messaggio successivo dal tubo.
267- `try receive from canale`: guarda se c'è un messaggio senza restare in attesa; restituisce `Some valore` o `None`.
268- `receive from timeout canale tick`: attende un limite di tempo e restituisce `Ok valore` o `Err Timeout`.
269
270```ridgun
271fn :: main => ()
272 main =
273 tubo = new bounded channel 4
274 send through "Attività A completata" tubo
275 send through "Attività B completata" tubo
276 primo = receive from tubo
277 secondo = receive from tubo
278 echo primo "|" secondo
279```
280
281### 4. Agenti pozzo (*fire-and-forget* `agent sink`), caselle globali (`agent ::`) e test deterministici (`with agent`)
282Quando hai bisogno di audit asincrono, metriche, tracce o *rate limiting* trasversale senza sporcare tutte le firme con un parametro extra né accumulare risposte non lette in una coda di uscita, Ridgun offre **agenti pozzo (`agent sink`)** e **caselle globali di livello superiore (`agent ::`)**:
283- `rec = agent sink (passo) [stato_iniziale], mailbox N drop|block, drain 2_s`: elabora ogni messaggio in ordine FIFO (`S M => S` o `M => ()`), libera lo stato precedente con `RMM1`/`RMM2` e **non accumula coda di uscita** (`receive from`, `try receive from` e `ask` su un `SinkAgent` sono errori di compilazione).
284- `agent :: audit_log = sink (passo) [], mailbox 10_000 drop, drain 2_s`: dichiara un agente pozzo globale inizializzato prima di `main` e svuotato ordinatamente all'uscita del programma.
285- `agent dropped ag`, `agent flush ag`, `agent state ag` e `with agent audit_log = spia do ...`: permettono di osservare i messaggi scartati per contropressione (`drop`), svuotare la casella, ispezionare lo stato e reindirizzare temporaneamente una casella globale a un agente spia isolato nei test.
286- Inoltre, i descrittori di `Channel`, `Agent` e `SinkAgent` possono essere condivisi liberamente tra più worker (`send ch 1 to w1`, `send ch 2 to w2`), mentre i dati inviati attraverso il canale mantengono la loro proprietà affine (`ErrUseAfterSend`).
287
288E ora, allaccia le cinture per il **Capitolo 08**, il gran finale: **tensori con dimensioni verificate in compilazione, variabili di ambito (`context`/`shared`), regioni di memoria deterministiche, accesso diretto all'hardware MMIO e programmazione di sistema UNIX/POSIX**.