Funzioni
Le funzioni in Lua sono il principale mezzo di astrazione e lo strumento base per rendere il codice strutturato. Nelle prime puntate di questo corso base su Lua ne avete sentito la mancanza, dite la verità!Coerentemente con il resto del linguaggio la sintassi di una funzione comprende due parole chiave che servono per delimitare il blocco di codice contenuto in essa: 'function' ed 'end'. Una funzione può accettare argomenti e può restituire dati tramite la parola chiave 'return'.
Come primo esempio, vi presento una funzione per calcolare l'ennesimo numero della serie di Fibonacci. Un elemento si ottiene sommando i precedenti due elementi avendo posto uguale a 1 i primi due:
function fibonacci(n)
if n < 2 then
return 1
end
local n1, n2 = 1, 1
for i = 1, n-1 do
n1, n2 = n2, n1 + n2 -- assegnazione multipla
end
return n1
end
print(fibonacci(10)) --> 55
Con le regole dell'assegnazione multipla una funzione può accettare più argomenti. Se essa verrà chiamata con più argomenti rispetto a quelli che essa prevede quelli in eccesso verranno ignorati mentre, viceversa, se gli argomenti sono inferiori a quelli previsti allora a quelli mancanti verrà assegnato il valore nil.
Ma questo vale anche per i dati di ritorno quando la funzione è usata come espressione in un'istruzione di assegnamento. Basta inserire dopo l'istruzione return la lista delle espressioni separate da virgola che saranno valutate e assegnate alle corrispondenti variabili.
Per esempio, potremo modificare la funzione precedente per restituire la somma dei primi n numeri di Fibonacci oltre che solamente l'ennesimo elemento della serie stessa e considerare un valore di default se l'argomento è nil:
function fibonacci(n)
n = n or 10 -- the default value is 10
if n == 1 then
return 1, 1
end
if n == 2 then
return 1, 2
end
local sum = 1
local n1, n2 = 1, 1
for i = 1, n-1 do
n1, n2 = n2, n1 + n2
sum = sum + n1
end
return n1, sum
end
local fib_10, sum_fib_10 = fibonacci()
print(fib_10, sum_fib_10)
Funzioni: valori di prima classe, I
In Lua le funzioni sono un tipo. Possono essere assegnate a una variabile e passate come argomento a una funzione.Questa proprietà non si trova spesso nei linguaggi di scripting e offre una nuova flessibilità al codice.
Tutte le funzioni sono memorizzate in variabili. Per assegnare direttamente una funzione a una variabile esiste in Lua la sintassi anonima:
local add = function (a, b)
return a + b
end
print(add(45.4564, 161.486))
Essendo le funzioni valori di prima classe ne consegue che in Lua le funzioni sono oggetti senza nome esattamente come lo sono tipi come i numeri e le stringhe. Inoltre, la sintassi classica di definizione
function variable_name (args)
-- function body
end
è solo zucchero sintattico perché l'interprete Lua la tradurrà effettivamente e automaticamente nel codice equivalentevariable_name = function (args)
-- function body
end
Funzioni: valori di prima classe, II
Un esempio di funzione con un argomento funzione è il seguente, una funzione esegue un numero di volte dato la stessa funzione priva di argomenti:
function print_five()
print(5)
end
function do_many(fn, n)
for i=1, n or 1 do
fn()
end
end
do_many(print_five)
do_many(print_five, 10)
do_many(function () print("---") end, 12)
Molto interessante, anzi senza dubbio fantastico. Nell'ultima riga di codice l'argomento è una funzione definita in sintassi anonima che verrà eseguita 12 volte.
Per prendere confidenza con il concetto di funzioni come valori di prima classe, cambiamo il significato della funzione precostruita in Lua 'print()'. Ecco come:
local println = print
print = function (n)
println("Argomento funzione -> "..n)
end
print(12)
Tabelle e funzioni
Se una tabella può contenere chiavi con qualsiasi valore allora può contenere anche funzioni!Le sintassi sono queste --- esplicitate con il codice riportato di seguito:
- assegnare la variabile di funzione a una chiave di tabella;
- assegnare direttamente la chiave di tabella con la definizione di funzione in sintassi anonima;
- usare il costruttore di tabelle per assegnare funzioni in sintassi anonima.
-- primo caso
local function tipo_i()
-- body
end
local t = {}
t.func_1 = tipo_i
-- secondo caso
local t = {}
t.func_2 = function ()
-- body
end
-- terzo caso con più di una funzione
local t = {
func_3_i = function ()
-- body
end,
func_3_ii = function ()
-- body
end,
func_3_iii = function ()
-- body
end,
}
Con questo meccanismo una tabella può svolgere il ruolo di modulo memorizzando funzioni utili a un certo scopo e in effetti la libreria standard di Lua si presenta all'utente proprio in questo modo.
Variadic arguments
Una funzione può ricevere un numero variabile di argomenti rappresentati da tre dot consecutivi '...'. Nel corpo della funzione i tre punti rappresenteranno la lista degli argomenti, dunque possiamo o costruire con essi una tabella oppure effettuare un'assegnazione multipla.Un esempio è una funzione che restituisce la somma di tutti gli argomenti numerici:
-- per un massimo di 3 argomenti
local function add_three(...)
local n1, n2, n3 = ...
return (n1 or 0) + (n2 or 0) + (n3 or 0)
end
-- con tutti gli argomenti
local function add_all(...)
local t = {...} -- collecting args in a table
local sum = 0
for i = 1, #t do
sum = sum + t[i]
end
return sum
end
print(add_three(40, 20))
print(add_all(45, 48, 5456))
print(add_three(14, 15), add_all(-89, 45.6))
Per inciso, anche la funzione base 'print()' accetta un numero variabile di argomenti.
Il meccanismo è ancora più flessibile perché tra i primi argomenti vi possono essere variabili "fisse".
Per esempio il primo parametro potrebbe essere un moltiplicatore:
local function add_and_multiply(molt, ...)
local t = {...}
local sum = 0
for i = 1, #t do
sum = sum + t[i]
end
return molt * sum
end
print(add_and_multiply(10, 45.23, 48, 9.36, -8, -56.3))
Un'altra funzione predefinita 'select()' consente di accedere alla lista degli argomenti in dettaglio. Infatti se tra gli argomenti compare un valore nil avremo problemi ad accedere ai paraemetri successivi nel codice precedente perché --- come sappiamo già --- l'operatore di lunghezza # considera il nil come valore sentinella di fine array/tabella.
Il selettore prevede un primo parametro fisso e la lista variabile inserita con i tre punti '...'. Se questo parametro è un intero allora questo verrà utilizzato come indice per restituire il corrispondente parametro. Se invece il parametro è la stringa "#" restitisce il numero di argomenti extra presenti dopo l'eleventuale nil intermedio.
Il codice seguente preso pari pari dal PIL --- certamente il punto di riferimento principale su Lua scritto dallo stesso Autore del linguaggio Roberto Ierusalimschy, tra l'altro composto in LaTeX e venduto come contributo al progetto Lua stesso:
for i = 1, select("#", ...) do
local arg = select(i, ...)
-- loop body
end
Omettere le parentesi tonde se...
In Lua esiste la sintassi di chiamata a funzione semplificata, ammessa opzionalmente solo se:- la funzione accetta un unico argomento di tipo stringa;
- la funzione accetta un unico argomento di tipo tabella.
Per esempio:
print "si è possibile anche questo..."
local function is_empty(t)
if #t == 0 then
return true
else
return false
end
end
-- questo:
print(is_empty{})
print(is_empty{1, 2, 3})
-- invece di questo (sempre possibile):
print(is_empty({}))
print(is_empty({1, 2, 3}))
Closure
Chiudiamo la puntata con uno strano termine forse meglio noto agli sviluppatori dei linguaggi funzionali: la closure.Questa proprietà di Lua amplia il concetto di funzione rendendo possibile l'accesso dall'interno di essa ai dati presenti nel contesto esterno. Ciò è possibile perché alla chiamata di una funzione viene creato uno spazio di memoria del contesto esterno unico e indipendente.
Tutte le chiamate a una stessa funzione condivideranno una stessa closure.
Se questo è vero una funzione potrebbe incrementare un contatore creato al suo interno, e anche qui prendo l'esempio di codice dal PIL:
local function new_counter()
local i = 0 -- variabile nel contesto esterno
return function ()
i = i + 1 -- accesso alla closure
return i
end
end
local c1 = new_counter()
print(c1()) --> 1
print(c1()) --> 2
print(c1()) --> 3
print(c1()) --> 4
print(c1()) --> 5
local c2 = new_counter()
print(c2()) --> 1
print(c2()) --> 2
print(c2()) --> 3
print(c1()) --> 6
A parole il codice definisce una funzione 'new_counter()' che restituisce una funzione che ha accesso indipendente al contesto (la variabile 'i').
Come si verifica dalle righe successive a ogni chiamata della funzione contatore il valore catturato è unico e indipendente.
Tecnicamente la closure è la funzione effettiva mentre invece la funzione non è altro che il prototipo della closure.
Le closure consentono di implementare diverse tecniche utili in modo naturale e concettualmente semplice. Una funzione di ordinamento potrebbe per esempio accettare come parametro una funzione di confronto per stabilire l'ordine tra due elementi tramite l'accesso a una seconda tabella esterna contenente informazioni utili per l'ordinamento stesso.
Nel prossimo esempio mettiamo in pratica l'idea appena presentata. Il codice utilizza una funzione della libreria di Lua, che introdurremo nella prossima puntata, in particolare table.sort(), per applicare l'algoritmo di ordinamento alla tabella passata come argomento in base al criterio di ordine stabilito con la funzione passata come secondo argomento in sintassi anonima.
local years = {1994, 1996, 1998, 2000, 2002}
local val = {
[1994] = 12.5,
[1996] = 10.2,
[1998] = 10.9,
[2000] = 8.9,
[2002] = 12.9,
}
local function sort_by_value(tab)
table.sort(tab,
function (a, b)
return val[a] > val[b]
end
)
end
sort_by_value(years)
for i = 1, #years do
print(years[i])
end
Esercizi
1 - Scrivere una funzione che sulla base della stringa in ingresso "+", "-", "*", "/" restituisca la funzione corrispondente per due argomenti.
2 - Scrivere la funzione che accetti due argomenti numerici e ne restituisca i risultati delle quattro operazioni aritmetiche.
3 - Scrivere una funzione che restituisca il fattoriale di un numero memorizzandone in una tabella di closure i risultati per evitare di ripetere il calcolo in chiamate successive con pari argomento.
4 - Scrivere una funzione con un argomento opzionale rispetto al primo parametro numerico che ne restituisca il seno interpretandolo in radianti se l'argomento opzionale è nil oppure "rad", in gradi sessadecimali se "deg" o in gradi centesimali se "grd".
5 - Scrivere una funzione che accetti come primo argomento una funzione f: R -> R (prende un numero e restituisce un numero), come secondo e terzo argomento i due valori dell'intervallo di calcolo e come quarto argomento il numero di punti in cui suddividere l'intervallo. La funzione dovrà stampare i valori che la funzione argomento assume nei punti definiti.
Se manca il return una funzione ritorna l'ultima espressione valutata, se non sbaglio.
RispondiEliminaCosa che capita anche con altri linguaggi, sempre se ricordo bene ;-)
Ciao Juhan,
RispondiEliminascusa ma ho visto il commento solo ora.
Ti rispondo alla tua domanda: se manca il return il valore restituito è nil.
Quello a cui ti riferisci accade in Rust, infatti in Lua la sintassi prevede che un'espressione debba essere assegnata, per esempio a una variabile o all'argomento di una funzione.
Alla prossima.
R.
OOPS! confusione mia, sto vedendo troppi linguaggi assieme alla ricerca di cose di altri ancora.
Elimina