Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capitolo 06: Tipi di Dati Algebrici (ADT), Polimorfismo e perché qui non vogliamo `null` né `Any` |
| 2 | |
| 3 | Nel 1965, Tony Hoare inventò il riferimento nullo (`null` / `nil`). Quarant'anni dopo chiese pubblicamente scusa e lo definì *"il mio errore da un miliardo di dollari"*. |
| 4 | |
| 5 | Perché? Perché nei linguaggi con `null`, una funzione ti promette nella sua firma che ti restituirà un `Utente`... e quando meno te lo aspetti ti restituisce `null` e ti butta giù il server alle tre del mattino. |
| 6 | |
| 7 | E altrettanto succede con l'ereditarietà degli oggetti e il tipo `Any` (o `Object`): metti un numero e un testo in una lista `[1, "due"]`, il linguaggio alza le spalle e dice *"va bene, allora adesso questa è una `List[Any]`, buona fortuna a indovinare cosa c'è dentro quando la percorrerai"*. |
| 8 | |
| 9 | In Ridgun tagliamo entrambi i problemi alla radice con i **Tipi di Dati Algebrici (ADT)** e il **Polimorfismo Parametrico**. Suona come il nome di un esame tosto, ma in cinque minuti vedrai che è il modo più naturale del mondo per modellare i dati. |
| 10 | |
| 11 | --- |
| 12 | |
| 13 | ## 6.1. Tipi Prodotto (Record): I tuoi soliti `struct`, ma con i superpoteri |
| 14 | |
| 15 | Quando vuoi raggruppare diversi dati che appartengono a una stessa entità (ad esempio, un server con il suo nome, il suo IP e la sua porta), definisci un **`type`** con i suoi campi: |
| 16 | |
| 17 | ```ridgun |
| 18 | type Server = host: String, porta: Int, tls_attivo: Bool |
| 19 | ``` |
| 20 | |
| 21 | Scrivendo solo quella riga, Ridgun ti regala **quattro cose** automaticamente: |
| 22 | |
| 23 | ### 1. Il costruttore `Server` |
| 24 | ```ridgun |
| 25 | fn :: mio nodo => Server |
| 26 | mio nodo = Server "api.esempio.it" 443 True |
| 27 | ``` |
| 28 | |
| 29 | ### 2. Una funzione *getter* pura per ogni campo (`host`, `porta`, `tls_attivo`) |
| 30 | Non devi scrivere `s.getHost()` né nulla di simile; ogni nome di campo è direttamente una funzione che estrae quel dato: |
| 31 | |
| 32 | ```ridgun |
| 33 | fn :: url connessione Server => String |
| 34 | url connessione srv = "Connettere a " + (host srv) + " sulla porta {porta srv}" |
| 35 | ``` |
| 36 | |
| 37 | ### 3. Aggiornamento funzionale dei record (`with campo: valore`) |
| 38 | Immagina un record di configurazione con 12 o 15 campi (come le opzioni di un comando `ls` o di un server web) e vuoi cambiare solo uno o due campi senza toccare il resto. Invece di ricostruire il record passando i 15 campi a mano, usi **`with`** e i due punti **`:`** (esattamente come nella definizione del `type`): |
| 39 | |
| 40 | ```ridgun |
| 41 | fn :: attiva modalita dev Server => Server |
| 42 | attiva modalita dev srv = srv with porta: 8080, tls_attivo: False |
| 43 | ``` |
| 44 | |
| 45 | Il meglio dei due mondi: |
| 46 | - Nel tuo codice è **100% immutabile e sicuro** (il compilatore controlla che `porta` e `tls_attivo` esistano in `Server` e abbiano il tipo corretto). |
| 47 | - Sotto il cofano, se `srv` non è condiviso con nessun altro, l'eseguibile nativo riutilizza la sua memoria *in-place* (`Arc::try_unwrap`) senza copiare il resto dei campi. |
| 48 | |
| 49 | ### 4. *Pattern matching* completo se vuoi aprire il record da cima a fondo |
| 50 | ```ridgun |
| 51 | fn :: e porta sicura Server => Bool |
| 52 | e porta sicura (Server _ 443 True) = True |
| 53 | e porta sicura _ = False |
| 54 | ``` |
| 55 | |
| 56 | --- |
| 57 | |
| 58 | ## 6.2. Tipi Somma (Varianti con `|`): Come mandare in pensione `null`, le eccezioni e l'ereditarietà |
| 59 | |
| 60 | Ora viene il bello. |
| 61 | Un record normale ha `host` **E** `porta` **E** `tls_attivo`. |
| 62 | Ma nel mondo reale molte volte hai bisogno di dire: *"Un metodo di pagamento è **O** una carta con il suo numero, **O** un bonifico con il suo IBAN, **O** contanti (che non portano alcun dato extra)"*. |
| 63 | |
| 64 | Nella programmazione orientata agli oggetti dovresti creare un'interfaccia `MetodoPagamento`, tre sottoclassi in tre file diversi, costruttori, `instanceof`... In Ridgun è **una riga**: |
| 65 | |
| 66 | ```ridgun |
| 67 | type MetodoPagamento = Carta String Int | Bonifico String | Contanti |
| 68 | ``` |
| 69 | |
| 70 | E guarda cosa succede quando elabori un `MetodoPagamento` in una funzione: |
| 71 | |
| 72 | ```ridgun |
| 73 | fn :: descrivi pagamento MetodoPagamento => String |
| 74 | descrivi pagamento (Carta num _) = "Pagamento con carta che termina con {num}" |
| 75 | descrivi pagamento (Bonifico iban) = "Bonifico dal conto {iban}" |
| 76 | descrivi pagamento Contanti = "Pagamento in contanti allo sportello" |
| 77 | ``` |
| 78 | |
| 79 | Ogni variante (`Carta`, `Bonifico`, `Contanti`) porta attaccati unicamente i dati che hanno senso per lei. È **impossibile** costruire un pagamento in `Contanti` che abbia un `iban` appeso per errore. |
| 80 | |
| 81 | --- |
| 82 | |
| 83 | ## 6.3. "E come metto `[1, "due"]` in una lista senza usare `Any`?" |
| 84 | |
| 85 | Torniamo alla domanda da un milione di dollari. Immagina di leggere un file CSV o JSON in cui una cella può essere un numero intero, un testo o essere vuota. |
| 86 | |
| 87 | Se avessi un tipo `Any`, metteresti `[1, "due"]` e perderesti tutto l'aiuto del compilatore: da quel momento in poi qualsiasi operazione sulla lista sarebbe camminare bendati. |
| 88 | |
| 89 | In Ridgun (proprio come in Haskell o Rust), crei in due secondi un tipo somma che spiega esattamente quali cose possono vivere dentro quella lista: |
| 90 | |
| 91 | ```ridgun |
| 92 | type ValoreCella = Intero Int | Testo String | Vuoto |
| 93 | |
| 94 | fn :: formatta cella ValoreCella => String |
| 95 | formatta cella (Intero n) = "Numero: {n}" |
| 96 | formatta cella (Testo s) = "Testo: {s}" |
| 97 | formatta cella Vuoto = "(vuoto)" |
| 98 | |
| 99 | fn :: mia riga => [ValoreCella] |
| 100 | mia riga = [Intero 1, Testo "due", Vuoto] |
| 101 | ``` |
| 102 | |
| 103 | Vedi la differenza? |
| 104 | - Con `Any`, il compilatore non sa se nella lista ci sono interi, stringhe o una caffettiera collegata via USB. |
| 105 | - Con `[ValoreCella]`, puoi mescolare numeri e testi nella stessa lista, **ma il compilatore sa al 100% che esistono solo quelle tre possibilità** e ti obbliga a spacchettare ogni caso (`Intero n`, `Testo s`, `Vuoto`) prima di usarlo. Sicurezza totale senza perdere nemmeno un briciolo di flessibilità. |
| 106 | |
| 107 | --- |
| 108 | |
| 109 | ## 6.4. `Option` e `Result`: Quando qualcosa può non esserci o può fallire |
| 110 | |
| 111 | Poiché in Ridgun non esiste `null` né esistono eccezioni che saltano per aria (`throw new Exception()`), il linguaggio porta integrati due tipi somma universali: |
| 112 | |
| 113 | 1. **`Option`**: o hai **`Some valore`**, o hai **`None`** (niente). |
| 114 | 2. **`Result`**: o l'operazione è andata bene (**`Ok valore`**), o è fallita per un motivo preciso (**`Err errore`**). |
| 115 | |
| 116 | ```ridgun |
| 117 | fn :: cerca divisione Int Int => Result |
| 118 | cerca divisione _ 0 = Err "Ehi! Non si può dividere per zero" |
| 119 | cerca divisione a b = Ok (a / b) |
| 120 | |
| 121 | fn :: stampa ris Result => () |
| 122 | stampa ris (Ok n) = echo "Il risultato è {n}" |
| 123 | stampa ris (Err msg) = echo "Errore controllato: {msg}" |
| 124 | ``` |
| 125 | |
| 126 | Nessuno può ignorare un errore per distrazione perché lo stesso tipo `Result` ti obbliga a guardare se dentro c'è un `Ok` o un `Err`. |
| 127 | |
| 128 | --- |
| 129 | |
| 130 | ## 6.5. Polimorfismo Parametrico: Funzioni generiche con `A`, `B`, `T`... |
| 131 | |
| 132 | Supponi di scrivere una funzione che inverte una coppia `(x, y)` o che estrae il primo elemento di una lista. Importa davvero se la lista è di `Int`, di `String` o di `Server`? Proprio per niente! L'algoritmo è identico. |
| 133 | |
| 134 | In Ridgun, **qualsiasi lettera maiuscola singola (`A`..`Z`)** in una firma dei tipi funziona automaticamente come un **tipo jolly polimorfico (variabile di tipo)**: |
| 135 | |
| 136 | ```ridgun |
| 137 | fn :: testa sicura [A] A => A |
| 138 | testa sicura [] predefinito = predefinito |
| 139 | testa sicura (x:_) _ = x |
| 140 | |
| 141 | fn :: gira coppia (A B) => B A |
| 142 | gira coppia (x, y) = (y, x) |
| 143 | ``` |
| 144 | |
| 145 | Nota la magia di `testa sicura [A] A => A` e `gira coppia (A B) => B A`: |
| 146 | - Non è un `Any` incontrollato: dice che **il tipo della lista `[A]`, il tipo del valore `predefinito` (`A`) e il tipo restituito (`A`) devono essere lo stesso**. |
| 147 | - Se chiami `testa sicura [10, 20] 0`, il compilatore tira le somme (`A = Int`) e ti restituisce un `Int`. |
| 148 | - Se chiami `testa sicura ["a", "b"] "nessuno"`, tira le somme (`A = String`) e ti restituisce uno `String`. |
| 149 | - Ma se provi a barare e chiami `testa sicura [10, 20] "nessuno"`, zac! Il compilatore ti ferma su due piedi perché il primo argomento dice che `A` è `Int` e il secondo dice che `A` è `String`. |
| 150 | |
| 151 | --- |
| 152 | |
| 153 | ## 6.6. Crea i tuoi operatori (`op ::`): Dai vettori alle frasi su misura |
| 154 | |
| 155 | Perché solo i numeri nativi dovrebbero avere il diritto di usare `+` o `*`? E perché accontentarsi dei soliti operatori quando puoi inventare la sintassi perfetta per il tuo problema? |
| 156 | |
| 157 | Con **`op ::`** (o direttamente senza prefisso se usi pattern di operatore) puoi fare due prodezze: |
| 158 | |
| 159 | ### 1. Insegnare a `+`, `-`, `*`, `/`, `%` a operare con i tuoi tipi |
| 160 | ```ridgun |
| 161 | type Complesso = reale: Int, immag: Int |
| 162 | |
| 163 | op :: Complesso + Complesso => Complesso |
| 164 | a + b = Complesso ((reale a) + (reale b)) ((immag a) + (immag b)) |
| 165 | ``` |
| 166 | |
| 167 | ### 2. Creare operatori *Mixfix* (che si leggono come frasi naturali, inclusi `type` e `with`) |
| 168 | In una firma di operatore, **le parole con iniziale maiuscola (`Int`, `String`, `A`..`Z`) o tra parentesi quadre/tonde (`[Int]`, `(A, B)`) sono gli slot per gli argomenti**, e **le parole in minuscolo fanno parte della frase dell'operatore** (incluse parole comunissime come `type` o `with`, che in Ridgun sono contestuali e non ti bloccano mai): |
| 169 | |
| 170 | ```ridgun |
| 171 | op :: se Bool allora A altrimenti A => A |
| 172 | se True allora v altrimenti _ = v |
| 173 | se False allora _ altrimenti f = f |
| 174 | |
| 175 | op :: format String type String with Int => String |
| 176 | format label type kind with w = "{label} ({kind}, width={w})" |
| 177 | |
| 178 | main => () |
| 179 | main = |
| 180 | messaggio = se (10 > 5) allora "Vince il dieci!" altrimenti "Impossibile" |
| 181 | echo messaggio |
| 182 | echo (format "panel" type "bordered" with 80) |
| 183 | ``` |
| 184 | |
| 185 | Ti sei appena costruito il tuo operatore ternario polimorfico e un DSL di formattazione in **3 righe** di Ridgun. |
| 186 | |
| 187 | --- |
| 188 | |
| 189 | ## 6.7. Costruttori condivisi, varianti di tipo diretto, `expand type` e spazi naturali in `fn ::` |
| 190 | |
| 191 | Ridgun porta i tipi algebrici e le funzioni un passo oltre in espressività ed estensibilità: |
| 192 | |
| 193 | 1. **Costruttori condivisi tra diversi `type`**: Più tipi somma possono riutilizzare lo stesso nome di costruttore (`Error`, `Var`, `Lit`) con uguale o diversa arità; il verificatore dei tipi li distingue in base alla colonna della firma. |
| 194 | 2. **Varianti con nome di tipo diretto (`type Something = String | Int | MyStruct Float Int`)**: Quando una variante è un tipo primitivo (`String`, `Int`, `Float`/`Real`, `Bool`, `Char`), non hai bisogno di ripetere `Int Int` né `String String`. Puoi passare valori diretti (`"ciao"`, `42`) o costruttori espliciti (`String "ciao"`, `Int 42`), e fare pattern matching sia con letterali (`"admin"`, `0`) sia con pattern tipizzati (`(String s)`, `(Int n)`). |
| 195 | 3. **Estensione aperta dei tipi (`expand type`) e fusione automatica delle clausole `fn ::`**: Con `expand type` puoi aggiungere nuove varianti a un tipo somma o nuovi campi a un record. Ridichiarando `fn ::` per la stessa funzione, le nuove clausole vengono fuse automaticamente **prima** di qualsiasi clausola jolly `_` (senza bisogno di scrivere `expand fn`). |
| 196 | 4. **Spazi naturali `" "` invece di `_` in `fn ::`, funzioni native e nomi di file**: Proprio come in `op ::`, i nomi di funzione in `fn ::`, le chiamate a funzioni native (`assert equals`, `str len`, `read file`) e i file `.rg` (`test core language.rg`) possono essere scritti con spazi naturali: |
| 197 | |
| 198 | ```ridgun |
| 199 | type Something = String | Int | MyStruct Float Int |
| 200 | |
| 201 | fn :: descrivi qualcosa Something => String |
| 202 | descrivi qualcosa "admin" = "admin_speciale" |
| 203 | descrivi qualcosa 0 = "zero" |
| 204 | descrivi qualcosa (String s) = "testo:{s}" |
| 205 | descrivi qualcosa (Int n) = "intero:{n}" |
| 206 | descrivi qualcosa (MyStruct ratio cnt) = "struttura:{ratio}:{cnt}" |
| 207 | descrivi qualcosa _ = "altro" |
| 208 | |
| 209 | expand type Something = Bool |
| 210 | |
| 211 | fn :: descrivi qualcosa Something => String |
| 212 | descrivi qualcosa (Bool b) = "booleano:{b}" |
| 213 | ``` |
| 214 | |
| 215 | ### 5. Tipi generici personalizzati (`type Result T E`, `type Page T`) e namespace di modulo (`import ... as`, `mod.Tipo`) |
| 216 | Oltre alle funzioni polimorfiche, puoi dichiarare **i tuoi tipi somma e record generici** con parametri di tipo (`A`..`Z`), nonché organizzare tipi, costruttori, campi e funzioni con lo stesso nome in moduli diversi senza collisioni (`module ::`, `import db as d`, `http.Config`, `d.Config`). Ogni modulo ha **visibilità stretta per importazione su 3 livelli**: risolve i nomi senza prefisso cercando prima nelle **(1) proprie dichiarazioni** (inclusi più file che condividano `module :: nome`), poi nei **(2) suoi `import` diretti** e solo in ultimo luogo nei **(3) suoi `import` transitivi**, in modo che altri moduli fratelli o l'applicazione principale non contaminino mai una libreria importata: |
| 217 | |
| 218 | ```ridgun |
| 219 | type Risultato T E = Successo T | Fallimento E |
| 220 | type Pagina T = elementi: [T], totale: Int |
| 221 | |
| 222 | fn :: spacchetta successo (Risultato A String) A => A |
| 223 | spacchetta successo (Successo v) _ = v |
| 224 | spacchetta successo (Fallimento _) d = d |
| 225 | ``` |
| 226 | |
| 227 | Nel **Capitolo 07** metteremo tutti i core del tuo processore a lavorare contemporaneamente senza versare una goccia di sudore: **funzioni di ordine superiore (`map`, `||map`, `filter`, `||filter`, `fold`), chiusure lessicali (`(x => expr)`), grafi paralleli `||` con `->` e canali concorrenti**. |