Repository navigation
Libro 14 Tipi e dispatch
Con questo capitolo entriamo in Triad-Full, il linguaggio completo definito da SPEC.md. Una
premessa vale per tutta la parte terza: i programmi qui descritti non girano ancora sul
runtime attuale, che esegue solo Triad-Core. Li presentiamo perché sono lo standard, la meta
verso cui lavora l'implementazione self-hosted, e perché capirli aiuta a scrivere oggi codice
Core che domani si integrerà senza attriti. Dove esiste un output di riferimento (i golden della
suite Full), lo indichiamo.
In Core i tipi esistono ma restano impliciti: si scoprono durante l'esecuzione. Full permette di
dichiararli. Una dichiarazione di tipo è insieme documentazione e controllo: dice al lettore
che cosa ci si aspetta e chiede al linguaggio di verificarlo. Full aggiunge anche nuovi tipi:
Float (numeri decimali, con Number come supertipo comune a Int e Float), i tipi definiti
dall'utente con struct, e Any, il tipo che comprende tutti gli altri.
x::Int = 5
x = 6 # va bene
x = 5.5 # Err(:type): x è stata dichiarata Int
y::Float = 1.0
y = 2 # diventa 2.0: un Int si converte in FloatIl vincolo nasce con la dichiarazione e vive nell'ambiente in cui il nome è stato creato. Un nome mai dichiarato con un tipo resta libero, come in Core.
Una struct definisce un nuovo tipo con campi nominati e, facoltativamente, tipizzati:
struct Punto
x::Float
y::Float
end
p = Punto(3, 4) # costruttore posizionale; 3 e 4 diventano 3.0 e 4.0
p.x # 3.0
typeof(p) == Punto # true
p isa Punto # trueLe struct sono immutabili: una volta costruito, un Punto non cambia. Leggere un campo che
non esiste produce Err(:field); costruirne uno con un tipo incompatibile produce Err(:type).
Nel capitolo 10 abbiamo rappresentato la stessa idea in Core con una lista etichettata
[:Punto, x, y]: è precisamente il modo in cui S1 implementerà le struct dentro l'interprete.
I parametri e il valore di ritorno di una funzione possono essere annotati:
function distanza(a::Punto, b::Punto)::Float
sqrt((a.x - b.x)^2 + (a.y - b.y)^2)
endIl tipo di ritorno ::Float viene controllato all'uscita (con la stessa conversione da Int a
Float degli assegnamenti). Anche le lambda possono avere parametri tipati:
function (x::Int) x end rifiuta un Float con Err(:type).
Qui sta l'idea più caratteristica che Triad eredita da Julia. In molti linguaggi a oggetti, la scelta del codice da eseguire dipende dal tipo di un oggetto, il destinatario del metodo. In Triad una funzione è un insieme di metodi, ognuno con la sua firma, e la scelta dipende dai tipi di tutti gli argomenti.
area(n::Number) = :numero
area(n::Int) = :intero
area(p::Punto) = :punto
area(1) # :intero — Int è più specifico di Number
area(1.5) # :numero
area(Punto(0, 0)) # :puntoLa regola è che vince il metodo più specifico tra quelli applicabili. Se nessun metodo è
applicabile si ottiene Err(:method); se due metodi sono ugualmente specifici e nessuno prevale,
Err(:ambiguous): il linguaggio rifiuta di scegliere a caso.
Perché dispatch multiplo? Perché molte operazioni non "appartengono" a un solo argomento. Sommare un intero e un decimale, far collidere due forme geometriche, convertire un valore da un tipo a un altro: sono relazioni tra tipi, e il dispatch multiplo permette di definirle caso per caso senza gerarchie forzate.
In Triad a + b è zucchero sintattico per la chiamata +(a, b). Ne segue che un operatore si
può estendere a un nuovo tipo semplicemente aggiungendo un metodo:
function +(a::Punto, b::Punto)
Punto(a.x + b.x, a.y + b.y)
end
r = Punto(3, 4) + Punto(1, 2) # Punto(4.0, 6.0)E poiché gli operatori sono funzioni, si possono passare come valori: reduce(+, xs) somma una
lista, map(sq, xs) applica sq a ogni elemento.
Il caso di conformità Full dispatch (si veda il Catalogo conformance Full)
mette insieme struct, operatori estesi, dispatch ed errori come dati. Leggerlo con l'output di
riferimento accanto è un ottimo esercizio di comprensione.
- Full aggiunge annotazioni di tipo controllate,
Float,Number,Anye le struct. - Le struct sono immutabili, con costruttore posizionale e campi tipizzati.
- Una funzione è un insieme di metodi; vince il più specifico tra gli applicabili; l'ambiguità è un errore.
- Gli operatori sono funzioni estendibili e passabili come valori.
- Definisci una struct
Frazionecon numeratore e denominatore interi e un metodo*per moltiplicarle. - Scrivi tre metodi di
descriviperInt,StringeAnye prevedi quale viene scelto perdescrivi(:a). - Riscrivi in Core (liste etichettate e
typeof) l'esempioareadel paragrafo 14.4.
← Capitolo 13 — Progetto: un piccolo interprete · Sommario · Capitolo 15 — Programmazione logica →
Parte prima
Parte seconda 4. Le parole del linguaggio 5. Valori e tipi 6. Espressioni, operatori e variabili 7. Decidere e ripetere 8. Funzioni e ricorsione 9. Lambda, chiusure e visibilità dei nomi 10. Liste e dati strutturati 11. Stringhe, simboli e byte 12. Quando le cose vanno storte 13. Progetto: un piccolo interprete
Parte terza 14. Tipi, struct e multiple dispatch 15. Programmazione logica 16. Codice come dato e macro 17. Garanzie, costrutti Full e interfaccia
Parte quarta 18. Anatomia del progetto e contratto di conformità 19. L'interprete di riferimento 20. Il seed in codice macchina 21. Verso il self-hosting 22. Gli strumenti di sviluppo
Appendici
- Riferimento dei builtin Core
- Catalogo conformance Core
- Catalogo conformance Full
- Sorgenti degli esempi
- Sorgenti self-hosted
- Documenti di riferimento
- Indice analitico
Manuale di consultazione