Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítol 06: Tipus de Dades Algebraics (ADTs), Polimorfisme i per què aquí no volem `null` ni `Any` |
| 2 | |
| 3 | El 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 | |
| 5 | Per 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 | |
| 7 | I 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 | |
| 9 | En 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 | |
| 15 | Quan 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 |
| 18 | type Servidor = host: String, port: Int, tls_actiu: Bool |
| 19 | ``` |
| 20 | |
| 21 | Només escrivint aquesta línia, Ridgun et regala **quatre coses** automàticament: |
| 22 | |
| 23 | ### 1. El constructor `Servidor` |
| 24 | ```ridgun |
| 25 | el meu node => Servidor |
| 26 | el meu node = Servidor "api.exemple.com" 443 True |
| 27 | ``` |
| 28 | |
| 29 | ### 2. Una funció *getter* pura per a cada camp (`host`, `port`, `tls_actiu`) |
| 30 | No has d'escriure `s.getHost()` ni res semblant; cada nom de camp és directament una funció que extreu aquella dada: |
| 31 | |
| 32 | ```ridgun |
| 33 | url connexio Servidor => String |
| 34 | url connexio srv = "Connectar a " + (host srv) + " al port {port srv}" |
| 35 | ``` |
| 36 | |
| 37 | ### 3. Actualització funcional de registres (`with camp: valor`) |
| 38 | Imagina 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 |
| 41 | activar mode dev Servidor => Servidor |
| 42 | activar mode dev srv = srv with port: 8080, tls_actiu: False |
| 43 | ``` |
| 44 | |
| 45 | El 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 |
| 51 | es port segur Servidor => Bool |
| 52 | es port segur (Servidor _ 443 True) = True |
| 53 | es port segur _ = False |
| 54 | ``` |
| 55 | |
| 56 | --- |
| 57 | |
| 58 | ## 6.2. Tipus Suma (Variants amb `|`): Com jubilar `null`, les excepcions i l'herència |
| 59 | |
| 60 | Ara ve el millor. |
| 61 | Un registre normal té `host` **I** `port` **I** `tls_actiu`. |
| 62 | Però 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 | |
| 64 | En 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 |
| 67 | type MetodePagament = Targeta String Int | Transferencia String | Efectiu |
| 68 | ``` |
| 69 | |
| 70 | I mira què passa quan processes un `MetodePagament` en una funció: |
| 71 | |
| 72 | ```ridgun |
| 73 | descriure pagament MetodePagament => String |
| 74 | descriure pagament (Targeta num _) = "Pagament amb targeta acabada en {num}" |
| 75 | descriure pagament (Transferencia iban) = "Transferència des del compte {iban}" |
| 76 | descriure pagament Efectiu = "Pagament en metàl·lic a finestreta" |
| 77 | ``` |
| 78 | |
| 79 | Cada 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 | |
| 85 | Tornem 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 | |
| 87 | Si 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 | |
| 89 | En 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 |
| 92 | type ValorCella = Enter Int | Text String | Buit |
| 93 | |
| 94 | formatar cella ValorCella => String |
| 95 | formatar cella (Enter n) = "Número: {n}" |
| 96 | formatar cella (Text s) = "Text: {s}" |
| 97 | formatar cella Buit = "(buit)" |
| 98 | |
| 99 | la meva fila => [ValorCella] |
| 100 | la meva fila = [Enter 1, Text "dos", Buit] |
| 101 | ``` |
| 102 | |
| 103 | Hi 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 | |
| 111 | Com 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 | |
| 113 | 1. **`Option`**: o tens **`Some valor`**, o tens **`None`** (res). |
| 114 | 2. **`Result`**: o l'operació ha sortit bé (**`Ok valor`**), o ha fallat per un motiu concret (**`Err error`**). |
| 115 | |
| 116 | ```ridgun |
| 117 | cercar divisio Int Int => Result |
| 118 | cercar divisio _ 0 = Err "Ei! No es pot dividir per zero" |
| 119 | cercar divisio a b = Ok (a / b) |
| 120 | |
| 121 | imprimir res Result => () |
| 122 | imprimir res (Ok n) = echo "El resultat és {n}" |
| 123 | imprimir res (Err msg) = echo "Fallada controlada: {msg}" |
| 124 | ``` |
| 125 | |
| 126 | Ningú 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 | |
| 132 | Suposa 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 | |
| 134 | En 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 |
| 137 | cap segur [A] A => A |
| 138 | cap segur [] per_defecte = per_defecte |
| 139 | cap segur (x:_) _ = x |
| 140 | |
| 141 | girar parell (A, B) => (B, A) |
| 142 | girar parell (x, y) = (y, x) |
| 143 | ``` |
| 144 | |
| 145 | Fixa'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 | |
| 155 | Per 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 | |
| 157 | Amb **`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 |
| 161 | type Complex = real: Int, imag: Int |
| 162 | |
| 163 | op :: 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`) |
| 168 | En 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 |
| 171 | op :: si Bool aleshores A sino A => A |
| 172 | si True aleshores v sino _ = v |
| 173 | si False aleshores _ sino 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 | missatge = si (10 > 5) aleshores "El deu guanya!" sino "Impossible" |
| 181 | echo missatge |
| 182 | echo (format "panell" type "vorejat" with 80) |
| 183 | ``` |
| 184 | |
| 185 | Acabes 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 | |
| 191 | Ridgun porta els tipus algebraics i les funcions un pas més enllà en expressivitat i extensibilitat: |
| 192 | |
| 193 | 1. **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. |
| 194 | 2. **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)`). |
| 195 | 3. **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`). |
| 196 | 4. **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 |
| 199 | type Something = String | Int | MyStruct Float Int |
| 200 | |
| 201 | fn :: 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 | |
| 209 | expand type Something = Bool |
| 210 | |
| 211 | fn :: 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`) |
| 216 | A 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 |
| 219 | type Resultat T E = Exit T | Fallada E |
| 220 | type Pagina T = elements: [T], total: Int |
| 221 | |
| 222 | fn :: desempaquetar exit (Resultat A String) A => A |
| 223 | desempaquetar exit (Exit v) _ = v |
| 224 | desempaquetar exit (Fallada _) d = d |
| 225 | ``` |
| 226 | |
| 227 | Al **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**. |