Vai al contenuto

Lua/Funzioni

Wikibooks, manuali e libri di testo liberi.
< Lua
Indice del libro
Un'illustrazione di una pila e delle operazioni che possono essere eseguite su di essa.
Un'illustrazione di una pila e delle operazioni che possono essere eseguite su di essa.

Una pila (stack) è un elenco in cui gli elementi possono essere aggiunti (pushed) o rimossi (popped), e che si comporta secondo il principio last-in-first-out. Questo significa che l'ultimo elemento aggiunto sarà il primo a essere rimosso. Ecco perché tali elenchi sono chiamati pile: in una pila non puoi rimuovere un elemento senza prima aver rimosso gli elementi che si trovano sopra. Tutte le operazioni avvengono quindi in cima alla pila. Un elemento è sopra un altro se è stato aggiunto dopo quell'elemento, mentre è sotto se è stato aggiunto prima di quell'elemento.

Una funzione (chiamata anche subroutine, procedura, routine o sottoprogramma) è una sequenza di istruzioni che eseguono un'attività specifica e che può essere chiamata da un'altra parte del programma ogni volta che quella sequenza di istruzioni deve essere eseguita. Le funzioni possono anche ricevere valori come input e restituire un output dopo aver potenzialmente manipolato l'input o eseguito un'attività in base all'input. Le funzioni possono essere definite in qualsiasi punto del programma, anche all'interno di altre funzioni. Inoltre possono essere chiamate da qualsiasi parte di un programma che abbia accesso a esse. Proprio come i numeri e le stringhe, le funzioni sono valori e possono quindi essere memorizzate in variabili e avere tutte le proprietà che sono comuni alle variabili. Queste caratteristiche rendono le funzioni molto utili.

Poiché le funzioni possono essere chiamate da altre funzioni, l'interprete Lua (il programma che legge ed esegue il codice Lua) deve essere in grado di sapere quale funzione ha chiamato la funzione che sta eseguendo al momento, in modo che, quando la funzione termina (quando non c'è più codice da eseguire), possa tornare all'esecuzione della funzione corretta. Ciò avviene con una pila chiamata stack delle chiamate: ogni elemento nello stack delle chiamate è una funzione che ha chiamato la funzione che si trova direttamente sopra di esso nello stack, fino all'ultimo elemento nello stack, che è la funzione attualmente in esecuzione. Quando una funzione termina, l'interprete utilizza l'operazione pop dello stack per rimuovere l'ultima funzione nell'elenco e quindi torna alla funzione precedente.

Tipi di funzioni

[modifica | modifica sorgente]

Esistono due tipi di funzioni: funzioni integrate e funzioni definite dall'utente. Le funzioni integrate (built-in functions) sono funzioni fornite con Lua e includono funzioni come la funzione print, che già conosci. Ad alcune è possibile accedere direttamente, come appunto la funzione print, ma ad altre è necessario accedere tramite una libreria, come la funzione math.random, che restituisce un numero casuale. Le funzioni definite dall'utente (user-defined functions), invece, sono definite tramite un costruttore di funzioni:

local func = function(primo_parametro, secondo_parametro, terzo_parametro)
	-- corpo della funzione (il corpo di una funzione è il codice che contiene)
end

Il codice sopra crea una funzione con tre parametri e la memorizza nella variabile func. Il codice seguente fa esattamente la stessa cosa del codice sopra, ma usa il syntactic sugar per definire la funzione in modo più conciso:

local function func(primo_parametro, secondo_parametro, terzo_parametro)
	-- corpo della funzione
end

Va notato che, quando si usa la seconda forma, è possibile fare riferimento alla funzione dall'interno di se stessa, cosa che non è possibile quando si usa la prima forma. Questo perché local function foo() end si traduce in local foo; foo = function() end piuttosto che local foo = function() end. Ciò significa che foo fa parte dell'ambiente della funzione nella seconda forma e non nella prima, il che spiega perché la seconda forma rende possibile fare riferimento alla funzione stessa. In entrambi i casi, è possibile omettere la parola chiave local per memorizzare la funzione in una variabile globale.

Parametri e argomenti

[modifica | modifica sorgente]

I parametri funzionano come variabili e consentono alle funzioni di ricevere valori. Quando una funzione viene chiamata, possono essere forniti gli argomenti e la funzione li riceverà come parametri. I parametri sono come le variabili locali definite all'inizio di una funzione e saranno assegnati in base all'ordine degli argomenti forniti nella chiamata di funzione; se manca un argomento, il parametro avrà il valore nil. La funzione nell'esempio seguente somma due numeri e stampa il risultato. Pertanto stamperà 5 quando il codice viene eseguito.

local function add(primo_numero,  secondo_numero)
	print(primo_numero + secondo_numero)
end

add(2, 3)

Le chiamate di funzione sono per la maggior parte nella forma name(arguments). Tuttavia, se c'è un solo argomento ed è una tabella o una stringa, e non è in una variabile (il che significa che è costruito direttamente nella chiamata di funzione, espresso come letterale), le parentesi possono essere omesse:

print "Hello, world!"
print {4, 5}

La seconda riga di codice nell'esempio precedente stamperebbe l'indirizzo di memoria della tabella. Quando si convertono i valori in stringhe, cosa che la funzione print fa automaticamente, i tipi complessi (funzioni, tabelle, userdata e thread) vengono modificati nei loro indirizzi di memoria. I valori booleani, i numeri e il valore nil, tuttavia, verranno convertiti nelle stringhe corrispondenti.

Nella pratica i termini parametro e argomento sono spesso usati in modo intercambiabile. In questo libro, e nei loro significati propri, i termini parametro e argomento significano, rispettivamente, un nome a cui verrà assegnato il valore dell'argomento corrispondente e un valore che viene passato a una funzione per essere assegnato a un parametro.

Valori restituiti

[modifica | modifica sorgente]

Le funzioni possono ricevere un input, manipolarlo e restituire un output. Sai già come possono ricevere input (parametri) e manipolarlo (corpo della funzione). Possono anche restituire output restituendo uno o più valori di qualsiasi tipo, cosa che si fa usando l'istruzione return. Ecco perché le chiamate di funzione sono sia istruzioni che espressioni: possono essere eseguite, ma possono anche essere valutate.

local function add(primo_numero, secondo_numero)
	return primo_numero + secondo_numero
end

print(add(5, 6))

Il codice nella funzione sopra definirà prima la funzione add, quindi, la chiamerà con 5 e 6 come valori. La funzione li sommerà e restituirà il risultato, che verrà quindi stampato. Ecco perché il codice soprastante stamperebbe 11. È anche possibile che una funzione restituisca molti valori separando con virgole le espressioni che valutano questi valori.

Gli utenti possono definire le proprie funzioni e personalizzarle in base alle proprie esigenze.

Oltre ad accettare argomenti e restituire valori, le funzioni definite dall'utente possono anche avere effetti collaterali, che sono modifiche alle variabili o allo stato del programma causate dall'esecuzione della funzione.[1]

Esistono tre tipi di errori: errori sintattici, errori semantici statici ed errori semantici. Gli errori sintattici si verificano quando il codice è palesemente non valido. Il seguente codice, ad esempio, verrebbe rilevato da Lua come non valido:

print(5 ++ 4 return)

Il codice sopra non ha senso: è impossibile ricavarne un significato. Allo stesso modo, in italiano, "cane gatto albero" non è sintatticamente valida, non ha alcun significato perché non segue le regole per creare una frase.

Gli errori semantici statici si verificano quando il codice ha un significato, ma non ha comunque senso. Ad esempio, se provi a sommare una stringa con un numero, ottieni un errore semantico statico perché è impossibile sommare una stringa con un numero:

print("hello" + 5)

Il codice sopra segue le regole sintattiche di Lua, ma non ha comunque senso perché è impossibile sommare una stringa con un numero (tranne quando la stringa rappresenta un numero, nel qual caso verrà forzata in un numero). Questo può essere paragonato in italiano alla frase "Io siamo". Segue le regole per creare frasi in italiano, ma non ha comunque senso perché "Io" è singolare e "siamo" è plurale.

Infine, gli errori semantici sono errori che si verificano quando il significato di un pezzo di codice non è quello che il suo creatore pensa che sia. Questi sono gli errori peggiori perché possono essere molto difficili da trovare. Lua ti dirà sempre quando c'è un errore sintattico o un errore semantico statico (questo è chiamato "generare un errore"), ma non può dirti quando c'è un errore semantico perché non sa cosa pensi che sia il significato del codice. Questi errori si verificano più spesso di quanto si pensi, e i programmatori dedicano molto tempo a trovarli e correggerli.

Il processo di ricerca degli errori e la loro correzione è chiamato debugging. Nella maggior parte dei casi, i programmatori impiegano più tempo a trovare gli errori che a correggerli. Questo vale per tutti i tipi di errori. Una volta che sai qual è il problema, di solito è semplice risolverlo; a volte, però, un programmatore può guardare un pezzo di codice per ore senza trovare cosa c'è che non va.

Chiamate protette

[modifica | modifica sorgente]

Generare un errore è l'azione di indicare, da parte dell'interprete (il programma che legge il codice e lo esegue), che qualcosa non va nel codice. Viene fatto automaticamente da Lua quando il codice fornito non è valido, ma può essere fatto manualmente con la funzione error:

local variable = 500
if variable % 5 ~= 0 then
	error("Deve essere possibile dividere il valore della variabile per 5 senza ottenere un numero decimale.")
end

La funzione error ha anche un secondo argomento, che indica il livello di stack a cui dovrebbe essere generato l'errore, ma questo non sarà trattato in questo libro. La funzione assert fa la stessa cosa della funzione error, ma genererà un errore solo se il suo primo argomento valuta nil o false e non ha un argomento che può essere utilizzato per specificare il livello di stack a cui dovrebbe essere generato l'errore. La funzione assert è utile all'inizio di uno script, ad esempio, per verificare se è disponibile una libreria necessaria per il funzionamento dello script.

Potrebbe essere difficile capire perché si vorrebbe volontariamente generare un errore, dal momento che il codice di un programma smette di funzionare ogni volta che viene generato un errore, ma spesso generare errori quando le funzioni vengono utilizzate in modo errato o quando un programma non viene eseguito nell'ambiente corretto può essere utile per aiutare la persona che dovrà eseguire il debug del codice a trovarlo immediatamente, senza dover fissare il codice per molto tempo senza rendersi conto di cosa non va.

A volte, può essere utile impedire che un errore interrompa il codice, e fare visualizzare un messaggio di errore all'utente in modo che possa segnalare il bug allo sviluppatore. Questo è chiamato "gestione delle eccezioni" (o gestione degli errori) e viene eseguito catturando l'errore per impedirne la propagazione ed eseguendo un gestore delle eccezioni per gestirlo. Il modo in cui viene eseguito nei diversi linguaggi di programmazione varia molto. In Lua, viene eseguito utilizzando chiamate protette.[2] Le chiamate protette hanno questo nome perché una funzione chiamata in modalità protetta non interromperà il programma se si verifica un errore. Ci sono due funzioni che possono essere utilizzate per chiamare una funzione in modalità protetta:

Funzione Descrizione
pcall(function, ...) Chiama la funzione in modalità protetta e restituisce un codice di stato (un valore booleano il cui valore dipende dal fatto che sia stato generato o meno un errore) e i valori restituiti dalla funzione, o il messaggio di errore se la funzione è stata interrotta da un errore. Gli argomenti possono essere forniti alla funzione passandoli alla funzione pcall dopo il primo argomento, che è la funzione che dovrebbe essere chiamata in modalità protetta.
xpcall(function, handler, ...) Fa la stessa cosa di pcall, ma, quando nella funzione si verifica un errore, invece di restituire gli stessi valori che restituirebbe pcall, chiama la funzione gestore con essi come parametri. La funzione gestore può quindi essere utilizzata, ad esempio, per visualizzare un messaggio di errore. Per quanto riguarda la funzione pcall, gli argomenti possono essere passati alla funzione tramite la loro trasmissione alla funzione xpcall.

Sovraccarico dello stack

[modifica | modifica sorgente]

Lo stack di chiamata, la pila che contiene tutte le funzioni che sono state chiamate nell'ordine in cui sono state chiamate, è stato citato in precedenza. Lo stack di chiamata nella maggior parte dei linguaggi, incluso Lua, ha una dimensione massima. Questa dimensione è così grande che non dovrebbe essere preoccupante nella maggior parte dei casi, ma le funzioni che chiamano se stesse (questo è chiamato ricorsività e tali funzioni sono chiamate funzioni ricorsive) possono raggiungere questo limite se non c'è nulla che impedisca loro di chiamare se stesse più e più volte all'infinito. Questo è chiamato stack overflow (sovraccarico della pila). Quando lo stack va in overflow, il codice smette di funzionare e viene generato un errore.

Funzioni variadiche

[modifica | modifica sorgente]

Le funzioni variadiche, chiamate anche funzioni vararg, sono funzioni che accettano un numero variabile di argomenti. Una funzione variadica è indicata da tre punti (...) alla fine dell'elenco di parametri. Gli argomenti che non rientrano nei parametri nell'elenco, invece di essere scartati, vengono resi disponibili alla funzione tramite un'espressione vararg, anch'essa indicata da tre punti. Il valore di un'espressione vararg è un elenco di valori (non una tabella) che può quindi essere inserito in una tabella per essere manipolato con maggiore facilità con la seguente espressione: {...}. In Lua 5.0, invece di essere disponibili tramite un'espressione vararg, gli argomenti extra erano disponibili in un parametro speciale chiamato arg. La seguente funzione è un esempio di una funzione che aggiungerebbe il primo argomento a tutti gli argomenti che riceve, quindi li sommerebbe tutti insieme e stamperebbe il risultato:

function add_one(increment, ...)
	local result = 0
	for _, number in next, {...} do
		result = result + number + increment
	end
end

Non è necessario comprendere il codice sopra riportato poiché è solo esempio di funzione variadica.

La funzione select è utile per manipolare elenchi di argomenti senza dover usare tabelle. È di per sé una funzione variadica, poiché accetta un numero indefinito di argomenti. Restituisce tutti gli argomenti dopo l'argomento con il numero dato come primo argomento (se il numero dato è negativo, indicizza a partire dalla fine, il che significa che -1 è l'ultimo argomento). Restituirà anche il numero di argomenti ricevuti, escluso il primo, se il primo argomento è la stringa "#". Può essere utile scartare tutti gli argomenti in un elenco di argomenti prima di un certo numero e, in modo più originale, distinguere tra i valori nil passati come argomenti e l'assenza completa di argomenti. Infatti, select distinguerà, quando "#" è dato come primo argomento, valori nil da nessun valore. Gli elenchi di argomenti (e anche gli elenchi di ritorno) sono istanze di tuple, che saranno esplorate nel capitolo sulle tabelle; la funzione select funziona con tutte le tuple.

print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)()) --> no value
print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)(nil)) --> nil
print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)(variable_that_is_not_defined)) --> nil

-- Come mostra questo codice, la funzione è in grado di rilevare se il valore nil è stato passato come argomento o se semplicemente non è stato passato alcun valore. 
-- In circostanze normali, entrambi sono considerati nil e questo è l'unico modo per distinguerli.
  1. (EN) Lua Functions - Comprehensive Guide with Examples, su mrexamples.com, 22 marzo 2023. URL consultato il 17 maggio 2023.
  2. Per approfondire: Roberto Ierusalimschy, Error Handling in Application Code, in Programming in Lua, Lua.org, 2003, ISBN 8590379817. URL consultato il June 20, 2014.

Bibliografia

[modifica | modifica sorgente]