Introduzione alla oop
Dal punto di vista dell'utente, il paradigma della Object Oriented Programming si basa sulla creazione di entità indipendenti chiamate oggetti e l'utilizzo di servizi attraverso i metodi.Ciascun oggetto incorpora dati e metodi che ne definiscono il comportamento che è identico per tutti quelli di una stessa famiglia chiamata classe, una sorta di prototipo che rappresenta un ``tipo di dati''.
Nel gergo della programmazione oop la creazione di un oggetto avviene con un metodo particolare chiamato costruttore. Nei linguaggi con tipi statici è obbligatorio specificare il tipo dell'oggetto --- a meno che il compilatore non sia in grado di determinare il tipo automaticamente --- mentre in quelli di scripting solitamente il tipo è definito a tempo di esecuzione in modo dinamico e la robustezza dei programmi diminuisce in favore della semplicità.
In Lua non esiste la parola chiave new che istanzia il nuovo oggetto in memoria chiamandone il costruttore che risulta essere un metodo qualsiasi dell'oggetto --- addirittura non obbligatorio --- che lo sviluppatore della libreria potrebbe chiamare in qualsiasi modo mentre per esempio in Java esso deve assumere il nome della classe.
Il linguaggio Lua non è quindi progettato con gli stessi obiettivi di Java: non possiede un controllo preventivo del tipo --- i relativi errori di programmazione emergono solamente nella fase di esecuzione --- non prevede una chiara e semplice modalità di generazione dell'oggetto sfruttando funzionalità tortuose poco eleganti e non impone una struttura sintattica unica ma lascia al programmatore la libertà --- e la responsabilità che ne deriva --- di sviluppare soluzioni sintattiche alternative.
I linguaggi oop prevedono la possibilità di dichiarare come privati campi e metodi. Lua non offre invece alcun meccanismo di protezione semplicemente contando sul comportamento corretto dell'utente della libreria.
Tuttavia Lua offre pieno supporto ai principi del paradigma a oggetti senza perdere le caratteristiche generali di un linguaggio semplice.
Una classe 'Rettangolo'
Costruiremo una classe per rappresentare un rettangolo. Si tratta di un ente geometrico definito da due soli parametri: larghezza e altezza.
Un primo tentativo potrebbe essere questo:
-- prima tentativo di implementazione
-- di una classe rettangolo
Rectangle = {} -- creazione tabella (oggetto)
-- creazione di due campi
Rectangle.width = 12
Rectangle.height = 7
-- un primo metodo assegnato direttamente
-- ad un campo della tabella
function Rectangle.area ()
-- accesso alla variabile 'Rectangle'
return Rectangle.larghezza * Rectangle.altezza
end
-- primo test
print(Rectangle.area()) --> stampa 84, OK
print(Rectangle.height) --> stampa 7, OK
Ci accorgiamo presto che questa implementazione basata sulle tabelle è difettosa in quanto non rispetta l'indipendenza degli oggetti rispetto al loro nome e infatti il prossimo test fallisce:
-- ancora la prima implementazione
Rectangle = {width = 12, height = 7}
-- un metodo dell'oggetto
function Rectangle.area ()
-- accesso alla variabile 'Rectangle' attenzione!
local l = Rectangle.larghezza
local a = Rectangle.altezza
return l * a
end
-- secondo test
r = Rectangle -- creiamo un secondo riferimento
Rectangle = nil -- distruggiamo il riferimento originale
print(r.width) --> stampa 12, OK
print(r.area()) --> errore!
Il problema sta nel fatto che nel metodo 'area()' compare il particolare riferimento alla tabella 'Rectangle' che invece deve poter essere qualunque. La soluzione non può che essere l'introduzione del riferimento all'oggetto come parametro esplicito nel metodo stesso, ed è la stessa utilizzata --- in modo nascosto ma vedremo che è possibile nascondere il riferimento anche in Lua --- dagli altri linguaggi di programmazione che supportano gli oggetti.
Secondo quest'idea dovremo riscrivere il metodo 'area()' in questo modo (in Lua il riferimento esplicito all'oggetto deve chiamarsi self pertanto abituiamoci fin dall'inizio a questa convenzione così da poter generalizzare la validità del codice):
-- seconda tentativo
Rettangolo = {larghezza=12, altezza=7}
-- il metodo diviene indipendente dal particolare
-- riferimento all'oggetto:
function Rettangolo.area ( self )
return self.larghezza * self.altezza
end
-- ed ora il test
myrect = Rettangolo
Rettangolo = nil -- distruggiamo il riferimento
print(myrect.larghezza) --> stampa 12, OK
print(myrect.area(myrect)) --> stampa 84, OK
-- funziona!
Fino a ora abbiamo costruito l'oggetto sfruttando le caratteristiche della tabella e la particolarità che consente di assegnare una funzione ad una variabile. Da questo momento entra in scena l'operatore ':' nella chiamata di funzione. Si tratta di zucchero sintattico ovvero una sorta di aiuto fornito dal compilatore in questo caso per rendere il passaggio del riferimento.
L'operatore ':' fa in modo che le seguenti due espressioni siano perfettamente equivalenti anche se le rende differenti dal punto di vista concettuale agli occhi del programmatore:
-- forma classica:
myrect.area(myrect)
-- forma implicita
-- (self prende lo stesso riferimento di myrect)
myrect:area()
Questo operatore è il primo nuovo elemento di Lua introdotto per supportare la programmazione orientata agli oggetti. Se si accede a un metodo memorizzato in una tabella con l'operatore due punti ':' anziché con l'operatore '.' l'interprete Lua aggiungerà implicitamente un primo parametro con il riferimento alla tabella stessa a cui assegnerà il nome di 'self'.
Metatabelle
Il linguaggio Lua si fonda sull'essenzialità tanto che supporta la programmazione ad oggetti utilizzando quasi esclusivamente le proprie risorse di base senza introdurre nessun nuovo costrutto. In particolare Lua implementa gli oggetti utilizzando la tabella l'unica struttura dati disponibile nel linguaggio, e particolari funzionalità dette metatabelle e metametodi.
Il salto definitivo nella programmazione \textsc{oop} consiste nel poter costruire una \emph{classe} senza ogni volta assemblare i campi ed i metodi ma introducendo un qualcosa che faccia da stampo per gli oggetti.
Abbiamo fatto cenno a Java in cui esiste la parola chiave \texttt{class} per la definizione di nuove classi, ma in Lua, dove la semplicità è sempre importante e dove devono continuare ad essere supportati più stili di programmazione, non esistono né nuove parole chiave né una sintassi specifica.
In Lua l'unico meccanismo disponibile per compiere questo ultimo importante passo consiste nelle metatabelle, normali tabelle contenenti funzioni dai nomi prestabiliti che vengono chiamate quando si verificano particolari eventi come l'esecuzione di un'espressione di somma tra due tabelle con l'operatore '+'. Ogni tabella può essere associata a una metatabella e questo consente di creare degli insiemi di tabelle che condividono una stessa aritmetica.
I nomi di queste funzioni particolari dette metametodi iniziano tutti con un doppio trattino basso, per esempio nel caso della somma sarà richiesta la funzione __add() della metatabella associata alle due tabelle addendo --- e se non esiste verrà generato un errore.
Il metametodo più semplice di tutti è __tostring(). Esso viene invocato se una tabella è data come argomento alla funzione 'print()' per ottenere il valore stringa effettivo da stampare.
Se non esiste una metatabella associata con questo metametodo verrà stampato l'indirizzo di memoria della variabile:
-- un numero complesso
complex = {real = 4, imag = -9}
print(complex) --> stampa: 'table: 0x9eb65a8'
-- un qualcosa di più utile: metatabella in sintassi
-- anonima con il metametodo __tostring()
mt = {}
mt.__tostring = function (c)
local r = string.format("%0.2f", c.real)
if c.imag == 0 then -- numero reale
return "("..r..")"
end
-- numero complesso
local i = string.format("%0.2f", c.imag)
return "("..r..", "..i..")"
end
-- assegnazione della metatabella mt a complex
setmetatable(complex, mt)
-- riprovo la stampa
print(complex) --> stampa '(4.00, -9.00)'
Il metametodo '__index'
Il metametodo che interessa la programmazione a oggetti in Lua è '__index'. Esso interviene quando viene chiamato un campo di una tabella che non esiste e che normalmente restituirebbe il valore 'nil'. Un esempio di codice chiarirà il meccanismo:
-- una tabella con un campo 'a'
-- ma senza un campo 'b'
t = {a = 'Campo A'}
print(t.a) --> stampa 'Campo A'
print(t.b) --> stampa 'nil'
-- con metatabella e metametodo
mt = {}
mt.__index = function ()
return 'Attenzione: campo inesistente!'
end
-- assegniamo 'mt' come metatabella di 't'
setmetatable(t, mt)
-- adesso riproviamo ad accedere al campo b
print(t.b) --> stampa 'Attenzione: campo inesistente!'
Tornando all'oggetto 'Rettangolo' riscriviamo il codice creando adesso una tabella che assumerà il ruolo concettuale di una vera e propria classe:
-- una nuova classe Rettangolo (campi):
Rettangolo = {larghezza=10, altezza=10}
-- un metodo:
function Rettangolo:area()
return self.larghezza * self.altezza
end
-- creazione metametodo
Rettangolo.__index = Rettangolo
-- un nuovo oggetto Rettangolo
r = {}
setmetatable(r, Rettangolo)
print( r.larghezza ) --> stampa 10, Ok
print( r:area() ) --> stampa 100, Ok
Queste poche righe di codice racchiudono il meccanismo un po' tortuoso della creazione di una nuova classe in Lua: abbiamo infatti assegnato a una nuova tabella 'r' la metatabella con funzione di classe Rettangolo. Quando viene richiesta la stampa del campo 'larghezza' poiché tale campo non esiste nella tabella vuota 'r' verrà eseguito il metametodo '__index' nella metatabella associata che è appunto la tabella 'Rettangolo'.
A questo punto il metametodo restituisce semplicemente la tabella 'Rettangolo' stessa e questo fa sì che divengano disponibili tutti i campi e i metodi in essa contenuti. Il campo 'larghezza' e il metodo 'area()' del nuovo oggetto 'r' sono in realtà quelli definiti nella tabella 'Rettangolo'.
Se volessimo creare invece un rettangolo assegnando direttamente la dimensione dei lati dovremo semplicemente crearli con i nomi previsti dalla classe: 'larghezza' e 'lunghezza'. Il metodo 'area()' sarà ancora caricato dalla tabella 'Rettangolo' ma i campi numerici con le nuove misure dei lati saranno quelli interni dell'oggetto 'r' poiché semplicemente esistono e perciò non sarà presa in considerazione la metatabella. Questa costruzione funziona perfettamente ma lascia al programmatore una situazione di scomodità che deriva dal progetto stesso di Lua e che può essere in parte sanata con l'introduzione del costrutture come vedremo meglio poi.
Le chiamate alle metatabelle e ai metametodi complicano la comprensione del funzionamento del meccanismo di stampa, compito della classe. Può sembrare che ciò influisca negativamente nella scrittura di programmi ad oggetti in Lua. Tuttavia quello che è importante è comprendere il meccanismo delle metatabelle svolto dietro le quinte, poiché in fase di utilizzo degli oggetti, il linguaggio ci apparirà concettualmente simile a una classe.
Il costruttore
Le cose da ricordare di scrivere nel codice sono l'impostazione del metametodo '__index' con la tabella di classe e l'esecuzione della funzione interna 'setmetatable()' per impostare la tabella dell'oggetto stessa come metatabella.
Riproponendo ancora il problema di rappresentare il concetto di rettangolo completiamo il quadro introducendo quello che adesso ci appare come un normale metodo ma che concettualmente assumerà il ruolo di costrutture della classe che chiameremo con il nome convenzionale 'new()'. Il lavoro che dovrà svolgere sarà quello di inizializzare i campi argomento in una delle tante modalità possibili una volta effettuato l'eventuale controllo di validità degli argomenti.
Il codice completo della classe 'Rettangolo' è il seguente:
-- nuova classe Rettangolo (campi con valore di default)
Rettangolo = {larghezza = 1, altezza = 1}
-- metametodo
Rettangolo.__index = Rettangolo
-- metodo di classe
function Rettangolo:area()
return self.larghezza * self.altezza
end
-- costruttore di classe
function Rettangolo:new( o )
-- creazione nuova tabella
-- se non ne viene fornita una
o = o or {}
-- assegnazione metatabella
setmetatable(o, self)
-- restituzione riferimento oggetto
return o
end
-- codice utente ------------------
r = Rettangolo:new{larghezza=12, altezza=2}
print(r.larghezza) --> stampa 12, Ok
print(r:area()) --> stampa 24, Ok
q = Rettangolo:new{larghezza=12}
print(q:area()) --> stampa 12, Ok
Il costruttore accetta una tabella come argomento, altrimenti ne crea una vuota e la restituisce non appena ne ha assegnato la metatabella. In questo modo una tabella qualsiasi entra a far parte della famiglia 'Rettangolo'.
Il costruttore passa al metodo 'new()' il riferimento implicito a 'Rettangolo' grazie all'uso dell'operatore ':' per cui 'self' punterà a 'Rettangolo'.
Quando viene passata una tabella con uno o due campi sulle misure dei lati al costruttore, l'oggetto disporrà delle misure come valori interni effettivi, cioè dei parametri indipendenti che costituiscono il suo stato interno.
Lo sviluppatore può fare anche una diversa scelta, quella per esempio di considerare la tabella argomento del costruttore come semplice struttura di chiavi/valori da sottoporre al controllo di validità e poi includere in una nuova tabella con modalità e nomi che riguardano solo l'implemenazione interna della classe.
Questa volta un cerchio
Per capire ancor meglio i dettagli e renderci conto di come funziona il meccanismo nascosto e automatico delle metatabelle, costruiamo una classe 'Cerchio' che annoveri fra i suoi metodi uno che modifichi il valore del raggio aggiungendo una misura specificata:
Cerchio = {radius=0}
Cerchio.__index = Cerchio
function Cerchio:area()
return math.pi*self.radius^2
end
function Cerchio:addToRadius(v)
self.radius = self.radius + v
end
function Cerchio:__tostring()
local frmt = 'Sono un cerchio di raggio %0.2f.'
return string.format(frmt, self.radius)
end
-- il costruttore attende l'eventuale valore del raggio
function Cerchio:new(r)
local o = {}
if r then
o.radius = r
end
setmetatable(o, self)
return o
end
-- codice utente ----------------------
o = Cerchio:new()
print(o) --> stampa 'Sono un cerchio di raggio 0.00'
o:addToRadius(12.342)
print(o) --> stampa 'Sono un cerchio di raggio 12.34'
print(o:area()) --> stampa '478.54298786'
Nella sezione del codice utente viene dapprima creato un cerchio senza fornire alcun valore per il raggio. Ciò significa che quando stampiamo il valore del raggio con la successiva istruzione otteniamo 0 che è il valore di default del raggio dell'oggetto 'Cerchio' per effetto della chiamata a '__index' della metatabella.
Fino a questo momento la tabella dell'oggetto 'o' non contiene alcun campo 'radius'. Cosa succede allora quando viene lanciato il comando 'o:addToRadius(12.342)'?
Il metodo 'addToRadius()' contiene una sola espressione. Come da regola viene prima valutata la parte a destra ovvero 'self.radius + v'. Il primo termine assume il valore previsto in 'Cerchio' --- quindi zero --- grazie al metametodo, e successivamente il risultato della somma uguale all'argomento 'v' è memorizzato nel campo 'o.radius' che viene creato effettivamente solo in quel momento per poi eventualmente venir utilizzato successivamente in lettura o scrittura.
Ereditarietà
Il concetto di ereditarietà nella programmazione a oggetti consiste nella possibilità di derivare una classe da un'altra per specializzarla.L'operazione di derivazione incorpora automaticamente nella sottoclasse tutti i campi e i metodi della classe base. Dopodiché si implementano o si modificano i metodi della classe derivata creando una gerarchia di oggetti.
In Lua l'operazione di derivazione consiste nel creare un oggetto con il costruttore della classe base. Si ottiene un riferimento a una tabella che potrà essere popolato di nuovi metodi o campi. Vediamo un esempio semplice:
-- classe base
Sportivo = {}
-- costructor
function Sportivo:new(t)
t = t or {}
setmetatable(t, self)
self.__index = self
return t
end
-- base methods
function Sportivo:set_name(name)
self.name = name
end
function Sportivo:print()
print("'"..self.name.."'")
end
-- derivazione
Schermista = Sportivo:new()
-- specializzazione classe derivata
-- nuovo campo
Schermista.rank = 0
function Schermista:add_to_rank(points)
self.rank = self.rank + (points or 0)
end
function Schermista:set_weapon(w)
self.weapon = w or ""
end
-- overriding method
function Schermista:print()
local fmt = "'%s' weapon->'%s' rank->%d"
print(string.format(fmt,
self.name,
self.weapon,
self.rank
))
end
-- test
s = Sportivo:new{name="Gianni"}
s:print() --> stampa 'Gianni' OK
-- il metodo costruttore new() è quella della classe base!
f = Schermista:new{name="Fencing tiger", weapon="Foil"}
f:add_to_rank(45)
f:print() --> stampa 'Fencing tiger' weapon->'Foil' rank->45
-- chiamata a un metodo della classe base
f:set_name("Amedeo")
f:print() --> stampa 'Amedeo' weapon->'Foil' rank->45
Continua tutto a funzionare per via della ricerca effettuata dal metametodo '__index' che funziona a ritroso fino alla classe base se viene chiamato un campo che non si trova ne nella tabella dell'oggetto ne nella tabella della classe derivata.
E ora gli esercizi...
1 - Aggiungere alla classe 'Rettangolo' vista nel testo il metodo 'print()' che stampi in console con la AsciiArt il rettangolo dalle dimensioni corrispondenti a altezza e larghezza con la corrispondenza 1 unità = 1 carattere.
Si usi il carattere '+' per gli spigoli e i caratteri '-' e '|' per disegnare i lati. Utilizzarre le funzioni di libreria string.rep() e string.format().
2 - Creare una classe corrispondente al concetto di numero complesso e implementare le quattro operazioni tramite metodi (riferimento matematico qui). Aggiungere anche il metodo print() per stampare il numero complesso e poter controllare i risultati di operazioni di test.
3 - ideare una classe base e una classe derivata dandone un'implementazione.
Riassunto della puntata
Abbiamo affrontato la programmazione a oggetti in Lua presentando l'operatore ':' e la tecnologia delle metatabelle e dei metametodi che rendono le usuali tabelle di Lua vere e proprie classi.Implementare la propria libreria Lua con il paradigma della programmazione a oggetti offre sia allo sviluppatore sia all'utente una maggiore intuitività del linguaggio.
La prossima volta, per l'ultima puntata :-( ci dedicheremo a esempi applicativi.