Scope e Closure in Lua
In Lua, comprendere lo scope (ambito di visibilita) delle variabili e il concetto di closure e fondamentale per scrivere codice pulito, sicuro e ben organizzato. Lua adotta un sistema di scoping lessicale che, combinato con le funzioni di prima classe, permette di creare pattern potenti come lo stato privato e le funzioni factory. In questa guida esploreremo questi concetti in dettaglio.
Variabili Locali e Globali
In Lua, le variabili dichiarate con local sono locali al blocco in cui vengono definite. Le variabili senza local sono globali e accessibili ovunque nel programma.
-- Variabile globale (sconsigliata nella maggior parte dei casi)
messaggio = "Sono globale"
-- Variabile locale
local saluto = "Sono locale"
print(messaggio) --> Sono globale
print(saluto) --> Sono locale
E una buona pratica usare sempre local per le variabili. Le variabili globali inquinano il namespace globale e possono causare conflitti difficili da individuare.
-- Cattiva pratica
contatore = 0 -- globale, visibile ovunque
-- Buona pratica
local contatore = 0 -- locale, visibile solo nel blocco corrente
Scoping Lessicale
Lua utilizza lo scoping lessicale (o statico): la visibilita di una variabile e determinata dalla sua posizione nel codice sorgente, non dal flusso di esecuzione.
local x = 10
local function stampa_x()
print(x) -- x e visibile perche dichiarata nello scope esterno
end
stampa_x() --> 10
Le funzioni interne possono accedere alle variabili dichiarate nei blocchi che le contengono:
local function esterna()
local valore = "ciao"
local function interna()
print(valore) -- accede alla variabile dello scope esterno
end
interna() --> ciao
end
esterna()
Variable Shadowing
Quando una variabile locale ha lo stesso nome di una variabile in uno scope esterno, la variabile interna nasconde (shadow) quella esterna.
local x = "esterno"
local function test()
local x = "interno" -- nasconde la x esterna
print(x) --> interno
end
test()
print(x) --> esterno (la x originale non e stata modificata)
Lo shadowing puo verificarsi anche nei cicli:
local valore = 100
for i = 1, 3 do
local valore = i * 10 -- shadow della variabile esterna
print("Dentro il ciclo: " .. valore)
end
print("Fuori dal ciclo: " .. valore)
-- Output:
-- Dentro il ciclo: 10
-- Dentro il ciclo: 20
-- Dentro il ciclo: 30
-- Fuori dal ciclo: 100
Blocchi do…end per Limitare lo Scope
Lua offre i blocchi do..end per creare scope locali senza bisogno di funzioni o strutture di controllo.
local risultato
do
local a = 10
local b = 20
risultato = a + b
-- a e b esistono solo dentro questo blocco
end
print(risultato) --> 30
-- print(a) --> errore: a non e definita qui
Questo e utile per limitare la vita di variabili temporanee e mantenere pulito il namespace:
do
local file = io.open("config.txt", "r")
if file then
local contenuto = file:read("*a")
file:close()
-- elaborare il contenuto...
print("Letto: " .. #contenuto .. " caratteri")
end
end
-- file e contenuto non sono piu accessibili qui
Cosa Sono le Closure
Una closure e una funzione che cattura e “ricorda” le variabili del suo ambiente lessicale, anche dopo che lo scope in cui e stata creata e terminato. Le variabili catturate si chiamano upvalue.
local function crea_saluto(nome)
-- 'nome' e un upvalue per la funzione restituita
return function()
print("Ciao, " .. nome .. "!")
end
end
local saluta_marco = crea_saluto("Marco")
local saluta_giulia = crea_saluto("Giulia")
saluta_marco() --> Ciao, Marco!
saluta_giulia() --> Ciao, Giulia!
Ogni chiamata a crea_saluto produce una closure con il proprio valore indipendente di nome.
Upvalue e Stato Mutabile
Le closure possono non solo leggere ma anche modificare i propri upvalue, mantenendo uno stato privato tra le chiamate.
local function crea_contatore(inizio)
local conteggio = inizio or 0
return {
incrementa = function()
conteggio = conteggio + 1
return conteggio
end,
decrementa = function()
conteggio = conteggio - 1
return conteggio
end,
valore = function()
return conteggio
end
}
end
local c = crea_contatore(0)
print(c.incrementa()) --> 1
print(c.incrementa()) --> 2
print(c.incrementa()) --> 3
print(c.decrementa()) --> 2
print(c.valore()) --> 2
La variabile conteggio e accessibile solo attraverso le tre funzioni restituite: non esiste modo di modificarla direttamente dall’esterno. Questo e un pattern di incapsulamento.
Closure come Factory Function
Le closure sono ideali per creare funzioni factory che generano funzioni specializzate.
-- Factory per moltiplicatori
local function moltiplicatore(fattore)
return function(x)
return x * fattore
end
end
local doppio = moltiplicatore(2)
local triplo = moltiplicatore(3)
local per_dieci = moltiplicatore(10)
print(doppio(5)) --> 10
print(triplo(5)) --> 15
print(per_dieci(5)) --> 50
-- Factory per validatori
local function lunghezza_minima(minimo)
return function(testo)
return #testo >= minimo
end
end
local almeno_3 = lunghezza_minima(3)
local almeno_8 = lunghezza_minima(8)
print(almeno_3("Lua")) --> true
print(almeno_3("ab")) --> false
print(almeno_8("password")) --> true
print(almeno_8("pass")) --> false
Closure per Stato Privato
Le closure permettono di simulare variabili private, un concetto comune nella programmazione orientata agli oggetti.
local function crea_conto_bancario(titolare, saldo_iniziale)
local saldo = saldo_iniziale or 0
local movimenti = {}
local function registra(tipo, importo)
table.insert(movimenti, {
tipo = tipo,
importo = importo,
saldo_dopo = saldo
})
end
return {
deposita = function(importo)
if importo > 0 then
saldo = saldo + importo
registra("deposito", importo)
print(titolare .. ": depositato " .. importo .. " EUR")
end
end,
preleva = function(importo)
if importo > 0 and importo <= saldo then
saldo = saldo - importo
registra("prelievo", importo)
print(titolare .. ": prelevato " .. importo .. " EUR")
else
print(titolare .. ": fondi insufficienti")
end
end,
saldo_corrente = function()
return saldo
end,
estratto_conto = function()
print("--- Estratto conto di " .. titolare .. " ---")
for _, m in ipairs(movimenti) do
print(string.format(" %s: %.2f EUR (saldo: %.2f)",
m.tipo, m.importo, m.saldo_dopo))
end
print(string.format(" Saldo attuale: %.2f EUR", saldo))
end
}
end
local conto = crea_conto_bancario("Marco", 1000)
conto.deposita(500)
conto.preleva(200)
conto.preleva(2000) --> fondi insufficienti
conto.estratto_conto()
print("Saldo: " .. conto.saldo_corrente() .. " EUR")
In questo esempio, saldo e movimenti sono completamente inaccessibili dall’esterno: l’unico modo per interagire con essi e attraverso le funzioni esposte.
Closure come Callback
Le closure sono perfette per i callback, funzioni passate come argomento che possono catturare contesto dal loro ambiente.
local function esegui_su_lista(lista, callback)
for i, elemento in ipairs(lista) do
callback(i, elemento)
end
end
local totale = 0
local prezzi = {12.50, 8.99, 25.00, 3.75}
esegui_su_lista(prezzi, function(i, prezzo)
totale = totale + prezzo -- la closure cattura 'totale'
print(string.format("Prodotto %d: %.2f EUR", i, prezzo))
end)
print(string.format("Totale: %.2f EUR", totale))
-- Output:
-- Prodotto 1: 12.50 EUR
-- Prodotto 2: 8.99 EUR
-- Prodotto 3: 25.00 EUR
-- Prodotto 4: 3.75 EUR
-- Totale: 50.24 EUR
Conclusioni
Lo scoping lessicale e le closure sono tra le caratteristiche piu potenti di Lua. Le variabili locali (local) garantiscono un codice pulito e privo di conflitti, i blocchi do..end permettono di limitare lo scope quando necessario e le closure consentono di creare funzioni con stato privato, factory function e callback espressivi. Padroneggiare questi concetti e essenziale per scrivere codice Lua idiomatico, modulare e manutenibile.