Skip to content
Ridgun
English

Ridgun

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

06-tipi-algebrici-e-polimorfismo.md227 lines · 12.7 KBRaw
1# Capitolo 06: Tipi di Dati Algebrici (ADT), Polimorfismo e perché qui non vogliamo `null` né `Any`
2
3Nel 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
5Perché? 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
7E 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
9In 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
15Quando 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
18type Server = host: String, porta: Int, tls_attivo: Bool
19```
20
21Scrivendo solo quella riga, Ridgun ti regala **quattro cose** automaticamente:
22
23### 1. Il costruttore `Server`
24```ridgun
25fn :: 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`)
30Non devi scrivere `s.getHost()` né nulla di simile; ogni nome di campo è direttamente una funzione che estrae quel dato:
31
32```ridgun
33fn :: 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`)
38Immagina 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
41fn :: attiva modalita dev Server => Server
42 attiva modalita dev srv = srv with porta: 8080, tls_attivo: False
43```
44
45Il 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
51fn :: 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
60Ora viene il bello.
61Un record normale ha `host` **E** `porta` **E** `tls_attivo`.
62Ma 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
64Nella programmazione orientata agli oggetti dovresti creare un'interfaccia `MetodoPagamento`, tre sottoclassi in tre file diversi, costruttori, `instanceof`... In Ridgun è **una riga**:
65
66```ridgun
67type MetodoPagamento = Carta String Int | Bonifico String | Contanti
68```
69
70E guarda cosa succede quando elabori un `MetodoPagamento` in una funzione:
71
72```ridgun
73fn :: 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
79Ogni 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
85Torniamo 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
87Se 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
89In 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
92type ValoreCella = Intero Int | Testo String | Vuoto
93
94fn :: formatta cella ValoreCella => String
95 formatta cella (Intero n) = "Numero: {n}"
96 formatta cella (Testo s) = "Testo: {s}"
97 formatta cella Vuoto = "(vuoto)"
98
99fn :: mia riga => [ValoreCella]
100 mia riga = [Intero 1, Testo "due", Vuoto]
101```
102
103Vedi 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
111Poiché in Ridgun non esiste `null` né esistono eccezioni che saltano per aria (`throw new Exception()`), il linguaggio porta integrati due tipi somma universali:
112
1131. **`Option`**: o hai **`Some valore`**, o hai **`None`** (niente).
1142. **`Result`**: o l'operazione è andata bene (**`Ok valore`**), o è fallita per un motivo preciso (**`Err errore`**).
115
116```ridgun
117fn :: 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
121fn :: stampa ris Result => ()
122 stampa ris (Ok n) = echo "Il risultato è {n}"
123 stampa ris (Err msg) = echo "Errore controllato: {msg}"
124```
125
126Nessuno 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
132Supponi 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
134In 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
137fn :: testa sicura [A] A => A
138 testa sicura [] predefinito = predefinito
139 testa sicura (x:_) _ = x
140
141fn :: gira coppia (A B) => B A
142 gira coppia (x, y) = (y, x)
143```
144
145Nota 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
155Perché 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
157Con **`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
161type Complesso = reale: Int, immag: Int
162
163op :: 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`)
168In 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
171op :: se Bool allora A altrimenti A => A
172 se True allora v altrimenti _ = v
173 se False allora _ altrimenti f = f
174
175op :: format String type String with Int => String
176 format label type kind with w = "{label} ({kind}, width={w})"
177
178main => ()
179main =
180 messaggio = se (10 > 5) allora "Vince il dieci!" altrimenti "Impossibile"
181 echo messaggio
182 echo (format "panel" type "bordered" with 80)
183```
184
185Ti 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
191Ridgun porta i tipi algebrici e le funzioni un passo oltre in espressività ed estensibilità:
192
1931. **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.
1942. **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)`).
1953. **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`).
1964. **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
199type Something = String | Int | MyStruct Float Int
200
201fn :: 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
209expand type Something = Bool
210
211fn :: 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`)
216Oltre 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
219type Risultato T E = Successo T | Fallimento E
220type Pagina T = elementi: [T], totale: Int
221
222fn :: spacchetta successo (Risultato A String) A => A
223 spacchetta successo (Successo v) _ = v
224 spacchetta successo (Fallimento _) d = d
225```
226
227Nel **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**.