venerdì 10 aprile 2015

Corso Lua - puntata 6 - Le funzioni


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 equivalente
variable_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:
  1. assegnare la variabile di funzione a una chiave di tabella;
  2. assegnare direttamente la chiave di tabella con la definizione di funzione in sintassi anonima;
  3. 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.
e consiste nella possibilità di ommettere le parentesi tonde ().
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.

Riassunto della puntata

Una puntata bella densa dedicata alle funzioni di Lua caratterizzate dall'essere valori di prima classe --- o anche valori higher-order --- e dalle closures. Niente male per un linguaggio di scripting ;-)

3 commenti:

  1. Se manca il return una funzione ritorna l'ultima espressione valutata, se non sbaglio.
    Cosa che capita anche con altri linguaggi, sempre se ricordo bene ;-)

    RispondiElimina
  2. Ciao Juhan,
    scusa 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.

    RispondiElimina
    Risposte
    1. OOPS! confusione mia, sto vedendo troppi linguaggi assieme alla ricerca di cose di altri ancora.

      Elimina