Repository navigation
Libro 09 Chiusure e scope
Nel capitolo 5 abbiamo detto che le funzioni sono valori. Questo capitolo prende sul serio quell'affermazione: vedremo funzioni create al volo, passate come argomenti, restituite da altre funzioni, e il meccanismo, le chiusure, che permette loro di ricordare l'ambiente in cui sono nate. Per capirlo fino in fondo bisogna prima chiarire una questione che finora abbiamo rimandato: dove è visibile un nome, e che cosa succede quando lo si assegna.
Una lambda è una funzione anonima scritta in forma compatta con la freccia ->:
doppio = x -> x * 2
somma = (a, b) -> a + b
println(doppio(21), " ", somma(2, 3))Output:
42 5
A sinistra della freccia ci sono i parametri (tra parentesi se sono più di uno), a destra il
corpo, che è un'unica espressione. Se servono più istruzioni si usa begin … end, oppure la
forma function (…) … end. Le lambda senza parametri, () -> …, non sono ammesse dalla
grammatica di Core: al loro posto si usa function () … end.
Poiché una funzione è un valore, la si può passare a un'altra funzione. Questo permette di separare che cosa fare da come iterare:
function applica_a_tutti(f, xs)
out = []
i = 1
while i <= length(xs)
push!(out, f(xs[i]))
i += 1
end
out
end
println(applica_a_tutti(x -> x * x, [1, 2, 3, 4]))
println(applica_a_tutti(x -> x > 2, [1, 2, 3, 4]))Output:
[1, 4, 9, 16]
[false, false, true, true]
applica_a_tutti non sa nulla di quadrati o confronti: sa solo percorrere una lista. Il lavoro
vero lo fa la funzione ricevuta. È l'idea dietro map, che Triad-Full fornisce già pronta e che
in Core si scrive così, una volta per tutte.
Il passo successivo è una funzione che restituisce una funzione:
function sommatore(n)
x -> x + n
end
piu10 = sommatore(10)
piu100 = sommatore(100)
println(piu10(5), " ", piu100(5), " ", piu10(1))Output:
15 105 11
Qui accade qualcosa di notevole. Quando sommatore(10) termina, il suo parametro n dovrebbe
sparire, come tutte le variabili locali. Eppure piu10, chiamata molto dopo, sa ancora che n
vale 10. La lambda ha catturato l'ambiente in cui è stata creata: insieme al codice, porta
con sé i nomi che quel codice usa. Una funzione con il suo ambiente catturato si chiama
chiusura. piu10 e piu100 condividono il codice ma hanno ambienti diversi.
Lo standard Core stabilisce che le variabili catturate sono condivise: se la chiusura le modifica, la modifica persiste tra una chiamata e l'altra. Questo permette di costruire funzioni con memoria:
function crea_contatore()
conteggio = 0
passo -> begin
conteggio += passo
conteggio
end
end
c = crea_contatore()
c(1)
c(1)
println(c(10))
d = crea_contatore()
println(d(1))Output:
12
1
Ogni chiamata a crea_contatore crea una nuova variabile conteggio e una nuova chiusura che
la custodisce; c e d sono contatori indipendenti. È un modo elegante di avere stato privato
senza oggetti né variabili globali.
Attenzione al seed. Il seed Windows, oggi, cattura le variabili per valore e solo l'ultimo legame dell'ambiente: un contatore come questo può non funzionare lì. L'interprete di riferimento segue lo standard. Il capitolo 12 riassume tutte le differenze.
Arriviamo alla questione rimandata. Triad usa uno scope lessicale: la visibilità di un nome dipende da dove è scritto nel testo del programma, non da chi chiama chi durante l'esecuzione. Le regole di Core sono cinque.
-
function, lambda,letewhileintroducono un nuovo ambiente. Un nome è visibile nell'ambiente in cui è legato e in tutti quelli annidati al suo interno. - Dentro una funzione nominata, l'assegnamento crea una variabile locale. Anche se esiste una variabile globale con lo stesso nome, la funzione non la modifica.
- Dentro una lambda, assegnare un nome già esistente all'esterno aggiorna quel nome. È la regola che fa funzionare il contatore del paragrafo precedente.
- Al livello più esterno (il top level), anche i cicli aggiornano le variabili globali.
-
let x = …crea sempre unaxnuova; assegnarexdentro illetmodifica quella nuova.
La regola 2 sorprende chi viene da linguaggi in cui le funzioni possono modificare i globali:
totale = 0
function aggiungi(n)
totale += n
totale
end
println(aggiungi(5))
println(totale)Output:
5
0
Dentro aggiungi, totale += n legge il totale globale (0) e crea un totale locale
che vale 5; il globale resta 0. È una scelta deliberata: una funzione nominata non può alterare
di nascosto lo stato del programma, e questo rende il codice più facile da ragionare (e prepara
il terreno agli effetti dichiarati di Full).
Come condividere stato, allora? In tre modi: restituendo il nuovo valore e lasciando al chiamante l'assegnamento (il più chiaro); usando una lista, che è mutabile e viene passata per riferimento (capitolo 10); usando una chiusura, come il contatore.
- Le lambda (
x -> e,(a, b) -> e) sono funzioni anonime; per più istruzioni si usabegin. - Le funzioni si passano come argomenti e si restituiscono come risultati.
- Una chiusura cattura l'ambiente in cui nasce; in Core le variabili catturate sono condivise.
- Lo scope è lessicale; le funzioni nominate assegnano in locale, le lambda possono aggiornare i nomi esterni.
- Scrivi
componi(f, g)che restituisca la funzionex -> f(g(x))e provala con due lambda. - Scrivi
crea_accumulatore()che restituisca una lambda che aggiunge un elemento a una lista interna e restituisce la lista. - Spiega, con le regole del paragrafo 9.5, che cosa stampa un programma in cui una funzione nominata assegna una variabile globale e poi la stampa.
← Capitolo 8 — Funzioni e ricorsione · Sommario · Capitolo 10 — Liste e dati strutturati →
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