Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capitolo 07: Funzioni di Ordine Superiore e Parallelismo senza Sudore (Né `Mutex`, né `Deadlock`) |
| 2 | |
| 3 | In 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 | |
| 7 | Cala 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 | |
| 9 | In 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 | |
| 11 | Perché? 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 | |
| 13 | Vediamolo passo dopo passo. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## 7.1. Funzioni che ricevono funzioni: `map`, `||map`, `filter`, `||filter` e `fold` |
| 18 | |
| 19 | In Ridgun una funzione è un valore come gli altri. Puoi passarla come parametro proprio come passi il numero `7`. |
| 20 | |
| 21 | Il 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) |
| 24 | Supponi di avere una funzione `costoso Int => Int` e una lista di numeri: |
| 25 | |
| 26 | ```ridgun |
| 27 | costoso Int => Int |
| 28 | costoso x = (x * x) + (x * 3) |
| 29 | |
| 30 | main => () |
| 31 | main = |
| 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 |
| 45 | Le passi una funzione che restituisca `Bool` (un *predicato*) e una lista: |
| 46 | |
| 47 | ```ridgun |
| 48 | fn :: e positivo Int => Bool |
| 49 | e positivo n = n > 0 |
| 50 | |
| 51 | fn :: 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 |
| 60 | Le passi una funzione combinatrice a due argomenti `(Accumulato Elemento => Accumulato)`, il valore iniziale e la lista: |
| 61 | |
| 62 | ```ridgun |
| 63 | fn :: accumula prodotto Int Int => Int |
| 64 | accumula prodotto acc x = acc * x |
| 65 | |
| 66 | fn :: 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? |
| 72 | Facilissimo: nella firma dei tipi metti la firma della funzione ospite tra parentesi `(Ingresso => Uscita)`: |
| 73 | |
| 74 | ```ridgun |
| 75 | fn :: 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)`) |
| 80 | Quando 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 |
| 83 | fn :: 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 | |
| 94 | Cinque 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 | |
| 107 | Le 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`) |
| 110 | Scrivere `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 |
| 113 | op :: [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 | |
| 119 | fn :: 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 | |
| 128 | Ridgun unifica i quattro modi di scrivere la catena nello stesso albero sintattico (AST), così puoi scegliere quello che si legge meglio in ogni caso: |
| 129 | 1. **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). |
| 130 | 2. **Stile `2` (`->` dal valore iniziale)**: `xs -> map f -> filter g -> compact limit 14 -> results`. |
| 131 | 3. **Stile `1` (infisso continuo senza `->` intermedi)**: `xs map f filter g compact limit 14 -> results`. |
| 132 | 4. **Stile `3` (avvio prefisso classico + `->`)**: `map f xs -> filter g -> compact limit 14 -> results`. |
| 133 | |
| 134 | E 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 | |
| 143 | La 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 | |
| 153 | Qui è dove Ridgun cambia il tuo modo di pensare. |
| 154 | |
| 155 | In un normale linguaggio imperativo, quando scrivi tre righe di fila: |
| 156 | 1. Prima viene eseguita la riga 1. |
| 157 | 2. Poi la riga 2 (anche se non ha nulla a che vedere con la riga 1). |
| 158 | 3. Poi la riga 3. |
| 159 | |
| 160 | Questo 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 |
| 163 | fn :: fib Int => Int |
| 164 | fib 0 = 0 |
| 165 | fib 1 = 1 |
| 166 | fib n = (fib (n - 1)) + (fib (n - 2)) |
| 167 | |
| 168 | fn :: fact Int => Int |
| 169 | fact 0 = 1 |
| 170 | fact n = n * (fact (n - 1)) |
| 171 | |
| 172 | fn :: super calcolo => Int |
| 173 | super calcolo = ramo_sinistro + ramo_destro |
| 174 | || fib 10 -> ramo_sinistro |
| 175 | || fact 6 -> ramo_destro |
| 176 | ``` |
| 177 | |
| 178 | Leggilo 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 `||`: |
| 182 | 1. Guarda le frecce `->` e costruisce l'albero delle dipendenze (DAG). |
| 183 | 2. **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. |
| 184 | 3. 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 | |
| 186 | Puoi 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 |
| 189 | fn :: 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 | |
| 197 | Nota 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 | |
| 203 | E se vuoi solo lanciare due o tre calcoli in parallelo e raccoglierli insieme in una tupla senza nemmeno inventare nomi di variabili con `->`? |
| 204 | |
| 205 | Usi `||` direttamente: |
| 206 | |
| 207 | ```ridgun |
| 208 | fn :: coppia parallela Int => Int Int |
| 209 | coppia parallela n = |
| 210 | || fib n |
| 211 | || fact n |
| 212 | ``` |
| 213 | |
| 214 | Diretto, 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 | |
| 220 | Il 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 | |
| 222 | In 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 |
| 232 | fn :: somma visita Int Int => Int |
| 233 | somma visita acum n = acum + n |
| 234 | |
| 235 | fn :: elabora lotto Int Int => Int |
| 236 | elabora lotto id t = (id * 100) + t |
| 237 | |
| 238 | fn :: 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) |
| 255 | Una domanda frequente è: *"I blocchi `||`, `||map` e gli agenti `agent` creano sempre thread del sistema operativo o girano come task leggeri?"* |
| 256 | |
| 257 | In 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`) |
| 262 | Proprio 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 |
| 271 | fn :: 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`) |
| 282 | Quando 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 | |
| 288 | E 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**. |