lunedì 22 giugno 2009

Programmazione a oggetti in Perl

Perl è un linguaggio molto elegante e potente, che viene spesso insegnato per evidenziare concetti un po' più evoluti riguardo lo scripting.
Pur supportando la programmazione ad oggetti, Perl non fornisce alcun costrutto particolare per la realizzazione di classi e tipi di dato astratto. Si può paragonare Perl a un C che viene programmato ad-hoc per implementare un comportamento ad oggetti.
Imparare a programmare Perl a oggetti è molto interessante, soprattutto perché permette di vedere effettivamente come molti compilatori a oggetti si comportano. Inoltre gran parte della libreria Perl/CPAN è orientata agli oggetti, e quindi è indispensabile avere nozioni di OOP. Non è comunque facile trovare una guida sulla programmazione OOP in Perl, perciò ho deciso di scrivere questo articolo introduttivo. Da notare che farò riferimento a versioni di Perl inferiori alla 6, dove la scrittura di classi e oggetti cambierà grazie alla presenza di apposite parole chiave.

Il seguente elenco mostra i punti per me fondamentali da tenere a mente quando si vuole implementare una classe e di conseguenza usare degli oggetti.

Gli oggetti possono essere acceduti solo per riferimento.

Ogni istanza di oggetto viene mantenuta in un riferimento, in maniera simile a come avviene in Java.

Un riferimento ad un oggetto è un normale riferimento scalare
.
Come i riferimenti alle strutture dati native (hash, array) sono contenuti in variabili scalari, così pure i riferimenti agli oggetti sono mantenuti in variabili scalari.

Una classe corrisponde ad uno spazio dei nomi, e quindi si implementa tramite un package.

In Perl una classe è una entità sul quale sono richiamati dei metodi. Per la precisione, un metodo di classe è un metodo al quale viene passato un parametro che indica lo spazio dei nomi nel quale si agisce. Ecco quindi che assegnare un metodo ad una classe corrisponde ad inserire un metodo in un determinato package: il metodo potrà essere invocato solo nell'ambito di quello spazio dei nomi (classe/package).

Se la struttura dati dell'oggetto è complessa, allora essa deve essere realizzata tramite un array o un hash, e il riferimento all'oggetto diventa il riferimento all'array/hash.
Questo punto va chiarito bene: in Perl esistono solo tre tipi di variabili (scalari, array, hash) e una classe sarà presumibilmente composta da diverse variabili di tale tipo. Dovendo però usare un riferimento ordinario (si vedano i punti precedenti) per l'oggetto, si ha che tale riferimento deve essere il punto di ingresso di una struttura più complessa. Di conseguenza, il riferimento all'oggetto deve essere un riferimento ad un array/hash che contenga tutte le altre variabili di istanza. Per meglio comprendere, se in Java una definizione di classe Person potrebbe essere così strutturata:
public class Person{
private String name = null;
private String surname = null;
}

In Perl non potrebbe avere la definizione duale, ossia:

package Person;

$name = undef();
$surname = undef();
poiché, così facendo, si avrebbe che lo spazio dei nomi della persona (Person) contiene due variabili scalari, ed è impossibile ritornare un singolo riferimento che fornisca accesso ad entrambe le variabili. E' quindi necessario incapsulare le variabili di classe all'interno di un unico contenitore e fornire accesso attraverso tale contenitore:

package Person;
...
$this = { name => undef(),
surname => undef(),
};


In questo modo la variabile $this consente accesso ai valori di name e surname, ed è possibile ritornare all'esterno un singolo riferimento a $this.
Da notare che la seguente traduzione non funziona come ci si aspetta:

package Person;

$name = undef();
$surname = undef();

$this = { name => \$name,
surname => \$surname,
};

Infatti così facendo si ha l'esistenza di uniche variabili $name e $surname e di conseguenza i riferimenti di ogni oggetto creato puntano alle stesse variabili globali (all'interno del package) e quindi agli stessi valori. La soluzione di cui sopra è invece appropriata per variabili statiche (ossia condivise all'interno di ogni classe).

Un metodo di classe o di oggetto riceve sempre in automatico un primo argomento che specifica la classe o l'istanza
.
Questa cosa non dovrebbe stupire particolarmente, poiché è già quello che avviene nei normali compilatori ad oggetti: il primo parametro di un metodo di istanza è il puntatore/riferimento all'istanza stessa. E' in questo modo che i compilatori predispongono la variabile this. In altre parole, il metodo Java seguente:
public class MyClass{
public void instanceMethod(String arg1, int arg2){
this.stringVariable = arg1;
this.integerVariable = arg2;
}
}

viene mutato dal compilatore silenziosamente nel seguente:

public class MyClass{
public void instanceMethod(MyClass this, String arg1, int arg2){
this.stringVariable = arg1;
this.integerVariable = arg2;
}
}

Da notare che il primo parametro, di nome simbolico this, è un riferimento all'oggetto stesso. In altre parole i metodi di istanza ottengon come parametro la zona di memoria (o meglio il riferimento a tale zona) dove risiede l'oggetto sul quale si agisce. Perl segue lo stesso percorso: il primo argomento di un metodo di istanza contiene il riferimento all'oggetto stesso, di modo che il metodo possa essere scritto come:

sub instanceMethod{
my ($this, $arg1, $arg2) = @_;
...
}
La differenza è che qui è compito del programmatore ricordarsi che il primo argomento è il riferimento all'istanza; il compilatore Perl infatti fa solo la prima parte del lavoro (passare al metodo il riferimento come primo argomento).
Quanto sopra per quello che riguarda gli oggetti, ma per le classi?
In un linguaggio OOP come Java/C++ i metodi di classe (es. static) non necessitano di nessun argomento particolare o di modifiche da parte del compilatore. Infatti quello che avviene è che viene tenuta in memoria la tabella di dispatching dei metodi e l'invocazione di un metodo corrisponde al salto all'indirizzo fornito da questa tabella. Ma il metodo non ha bisogno di sapere se appartiene ad una classe oppure no, come non ha bisogno di sapere a che classe appartiene poiché per forza di cose, se non è di istanza, deve essere di classe.
In Perl questo non è vero: essendo una classe implementata tramite un package, i metodi all'interno del package potrebbero essere di istanza, oppure di classe, oppure nessuno dei due (normali metodi globali). Perl allora fornisce un modo per informare un metodo che è stato invocato in un contesto di classe (ossia come se fosse un metodo di classe): questo metodo riceverà infatti come primo argomento il nome (stringa) della classe/package al quale appartiene.
Riassumendo quindi, tutti i metodi di classe o di oggetto in Perl ricevono come primo argomento una variabile che indica il nome della classe (metodo di classe) o il riferimento all'oggetto stesso (metodo di oggetto) e non ricevono nulla se sono chiamati in un normale contesto di package.

In Perl non esiste il concetto di costruttore (e di distruttore)
.
Siccome Perl non fornisce nessuna parole chiave per definire una classe, i metodi costruttori (e distruttori) non esistono. Questo non significa che non si possano creare dei metodi appositi per l'inizializzazione delle variabili di un oggetto. Tali metodi dovranno ovviamente restituire al chiamante il riferimento alla struttura dati dell'oggetto.

Un metodo usato come costruttore non deve avere lo stesso nome della classe
.
Siccome questi metodi non sono dei veri e propri costruttori, non devono seguire le regole standard dei costruttori Java e C++: essi possono (o meglio devono) avere un tipo di ritorno (solitamente il riferimento alla struttura dati dell'oggetto) e possono avere un nome non vincolante (solitamente new).

In Perl non esiste l'operatore new
.
Quando si incontra qualcosa che assomiglia all'operatore new per l'allocazione di un oggetto, si sta in effetti osservando la chiamata di un metodo di nome new (si veda il punto precedente).
Il compilatore passa automaticamente ad un metodo il primo argomento (nome della classe o riferimento ai dati) quando si usa l'operatore freccia.
Quando la chiamata ad un metodo avviene attraverso l'operatore freccia, il compilatore passa come primo argomento del metodo l'oggetto lvalue su cui viene applicato l'operatore freccia. In altre parole:
LVALUE->method(arg1, arg2, arg3);

corrisponde a:
method(LVALUE, arg1, arg2, arg3);
Questo significa quindi che usando l'operatore freccia su i metodi di un package si ha automaticamente una chiamata a metodo di classe, mentre usando l'operatore freccia su dei riferimenti ad oggetti si ha una chiamata ad un metodo di istanza. Quindi ad esempio, per creare un nuovo oggetto, si può invocare il metodo new nei due modi equivalenti:
$objectReference = Person->new('Luca', 'Ferrari'); # sintassi di classe
$objectReference = new( Person, 'Luca', 'Ferrari'); # sintassi di package


e analogamente, una invocazione di metodo di oggetto può essere scritta come:

$objectReference->setName('Giovanni'); # sintassi di oggetto
Person::setName( $objectReference, 'Giovanni'); # sintassi di package

Perl deve sapere quali riferimenti puntano ad un oggetto e quali no.
Il costruttore di un oggetto non deve solo inizializzare l'oggetto e il suo stato, ma deve anche informare Perl che il riferimento non è un normale riferimento ma un collegamento ad un oggetto. Questo avviene tramite l'operatore bless, che accetta il riferimento ad una struttura dati e la classe/package a cui la struttura dati appartiene. L'operatore memorizza in una tabella il collegamento fra il riferimento e il nome del package/classe a cui è associato, in modo da poter individuare a quale classe il riferimento appartiene. In particolare, in seguito al blessing, l'operatore ref applicato ad un riferimento di oggetto restituisce il nome della classe per la quale è stato fatto il blessing.
Sulla base di questa considerazione, il template generale per un costruttore di oggetto è il seguente:

sub new
{
my ($class, @args) = @_;
# inizializza i valori dell'oggetto
$this = { arg1 => $args[0],
arg2 => $args[1],
...
};

bless( $this, $class );
return $this;
}





Sulla base di tutti i punti di cui sopra, è possibile paragonare la creazione di una classe/oggetto fra un linguaggio OOP (es. Java) e Perl. Supponiamo di avere la seguente classe Java:

public class Person{
public static int personCounter = 0;

private String nome = null;
private String cognome = null;

public Person(String nome, String cognome){
this.nome = nome;
this.cognome = cognome;
Person.personCounter++;
}


public String getNome(){
return this.nome;
}

public String getCognome(){
return this.cognome;
}
}

e di volerla tradurre in Perl. La sua definizione diviene la seguente:

#!/usr/bin/perl

package Person;

# variabile statica (la stessa
# condivisa fra tutte le istanze)
$personCounter = 0;


sub new
{
my ($class, $name, $surname) = @_;

# creazione nuovo riferimento ad hash
# con i dati (stato) dell'oggetto
$this = { name => $name,
surname => $surname,
};



# associo il riferimento alla classe
bless( $this, $class );

# incremento il contatore di istanze create
$personCounter++;

print "\nCreazione di un nuovo oggetto di tipo $class (numero $personCounter)\n";

# restituisco il riferimento appena creato
return $this;
}




sub getNome
{
my ($this) = @_;

# restituisco il dato richiesto
return $this->{name};
}


sub getCognome
{
my ($this) = @_;

# restituisco il dato richiesto
return $this->{surname};
}




che può essere testato con il seguente main:

package main;

$person1 = Person->new('Luca','Ferrari');
$person2 = Person->new('Paperon','De Paperoni');

print "\nPersona con nome ", $person1->getNome();
print "\nAltra persona con nome ", $person2->getNome(), " ", $person2->getCognome();
print "\n\n";


che produce il seguente output:

Creazione di un nuovo oggetto di tipo Person (numero 1)

Creazione di un nuovo oggetto di tipo Person (numero 2)

Persona con nome Luca
Altra persona con nome Paperon De Paperoni


Ereditarietà e Polimorfismo
L'ereditarietà in Perl viene implementata in modo molto semplice: è disponibile un array globale all'interno dello spazio dei nomi di un package, denominato ISA, che contiene le classi/package dalle quali si vuole ereditare. L'idea quindi è semplice: ogni package definisce una lista di ulteriori package in cui cercare dati e/o metodi. Quando Perl non trova un attributo nel package specificato inizia ad esplorare (sequenzialmente) l'array ISA al fine di trovare in ogni classe specificata il simbolo, e quindi lo utilizza.
Per meglio comprendere questo concetto, si consideri di estendere la classe Person con una classe Male che implementa una persona di sesso maschile:


package Male;

@ISA=(Person);

sub new
{
my ($class, $name, $surname) = @_;

# creazione nuovo riferimento ad hash
# con i dati (stato) dell'oggetto
$this = { name => $name,
surname => $surname,
};



# associo il riferimento alla classe
bless( $this, $class );

# incremento il contatore di istanze create
$personCounter++;

print "\nCreazione di un nuovo oggetto di tipo $class (numero $personCounter)\n";

# restituisco il riferimento appena creato
return $this;
}


Come si può notare la definizione del costruttore è praticamente la stessa della classe Person, addirittura si referenzia un simbolo ($personCounter) che qui non è stato ancora definito. Ora se un programma crea un nuovo oggetto di tipo Male:

$maschio = Male->new( $person1->getNome(), $person1->getCognome() );
print "\nPersona maschio con nome ", $maschio->getNome(), " ", $maschio->getCognome();


l'output di esecuzione è del tutto simile a quello del programma precedente, e addirittura la variabile $personCounter inizia il suo conteggio come fosse separata dalla classe Person (ovvero come fosse stata definita singolarmente di classe). Tutta la magia viene fatta nella ricerca dei simboli, che anziché passare per una vtable passa per l'array ISA del package corrente (ogni package può ridefinire il proprio @ISA).

La ridefinizione dei metodi è automatica, quindi se si ridefinisce il seguente metodo nella classe Male si ha che la chiamata a getNome() verrà risolta da tale metodo:

package Male;

@ISA=(Person);

sub new
{
my ($class, $name, $surname) = @_;

# creazione nuovo riferimento ad hash
# con i dati (stato) dell'oggetto
$this = { name => $name,
surname => $surname,
};



# associo il riferimento alla classe
bless( $this, $class );

# incremento il contatore di istanze create
$personCounter++;

print "\nCreazione di un nuovo oggetto di tipo $class (numero $personCounter)\n";

# restituisco il riferimento appena creato
return $this;
}



sub getNome
{
my ($this) = @_;

return $this->{name} ." (maschio) ";
}


E' inoltre possibile riferirsi alle proprietà della classe base mediante l'uso di SUPER. Si presti attenzione poiché qui super è un operatore che si preoccupa di salire di un livello nella scala dei riferimenti, ma il nome della classe deve essere sempre specificato come primo argomento: SUPER deve quindi essere usato in combinazione con l'operatore freccia.

package Male;

@ISA=(Person);

sub new
{
my ($class, $name, $surname) = @_;

$this->SUPER::new( $name, $surname );


# associo il riferimento alla classe
bless( $this, $class );

# incremento il contatore di istanze create
$personCounter++;

print "\nCreazione di un nuovo oggetto di tipo $class (numero $personCounter)\n";

# restituisco il riferimento appena creato
return $this;
}



sub getNome
{
my ($this) = @_;

return $this->SUPER::getNome() ." (maschio) ";
}


Si noti che quando si usa SUPER la parte a destra viene sempre invocata con la sintassi di package (::) mentre quella di sinistra con la sintassi ad oggetto (->).


Per concludere
Riassumendo quindi si ha che in Perl la programmazione ad oggetti viene attualmente implementata tenendo separati i metodi e i dati, in modo piuttosto esplicito. Il riferimento ai dati rappresenta il riferimento all'oggetto (e quindi al suo stato), mentre l'invocazione dei metodi avviene grazie all'operatore freccia, che impone al compilatore di passare l'entità di sinistra (il riferimento o il nome della classe) come primo argomento. Grazie poi ad una tabella dei simboli gestita tramite l'operatore bless si ha l'associazione fra riferimento ai dati e classe, e questo consente, in combinazione all'array @ISA di sfruttare anche l'ereditarietà e il polimorfismo.

2 commenti:

Daniele ha detto...

ottima guida, grazie mille!
Una domanda: quando parli dell'operatore SUPER, ho notato che non è necessario il successivo 'bless' sul riferimento. Sbaglio?

Luca Ferrari ha detto...

Il bless viene usato per informare Perl che un riferimento contiene un oggetto, e quindi che l'operatore freccia applicato a quel riferimento deve essere trattato in un modo opportuno. SUPER identifica un package (infatti la sintassi di invocazione è con ::) e quindi il bless è necessario sul riferimento all'oggetto ma non su SUPER.