Introduzione al Perl Object Environment (POE)/1

Se vi siete sempre cimentati con script di shell, applicazioni web o semplici CGI — e questa è una sorte comune a molti programmatori Perl — allora siete probabilmente abituati a concepire i vostri programmi come una sequenza di operazioni che avvengono l’una dopo l’altra in maniera lineare, o al più pilotate dallo stato del sistema e dall’input fornito al programma.

Ci sono però tanti casi in cui un simile approccio non va bene: ad esempio tutto l’insieme delle applicazioni GUI, che devono essere reattive alle azioni dell’utente, e per le quali di conseguenza non è possibile individuare a priori una sequenza di operazioni da svolgere: l’impredicibilità dell’agire dell’utente, e la quantità di attori che devono modificare il proprio stato in base ad esse, impedisce — o rende molto difficile — usare il medesimo approccio che normalmente governa i programmi tradizionali. Altra categoria che può trarre beneficio da un approccio ad eventi è quella dei software che devono implementare protocolli di comunicazione con agenti esterni, come quelli di networking.

POE è uno strumento adatto allo sviluppo di questo genere di applicazioni: programmi ad eventi che sanno come comportarsi, che una volta progettati e costruiti vengono gettati nella mischia del mondo esterno, a fare cose interessanti.

Se preferite una definizione meno imprecisa, POE è un framework Perl per la scrittura di applicazioni multitasking basate su eventi. Per i pignoli, si tratta di multitasking “cooperativo”.

La materia non è banale: partiamo con qualche nozione di base, presentiamo un esempio, e vediamo come si mette.

POE: concetti di base

Vediamo dunque quali sono gli strumenti del framework che POE mette a disposizione per realizzare quanto finora accennato. L’oggetto su cui si basa il funzionamento del tutto è il Kernel: è uno smistatore, al quale ci si rivolge, principalmente, per inviare eventi al resto dell’applicazione.

Immediatamente dopo si incontrano le Session, vera e propria pietra angolare di ogni progetto: potremmo dire che scrivere una applicazione POE consiste nel configurare una o più sessioni. Le sessioni sono analoghe ai thread o ai processi in un sistema operativo: ne possono esistere più d’una all’interno di una applicazione, ma istante per istante una sola di esse può avere il controllo del calcolatore (stiamo facendo finta che non esistano i sistemi multiprocessore). In particolare, nel modello cooperativo di POE, il passagggio di controllo deve essere esplicito: è importante per questo motivo progettare le cose in maniera tale che una sessione non possa bloccare il sistema: non essendoci un meccanismo di prelazione l’intera applicazione si blocchererebbe inesorabilmente.

Le sessioni sono insiemi di handler di eventi: a ciascun evento corrisponde una subroutine che si incarica di eseguire una operazione, eventualmente modificando lo stato della sessione, inviando a sua volta altri eventi, oppure provocando side-effect. Le subroutine in questione possono essere definite in molti modi: con riferimenti a procedure definite altrove, specificando una classe che implementa i metodi corrispondenti agli eventi che avvengono, oppure usando subroutine anonime direttamente nella configurazione della sessione. Sarà quest’ultimo il caso degli esempi che descriveremo.

Un esempio

Veniamo ora al primo esempio. Con grande sforzo creativo, scriveremo la versione POE di “Hello world”. Non è molto tipica come applicazione ad eventi, ma sufficiente per mostrare alcuni concetti fondamentali:

use strict;
use warnings;

use POE;

POE::Session->create(
inline_states => {
_start => sub {
print "Hello, world!\n";
}
}
);

POE::Kernel->run();

Come abbiamo anticipato, il grosso del programma consiste nella configurazione della sessione (in questo caso una sola, ma nulla vieta di averne più d’una, anzi, è tipico avere più sessioni che interagiscono fra loro). POE::Session::create è il costruttore. Avvalendoci del paramentro inline_states, stabiliamo che la sessione, ricevuto il segnale _start, deve eseguire la subroutine anonima ad esso legata. _start è un evento speciale che viene mandato alla sessione automaticamente, solo per il fatto di essere stata creata. Infine, con l’ultima istruzione, ci si rivolge al kernel, dicendogli semplicemente che vogliamo che cominci a svolgere la sua attività di smistatore. Per un esempio così semplice, possiamo addirittura descrivere in buon dettaglio il ciclo di vita dell’applicazione:

  1. La sessione viene creata. Il costruttore di POE::Session, durante le varie operazioni di “house-keeping” correlate all’istanziazione della sessione, mette nella coda del kernel un evento _start che deve essere destinato alla sessione appena creata.
  2. Il kernel riceve l’ordine di partire.
  3. Il kernel vede che nella coda di eventi da smistare ce n’è uno chiamato _start, destinato alla sessione che abbiamo creato, e quindi lo invia.
  4. La sessione, addestrata a rispondere agli eventi _start, sa come comportarsi, ed esegue quindi la subroutine che ad esso avevamo legato. La stringa "Hello, world\n" viene stampata a video. Al termine dell’esecuzione della subroutine il controllo viene automaticamente restituito al kernel.
  5. Il kernel prende in esame la coda degli eventi da smistare, ma non ne trova alcuno. Perciò l’applicazione termina.

Proviamo a complicare leggermente il programma, usando questa volta due sessioni:

use strict;
use warnings;

use POE;

POE::Session->create(
inline_states => {
_start => sub {
print "Hello, world!\n";
}
}
);

POE::Session->create(
inline_states => {
_start => sub {
print "Io sono l'altra sessione!\n";
}
}
);

POE::Kernel->run();

Prima di lanciare il programma, provate a stilare l’elenco delle operazioni che vengono svolte eseguendolo, come abbiamo fatto per il precedente.

Comunicazione tra sessioni

Fin qui nulla di rivoluzionario. Penso anzi che quello appena visto sia l’Hello World più inutilmente complicato che abbiate mai incontrato. Per costruire applicazioni più interessanti introduciamo quindi un’altra tecnica basilare: la comunicazione tra sessioni. Rivolgendoci al kernel è infatti possibile fare in modo che sessioni distinte si spediscano eventi, in maniera tale che il ciclo di vita di una possa influire sullo stato dell’altra.

Possiamo cioè rivolgerci al kernel come se fosse un postino, chiedendogli di inoltrare ad una data sessione (di cui dovremo conoscere il nome) un determinato messaggio, e se vogliamo dei parametri aggiuntivi. Per dare un nome ad una sessione si usa il metodo alias_set() della classe POE::Kernel. È possibile attribuire più di un alias alla stessa sessione.

La chiamata di metodi del kernel dall’interno di una sessione è una cosa che non abbiamo ancora visto; quello che succede è che ciascuna delle subroutine che abbiamo usato sinora per associare un comportamento ad un evento viene implicitamente chiamata con una serie di parametri standard. Questi, come saprà chi già conosce un po’ di Perl, sono disponibili nell’array speciale @_. POE definisce una serie di costanti simboliche che ci aiutano a recuperare in maniera più leggibile gli elementi cui siamo interessati. Vediamo i più importanti (si trova un elenco più esaustivo nella documentazione di POE::Session):

Tabella 1. Costanti simboliche per la lettura di parametri
Costante Descrizione
KERNEL Un riferimento al kernel.
SENDER Riferimento al mittente di un evento. Si può usare al posto dell’alias, qualora questo non fosse stato specificato.
HEAP Ogni sessione dispone di uno spazio dove mantenere valori. Esso viene automaticamente inizializzato come un hash, ed è separato da quello delle altre sessioni (proseguendo vedremo un esempio d’uso).
ARG0 .. ARGN Parametri aggiuntivi inviati insieme all’evento.

Qualche programmatore Perl potrebbe aspettarsi che un così gran numero di parametri venga passato come sequenza di coppie nome-valore, in modo da poterli copiare in un hash e usarli da lì; gli autori di POE hanno invece deciso di passare solo i valori, in un ordine fissato: i valori delle costanti qui sopra sono gli indici (in @_) dei vari parametri. Questo metodo di passaggio parametri, benché possa apparire strano, ha vantaggi sia in termini di occupazione di memoria, che di tempo di esecuzione.

Possiamo quindi ottenere una copia del riferimento all’oggetto kernel scrivendo

my $kernel = $_[ KERNEL ];

Per associare un nome simbolico ad una sessione, dunque, scriviamo:

my $kernel = $_[ KERNEL ];
$kernel->alias_set( 'Alice' );

Oppure, se non abbiamo bisogno di rivolgerci ancora al kernel successivamente:

$_[ KERNEL ]->alias_set( 'Alice' );

Sempre al kernel possiamo chiedere di inviare un evento ad una sessione di cui conosciamo l’alias, o di cui possediamo un riferimento. Questo avviene grazie al metodo post(), che necessita di un destinatario, del nome di un evento, e opzionalmente di una lista di parametri. Possiamo dunque scrivere:

$_[ KERNEL ]->post( 'Alice', 'evento_1' );

E la sessione precedentemente chiamata Alice verrà notificata dell’occorrenza dell’evento chiamato evento_1. Poichè spesso capita che una sessione voglia mandare un evento a se stessa, esiste anche la scorciatoia yield(), usando la quale non si deve specificare il nome del destinatario.

Un altro modo importante per inviare eventi da una sessione a se stessa è l’impostazione di allarmi, con alarm_add().

Raccogliamo tutte le nozioni presentate fino ad ora scrivendo un esempio pratico, nel quale modelliamo Bob e la sua sveglia. Stiamo studiando POE, quindi non sarà una sorpresa scoprire che useremo due sessioni per modellare queste entità.

use strict; 
use warnings;

use POE;

# Sveglia
POE::Session->create(
inline_states => {
_start => sub {
$_[KERNEL]->alias_set( 'alarm_clock' );
},

imposta => sub {
$_[KERNEL]->alarm_add( drin => time() + $_[ ARG0 ] );
print "Sveglia impostata a $_[ARG0] secondi.\n";
},

drin => sub {
print "DRIIIIIN\n";
$_[KERNEL]->post( 'Bob', 'svegliati' );
}
}
);

# Bob dorme
POE::Session->create(
inline_states => {
_start => sub {
$_[KERNEL]->alias_set( 'Bob' );

print "Imposto la sveglia\n";
$_[KERNEL]->post('alarm_clock', 'imposta', 10);

print "Ora mi metto a dormire!\n";
},

svegliati => sub {
print "Sono sveglio!\n";
}
}
);

POE::Kernel->run();
$ perl alarmclock.pl 
Imposto la sveglia
Ora mi metto a dormire!
Sveglia impostata a 5 secondi.
DRIIIIIN
Sono sveglio!

In questo script ci sono un po’ di cose da notare oltre a quelle che abbiamo già precisato. Per creare la sveglia ci siamo basati sul metodo del kernel alarm_set(), che consente di posticipare l’invio di eventi, delegando al kernel stesso il compito di accorgersi di quando è il momento corretto per notificare l’evento. Notiamo inoltre che abbiamo usato post() con parametri aggiuntivi (il tempo, in secondi, passato il quale volevamo essere svegliati).

Un’osservazione che si potrebbe fare è che il codice della sveglia è poco generico: possiamo, certo, impostare l’allarme in maniera parametrica, ma l’evento drin invia l’evento di sveglia sempre e solo a Bob. La soluzione consiste nel memorizzare da qualche parte un riferimento al mittente dell’evento imposta, usandolo successivamente per inviare a lui — e solo a lui — l’evento di sveglia.

Per mantenere valori durante il ciclo di vita di una sessione, ciascuna di esse dispone di uno spazio chiamato heap, che normalmente è un hash. Come abbiamo visto prima, un riferimento allo heap è passato ad ognuno degli handler di eventi di una sessione. Presentiamo quindi il solo codice modificato della sveglia, che ora ricorda chi è stato ad impostarla, e invia l’evento di sveglia solo a lui. Per sapere chi è stato ad impostare la sveglia, usiamo il parametro speciale $_[SENDER]:

POE::Session->create(
inline_states => {
_start => sub {
$_[KERNEL]->alias_set( 'alarm_clock' );
},
imposta => sub {
$_[KERNEL]->alarm_add( drin => time() +$_[ARG0] );
$_[HEAP]->{ set_by } = $_[SENDER];
print "Sveglia impostata a $_[ARG0] secondi.\n";
},
drin => sub {
print "DRIIIIIN\n";
$_[KERNEL]->post( $_[HEAP]->{ set_by } , 'svegliati' );
}
}
);

Sofistichiamo ancora di più aggiungendo altre persone che dormono. Non è un problema, abbiamo gia` visto in precedenza che possiamo creare più sessioni che reagiscono allo stesso modo agli stessi eventi.

Se supponiamo però che solo una delle persone imposti la sveglia, ci troveremmo di fronte ad un modello di sveglia piuttosto irrealistico: sveglierebbe infatti solamente la persona che ha impostato l’allarme. Vogliamo invece trovare un modo che faccia sì che tutti gli interessati al segnale di sveglia lo ricevano, e vengano debitamente buttati giù dal letto. Si tratta di una situazione molto comune nella programmazione ad eventi: la sveglia viene detta broadcaster, e le varie persone dormienti vengono chiamati listener (osserverete che un dormiente listener suona un po’ strano, ma a questo punto mi si è imbizzarrito l’esempio). Ciascuno dei listener stipula un contratto con il broadcaster: “Sono interessato agli eventi di tipo X, per favore mandameli e reagirò di conseguenza”.

Un esempio concreto di situazione di questo genere è quello di una interfaccia grafica, dove l’interazione dell’utente con un controllo deve determinare la variazione di molti altri elementi dell’interfaccia. Se affrontato tradizionalmente, il problema sarebbe di difficile soluzione e porterebbe sicuramente a codice difficile da mantenere. Nell’approccio ad eventi, possiamo limitarci ad aggiungere un event handler all’oggetto che deve reagire ad esso, e notificare al broadcaster che deve mandare anche a quell’oggetto il messaggio.

Tornando a POE, una maniera semplice per realizzare il broadcast di eventi è usare i segnali: si tratta di una feature di POE che permette di intercettare i segnali provenienti dal sistema operativo, ma anche di crearne di propri. Sarà proprio quest’ultima caratteristica che sfrutteremo, implementando un segnale che dal kernel verrà mandato a tutte le sessioni (sui segnali c’è molto di più da sapere: se volete approfondire, leggete la documentazione di POE::Kernel).

use strict;
use warnings;

use POE;

# Sveglia
POE::Session->create(
inline_states => {
_start => sub {
$_[KERNEL]->alias_set( 'alarm_clock' );
},
imposta => sub {
my ($kernel, $sender, $heap, $timeout)
= @_[ KERNEL, SENDER, HEAP, ARG0 ];

if ( not exists $heap->{ set_by } ) {
my ($sender_alias) = $kernel->alias_list( $sender );
$heap->{ set_by } = $sender;
$kernel->alarm_add( drin => time() +$timeout );
print "Sveglia impostata da $sender_alias a $timeout secondi.\n";
}
else {
print "Sveglia gia` impostata.\n";
}
},
drin => sub {
print "DRIIIIIN\n";
$_[KERNEL]->signal( $_[ KERNEL ], 'sveglia_tutti' );
}
}
);

foreach my $name ( qw/ Bob Carl David / ) {

POE::Session->create(
inline_states => {
_start => sub {
$_[KERNEL]->alias_set( $name );
$_[KERNEL]->sig( sveglia_tutti => 'svegliati' );

print "Imposto la sveglia\n";
$_[KERNEL]->post('alarm_clock', 'imposta', 5);
print "Ora mi metto a dormire!\n";
},
svegliati => sub {
my ($alias) = $_[KERNEL]->alias_list();
print "Mi chiamo $alias e sono sveglio!\n";
}
}
);
}

POE::Kernel->run();
$ perl alarmclock_broadcast.pl 
Imposto la sveglia
Ora mi metto a dormire!
Imposto la sveglia
Ora mi metto a dormire!
Imposto la sveglia
Ora mi metto a dormire!
Sveglia impostata da Bob a 5 secondi.
Sveglia gia` impostata.
Sveglia gia` impostata.
DRIIIIIN
Mi chiamo David e sono sveglio!
Mi chiamo Bob e sono sveglio!
Mi chiamo Carl e sono sveglio!

Vediamo che l’handler dell’evento drin è cambiato. Anziché usare post() per mandare un evento ad una sessione specifica, usiamo questa volta il metodo signal() (ancora una volta un metodo di POE::Kernel) per inviare un segnale al kernel stesso: sarà quest’ultimo a farne il broadcast a tutte le sessioni. Le sessioni sono tutte configurate per reagire al segnale. Notiamo che usando il metodo sig() creiamo una mappatura tra il nome del segnale e il nome dell’evento che deve essere innescato. Osserviamo infine l’uso del nuovo metodo alias_list(), che restituisce la lista di alias assegnati alla sessione specificata (o alla sessione corrente, se non viene specificato diversamente): è stato utile per stampare messaggi più chiari.

La tecnica usata in questo caso per implementare la nozione di broadcaster e listener è piuttosto primitiva: benché il codice sia breve e semplice da comprendere, presenta degli svantaggi. Uno su tutti, il fatto che stiamo perdendo di vista il mittente originario dell’evento di sveglia: sarà il kernel infatti a comparire come mittente, dal punto di vista della sessione che riceve il messaggio. Parlando di componenti, vedremo un approccio più sano al problema.

Spero, fin qui, di aver dissipato le perplessità che potevano essere generate dall’involuta versione di Hello World presentata all’inizio dell’articolo. Come al solito, le cose si fanno davvero interessanti quando si cominciano ad esaminare le tecniche da usare per far comunicare il proprio programma con il mondo esterno. Non sono soltanto le sessioni a creare eventi, ma anche il kernel stesso può farlo, come risposta a stimoli esterni. Tali stimoli possono essere dei segnali (argomento che abbiamo già citato di sfuggita parlando di eventi broadcast), oppure avvenimenti che hanno a che fare con l’I/O. Visto che questa presentazione vuole avere un taglio pratico, nella prossima parte mostrerò direttamente gli oggetti di alto livello messi a disposizione dal framework.

Comments

  1. Gianluca says:

    Molto interessante ed esplicativo questo articolo, avevo gia’ visto POE su CPAN ma mi era rimasto un po oscuro dalla documentazione ufficiale. Con questi esempi mi sembra piu chiaro (forse anche perche’ e’ in italiano).

  2. Marco De Lellis says:

    Questo POE è veramente interessante, e poi raccontato così è nettamente più chiaro del capitolo POE di Advanced Perl Programming

    Mi sembra un ottimo strumento per la creazione di demoni in Perl … Lo metterò all’opera al più presto.

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.