Repository navigation
Libro 10 Liste
Triad-Core non ha array tipizzati, dizionari, record o oggetti. Ha una sola struttura dati composta, la lista, ed è sorprendente quanto si possa costruire con essa. Questo capitolo mostra le operazioni sulle liste e poi tre modi di usarle per rappresentare dati strutturati: record, tabelle associative e pile.
Una lista si scrive tra parentesi quadre: [1, 2, 3], [] (vuota), [1, "due", :tre, [4]]
(mista e annidata). Si legge un elemento con l'indice, che parte da 1:
xs = [10, 20, 30]
println(xs[1], " ", xs[3], " ", length(xs))Output:
10 30 3
Leggere fuori dai limiti (xs[0] o xs[4] qui) è un errore di esecuzione. Gli indici da 1
sono la convenzione di Julia e della matematica; chi viene da C o Python deve abituarsi a
scrivere i cicli come i = 1 … while i <= length(xs).
Le liste sono mutabili. Si può cambiare un elemento con un assegnamento indicizzato e
aggiungere elementi in coda con push!:
xs = [1, 2, 3]
xs[2] = 200
xs[1] += 100
push!(xs, 4)
println(xs)Output:
[101, 200, 3, 4]
push! modifica la lista che riceve e la restituisce; il punto esclamativo nel nome è proprio
il segnale di questa modifica.
Quando si assegna una lista a un'altra variabile, o la si passa a una funzione, non se ne fa una copia: entrambi i nomi indicano la stessa lista.
a = [1, 2]
b = a
push!(b, 3)
println(a)
function svuota_primo!(xs)
xs[1] = 0
end
svuota_primo!(a)
println(b)Output:
[1, 2, 3]
[0, 2, 3]
È il comportamento più utile (copiare liste grandi a ogni chiamata sarebbe costoso) e anche quello che permette a una funzione di "restituire" più risultati modificando una lista ricevuta. Ma è anche fonte di errori quando non lo si tiene presente. Per avere una copia indipendente la si costruisce elemento per elemento.
Liste e funzioni nominate. Il capitolo 9 ha mostrato che una funzione nominata non può riassegnare una variabile globale. Può però modificare il contenuto di una lista globale, perché non sta riassegnando il nome, sta cambiando l'oggetto a cui il nome si riferisce. Una lista di un elemento,
stato = [0], è il modo più semplice di avere un contatore condiviso.
Lo schema di iterazione è sempre lo stesso: un indice, un while, un incremento. Con esso si
scrivono le operazioni che Triad-Full offre già pronte. Ecco filtra e riduci:
function filtra(p, xs)
out = []
i = 1
while i <= length(xs)
if p(xs[i])
push!(out, xs[i])
end
i += 1
end
out
end
function riduci(f, acc, xs)
i = 1
while i <= length(xs)
acc = f(acc, xs[i])
i += 1
end
acc
end
numeri = [3, 8, 1, 9, 4]
println(filtra(x -> x > 3, numeri))
println(riduci((a, b) -> a + b, 0, numeri))
println(riduci((a, b) -> a > b ? a : b, numeri[1], numeri))Output:
[8, 9, 4]
25
9
riduci merita un commento: combina gli elementi uno alla volta con un accumulatore. Con la
somma come funzione calcola il totale; con "il maggiore dei due" calcola il massimo. Somma,
massimo, conteggio, concatenazione sono tutti casi particolari della stessa idea.
Senza struct, un dato con più campi si rappresenta come una lista il cui primo elemento è un
simbolo che ne dichiara la natura. Le funzioni di accesso danno un nome ai campi e nascondono la
posizione:
persona(nome, eta) = [:Persona, nome, eta]
nome_di(p) = p[2]
eta_di(p) = p[3]
is_persona(v) = typeof(v) == :List && length(v) == 3 && v[1] == :Persona
anna = persona("Anna", 34)
println(nome_di(anna), " ha ", eta_di(anna), " anni; record? ", is_persona(anna))Output:
Anna ha 34 anni; record? true
È esattamente la tecnica che SPEC-CORE.md prevede per S1: le struct di Triad-Full saranno
rappresentate, dentro l'interprete scritto in Core, come liste etichettate.
Una tabella che associa chiavi a valori si può rappresentare come una lista piatta di coppie
alternate, [chiave1, valore1, chiave2, valore2, …]. È la struttura usata per l'ambiente delle
variabili negli interpreti di selfhost/:
function tab_get(t, k)
i = 1
while i < length(t)
if t[i] == k
return t[i + 1]
end
i += 2
end
nothing
end
function tab_set!(t, k, v)
i = 1
while i < length(t)
if t[i] == k
t[i + 1] = v
return t
end
i += 2
end
push!(t, k)
push!(t, v)
end
voti = []
tab_set!(voti, :anna, 28)
tab_set!(voti, :luca, 30)
tab_set!(voti, :anna, 30)
println(voti, " ", tab_get(voti, :anna), " ", tab_get(voti, :zoe))Output:
[:anna, 30, :luca, 30] 30 nothing
La ricerca è lineare: per tabelle piccole va benissimo, per tabelle grandi servirebbe una struttura più furba (un albero, una tabella hash), che si può comunque costruire con le liste.
Una pila (stack) è una lista a cui si aggiunge e si toglie dalla stessa estremità. Core ha
push! ma non pop!; il "togliere" si ottiene tenendo a parte l'altezza della pila:
pila = [[], 0]
function spingi!(p, v)
if p[2] < length(p[1])
p[1][p[2] + 1] = v
else
push!(p[1], v)
end
p[2] += 1
end
function estrai!(p)
v = p[1][p[2]]
p[2] -= 1
v
end
spingi!(pila, :a)
spingi!(pila, :b)
spingi!(pila, :c)
println(estrai!(pila), " ", estrai!(pila))
spingi!(pila, :d)
println(estrai!(pila), " ", estrai!(pila))Output:
c b
d a
Le pile sono lo strumento con cui gli interpreti valutano le espressioni e gestiscono le chiamate: le ritroveremo parlando del seed.
- Le liste si indicizzano da 1, sono mutabili e vengono condivise per riferimento.
-
push!aggiunge in coda e restituisce la lista;lengthne dà la lunghezza. - Con un indice e un
whilesi scrivonomap,filtra,riducie ogni altra operazione. - Record, tabelle e pile si rappresentano con liste e funzioni di accesso.
- Scrivi
inverti(xs)che restituisca una nuova lista con gli elementi in ordine inverso. - Scrivi
copia(xs)che restituisca una copia indipendente (anche delle liste annidate). - Ordina una lista di interi con l'ordinamento per inserzione, modificandola sul posto.
← Capitolo 9 — Lambda, chiusure e visibilità dei nomi · Sommario · Capitolo 11 — Stringhe, simboli e byte →
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