Skip to content
Ridgun
English

Ridgun

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

06-tipus-algebraics-i-polimorfisme.md227 lines · 12.5 KBRaw
1# Capítol 06: Tipus de Dades Algebraics (ADTs), Polimorfisme i per què aquí no volem `null` ni `Any`
2
3El 1965, Tony Hoare va inventar la referència nul·la (`null` / `nil`). Quaranta anys després va demanar perdó públicament i la va anomenar *"el meu error de mil milions de dòlars"*.
4
5Per què? Perquè en els llenguatges amb `null`, una funció et promet a la seva signatura que et retornarà un `Usuari`... i quan menys t'ho esperes et retorna `null` i et tomba el servidor a les tres de la matinada.
6
7I el mateix passa amb l'herència d'objectes i el tipus `Any` (o `Object`): fiques un número i un text en una llista `[1, "dos"]`, el llenguatge s'arronsa d'espatlles i diu *"d'acord, doncs ara això és una `List[Any]`, bona sort endevinant què hi ha a dins quan la recorris"*.
8
9En Ridgun tallem aquests dos problemes de soca-rel amb **Tipus de Dades Algebraics (ADTs)** i **Polimorfisme Paramètric**. Sona a nom d'assignatura dura, però en cinc minuts veuràs que és la manera més natural del món de modelar dades.
10
11---
12
13## 6.1. Tipus Producte (Registres): Els teus `struct` de tota la vida, però amb superpoders
14
15Quan vols agrupar diverses dades que pertanyen a una mateixa entitat (per exemple, un servidor amb el seu nom, la seva IP i el seu port), defineixes un **`type`** amb els seus camps:
16
17```ridgun
18type Servidor = host: String, port: Int, tls_actiu: Bool
19```
20
21Només escrivint aquesta línia, Ridgun et regala **quatre coses** automàticament:
22
23### 1. El constructor `Servidor`
24```ridgun
25el meu node => Servidor
26el meu node = Servidor "api.exemple.com" 443 True
27```
28
29### 2. Una funció *getter* pura per a cada camp (`host`, `port`, `tls_actiu`)
30No has d'escriure `s.getHost()` ni res semblant; cada nom de camp és directament una funció que extreu aquella dada:
31
32```ridgun
33url connexio Servidor => String
34url connexio srv = "Connectar a " + (host srv) + " al port {port srv}"
35```
36
37### 3. Actualització funcional de registres (`with camp: valor`)
38Imagina un registre de configuració amb 12 o 15 camps (com les opcions d'una ordre `ls` o d'un servidor web) i vols canviar només un o dos camps sense tocar la resta. En lloc de reconstruir el registre passant els 15 camps a mà, fas servir **`with`** i dos punts **`:`** (exactament igual que en la definició del `type`):
39
40```ridgun
41activar mode dev Servidor => Servidor
42activar mode dev srv = srv with port: 8080, tls_actiu: False
43```
44
45El millor dels dos mons:
46- Al teu codi és **100% immutable i segur** (el compilador comprova que `port` i `tls_actiu` existeixin a `Servidor` i tinguin el tipus correcte).
47- Per sota, si `srv` no està compartit amb ningú més, l'executable natiu reutilitza la seva memòria *in-place* (`Arc::try_unwrap`) sense copiar la resta de camps.
48
49### 4. *Pattern matching* complet per si vols obrir el registre en canal
50```ridgun
51es port segur Servidor => Bool
52es port segur (Servidor _ 443 True) = True
53es port segur _ = False
54```
55
56---
57
58## 6.2. Tipus Suma (Variants amb `|`): Com jubilar `null`, les excepcions i l'herència
59
60Ara ve el millor.
61Un registre normal té `host` **I** `port` **I** `tls_actiu`.
62Però al món real moltes vegades necessites dir: *"Un mètode de pagament és **O BÉ** una targeta amb el seu número, **O BÉ** una transferència amb el seu IBAN, **O BÉ** efectiu (que no porta cap dada extra)"*.
63
64En orientació a objectes hauries de crear una interfície `MetodePagament`, tres subclasses en tres fitxers diferents, constructors, `instanceof`... En Ridgun és **una línia**:
65
66```ridgun
67type MetodePagament = Targeta String Int | Transferencia String | Efectiu
68```
69
70I mira què passa quan processes un `MetodePagament` en una funció:
71
72```ridgun
73descriure pagament MetodePagament => String
74descriure pagament (Targeta num _) = "Pagament amb targeta acabada en {num}"
75descriure pagament (Transferencia iban) = "Transferència des del compte {iban}"
76descriure pagament Efectiu = "Pagament en metàl·lic a finestreta"
77```
78
79Cada variant (`Targeta`, `Transferencia`, `Efectiu`) porta enganxades únicament les dades que tenen sentit per a ella. És **impossible** construir un pagament en `Efectiu` que tingui un `iban` penjant per error.
80
81---
82
83## 6.3. "I com fico `[1, "dos"]` en una llista sense fer servir `Any`?"
84
85Tornem a la pregunta del milió. Imagina que estàs llegint un fitxer CSV o JSON on una cel·la pot ser un número enter, un text o estar buida.
86
87Si tinguessis un tipus `Any`, hi ficaries `[1, "dos"]` i perdries tota l'ajuda del compilador: a partir d'aquell moment qualsevol operació sobre la llista seria caminar amb els ulls embenats.
88
89En Ridgun (igual que en Haskell o Rust), crees en dos segons un tipus suma que explica exactament quines coses poden viure dins d'aquesta llista:
90
91```ridgun
92type ValorCella = Enter Int | Text String | Buit
93
94formatar cella ValorCella => String
95formatar cella (Enter n) = "Número: {n}"
96formatar cella (Text s) = "Text: {s}"
97formatar cella Buit = "(buit)"
98
99la meva fila => [ValorCella]
100la meva fila = [Enter 1, Text "dos", Buit]
101```
102
103Hi veus la diferència?
104- Amb `Any`, el compilador no sap si a la llista hi ha enters, cadenes o una cafetera connectada per USB.
105- Amb `[ValorCella]`, pots barrejar números i textos a la mateixa llista, **però el compilador sap al 100% que només existeixen aquestes tres possibilitats** i t'obliga a desempaquetar cada cas (`Enter n`, `Text s`, `Buit`) abans de fer-lo servir. Seguretat total sense perdre ni un brí de flexibilitat.
106
107---
108
109## 6.4. `Option` i `Result`: Quan alguna cosa pot no ser-hi o pot fallar
110
111Com que en Ridgun no existeix `null` ni existeixen excepcions que saltin pels aires (`throw new Exception()`), el llenguatge porta integrats dos tipus suma universals:
112
1131. **`Option`**: o tens **`Some valor`**, o tens **`None`** (res).
1142. **`Result`**: o l'operació ha sortit bé (**`Ok valor`**), o ha fallat per un motiu concret (**`Err error`**).
115
116```ridgun
117cercar divisio Int Int => Result
118cercar divisio _ 0 = Err "Ei! No es pot dividir per zero"
119cercar divisio a b = Ok (a / b)
120
121imprimir res Result => ()
122imprimir res (Ok n) = echo "El resultat és {n}"
123imprimir res (Err msg) = echo "Fallada controlada: {msg}"
124```
125
126Ningú pot ignorar un error per descuit perquè el mateix tipus `Result` t'obliga a mirar si a dins hi ha un `Ok` o un `Err`.
127
128---
129
130## 6.5. Polimorfisme Paramètric: Funcions genèriques amb `A`, `B`, `T`...
131
132Suposa que escrius una funció que inverteix un parell `(x, y)` o que treu el primer element d'una llista. De debò importa si la llista és d'`Int`, de `String` o de `Servidor`? En absolut! L'algorisme és idèntic.
133
134En Ridgun, **qualsevol lletra majúscula individual (`A` a `Z`)** en una signatura de tipus funciona automàticament com un **tipus comodí polimòrfic (variable de tipus)**:
135
136```ridgun
137cap segur [A] A => A
138cap segur [] per_defecte = per_defecte
139cap segur (x:_) _ = x
140
141girar parell (A, B) => (B, A)
142girar parell (x, y) = (y, x)
143```
144
145Fixa't en la màgia de `cap segur [A] A => A`:
146- No és un `Any` descontrolat: diu que **el tipus de la llista `[A]`, el tipus del valor `per_defecte` (`A`) i el tipus retornat (`A`) han de ser el mateix**.
147- Si crides `cap segur [10, 20] 0`, el compilador lliga caps (`A = Int`) i et retorna un `Int`.
148- Si crides `cap segur ["a", "b"] "cap"`, lliga caps (`A = String`) i et retorna un `String`.
149- Però si intentes fer trampa i crides `cap segur [10, 20] "cap"`, zas! El compilador et frena en sec perquè el primer argument diu que `A` és `Int` i el segon diu que `A` és `String`.
150
151---
152
153## 6.6. Crea els teus propis operadors (`op ::`): Des de vectors fins a frases a mida
154
155Per què només els números natius haurien de tenir dret a fer servir `+` o `*`? I per què conformar-se amb els operadors de sempre quan pots inventar la sintaxi perfecta per al teu problema?
156
157Amb **`op ::`** (o directament sense prefix si fas servir patrons d'operador) pots fer dues filigranes:
158
159### 1. Ensenyar a `+`, `-`, `*`, `/`, `%` a operar amb els teus propis tipus
160```ridgun
161type Complex = real: Int, imag: Int
162
163op :: Complex + Complex => Complex
164 a + b = Complex ((real a) + (real b)) ((imag a) + (imag b))
165```
166
167### 2. Crear operadors *Mixfix* (que es llegeixen com frases naturals, inclosos `type` i `with`)
168En una signatura d'operador, **les paraules amb majúscula inicial (`Int`, `String`, `A`..`Z`) o entre claudàtors/parèntesis (`[Int]`, `(A, B)`) són els buits per als arguments**, i **les paraules en minúscula són part de la frase de l'operador** (fins i tot paraules tan habituals com `type` o `with`, que en Ridgun són contextuals i mai no et bloquegen):
169
170```ridgun
171op :: si Bool aleshores A sino A => A
172 si True aleshores v sino _ = v
173 si False aleshores _ sino 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 missatge = si (10 > 5) aleshores "El deu guanya!" sino "Impossible"
181 echo missatge
182 echo (format "panell" type "vorejat" with 80)
183```
184
185Acabes de fabricar-te el teu propi operador ternari polimòrfic i un DSL de formatat en **3 línies** de Ridgun.
186
187---
188
189## 6.7. Constructors compartits, variants de tipus directe, `expand type` i espais naturals a `fn ::`
190
191Ridgun porta els tipus algebraics i les funcions un pas més enllà en expressivitat i extensibilitat:
192
1931. **Constructors compartits entre diferents `type`**: Diversos tipus suma poden reutilitzar el mateix nom de constructor (`Error`, `Var`, `Lit`) amb igual o diferent aritat; el verificador de tipus els distingeix per la columna de la signatura.
1942. **Variants amb nom de tipus directe (`type Something = String | Int | MyStruct Float Int`)**: Quan una variant és un tipus primitiu (`String`, `Int`, `Float`/`Real`, `Bool`, `Char`), no cal repetir `Int Int` ni `String String`. Pots passar valors directes (`"hola"`, `42`) o constructors explícits (`String "hola"`, `Int 42`), i fer pattern matching tant amb literals (`"admin"`, `0`) com amb patrons tipats (`(String s)`, `(Int n)`).
1953. **Extensió oberta de tipus (`expand type`) i fusió automàtica de clàusules `fn ::`**: Amb `expand type` pots afegir noves variants a un tipus suma o nous camps a un registre. En tornar a declarar `fn ::` per a la mateixa funció, les noves clàusules es fusionen automàticament **abans** de qualsevol clàusula comodí `_` (sense necessitat d'escriure `expand fn`).
1964. **Espais naturals `" "` en lloc de `_` a `fn ::`, funcions natives i noms de fitxer**: Igual que a `op ::`, els noms de funció a `fn ::`, les crides a funcions natives (`assert equals`, `str len`, `read file`) i els fitxers `.rg` (`test core language.rg`) es poden escriure amb espais naturals:
197
198```ridgun
199type Something = String | Int | MyStruct Float Int
200
201fn :: descriure algo Something => String
202 descriure algo "admin" = "admin_especial"
203 descriure algo 0 = "zero"
204 descriure algo (String s) = "text:{s}"
205 descriure algo (Int n) = "enter:{n}"
206 descriure algo (MyStruct ratio cnt) = "estructura:{ratio}:{cnt}"
207 descriure algo _ = "altre"
208
209expand type Something = Bool
210
211fn :: descriure algo Something => String
212 descriure algo (Bool b) = "boolea:{b}"
213```
214
215### 5. Tipus genèrics propis (`type Result T E`, `type Page T`) i espais de noms de mòdul (`import ... as`, `mod.Tipus`)
216A més de funcions polimòrfiques, pots declarar **els teus propis tipus suma i registres genèrics** amb paràmetres de tipus (`A`..`Z`), així com organitzar tipus, constructors, camps i funcions amb el mateix nom en diferents mòduls sense col·lisió (`module ::`, `import db as d`, `http.Config`, `d.Config`). Cada mòdul té **visibilitat estricta per importació en 3 nivells**: resol els noms sense prefix cercant primer en **(1) les seves pròpies declaracions** (incloent múltiples fitxers amb el mateix `module :: nom`), després en **(2) els seus `import` directes** i només en últim lloc en **(3) els seus `import` transitius**, de manera que altres mòduls germans o l'aplicació principal mai contaminen una llibreria importada:
217
218```ridgun
219type Resultat T E = Exit T | Fallada E
220type Pagina T = elements: [T], total: Int
221
222fn :: desempaquetar exit (Resultat A String) A => A
223 desempaquetar exit (Exit v) _ = v
224 desempaquetar exit (Fallada _) d = d
225```
226
227Al **Capítol 07** posarem tots els nuclis del teu processador a treballar alhora sense vessar ni una gota de suor: **funcions d'ordre superior (`map`, `||map`, `filter`, `||filter`, `fold`), clausures lèxiques (`(x => expr)`), grafs paral·lels `||` amb `->` i canals concurrents**.