
Flavio Poletti
Consulente in telecomunicazioni per una piccola società a Roma, usa Perl
per lavoro e per diletto, con particolare attenzione all'automazione di
processi ripetitivi e noiosi. www.polettix.it
|
Per chi non lo conoscesse, Damian Conway è uno dei punti di riferimento
nella comunità Perl: moduli, libri, conferenze... Una delle sue
ultime fatiche, Perl Best Practices, riporta una serie di 256
raccomandazioni per migliorare il proprio modo di produrre codice.
Una cosa che ho trovato interessante nel libro è stata il fatto che,
in moltissime occasioni, sembra una promozione dei propri moduli
pubblicati su CPAN. Non so, m'è sembrato un po' autocelebrativo,
ma se non si autocelebra Conway parlando di Perl... chi può farlo?
In una raccomandazione, in particolare, suggerisce di utilizzare
il modulo Contextual::Return, che permette di restituire, in uscita
da una funzione, valori differenti a seconda del contesto in cui
la funzione viene chiamata.
Come i valori restituiti dalle funzioni, anche Conway cerca di essere
``un uomo per tutte le stagioni'': il modulo, infatti, può essere utilizzato
in vari modi a seconda del contesto (anche culturale) di chi lo usa.
Uno degli scopi del modulo è quello di consentire di scrivere codice
più leggibile. Perl, sostiene Conway, permette già di restituire valori
differenti a seconda del contesto in cui viene chiamata una funzione;
tali contesti sono sostanzialmente tre ``e mezzo'': void, scalare (che
conta come uno e mezzo, come si può vedere) e lista:
funzione(); # Contesto void
my $valore = funzione(); # Contesto scalare
print "ciao" if (funzione()); # Contesto scalare/booleano
my @valori = funzione(); # Contesto lista
Per distinguere i tre casi Perl mette a disposizione la funzione
wantarray, che può assumere tre valori:
- undef in contesto void;
- definito ma falso (ossia 0, '0' o stringa vuota) in contesto scalare
(booleano compreso);
- definito e vero in contesto lista.
Per quanto wantarray consenta di distinguere i tre contesti di
chiamata, però, va detto che Conway ha ragione a dire che il codice
risultante è poco leggibile:
sub funzione {
return qw( ciao a tutti ) if wantarray; # definito e vero
return 'scalare' if defined wantarray;
print "void!\n"; # E` avanzato solo il contesto scalare!
return;
}
Si sta chiedendo al lettore del nostro codice di avere sempre ben
chiara l'associazione fra i tre possibili valori di wantarray ed
il contesto di chiamata.
Qui entra in gioco una prima incarnazione di Contextual::Return: questo
definisce infatti le funzioni LIST, SCALAR e VOID che - l'avreste mai
detto? - fanno il loro dovere in maniera piuttosto leggibile:
use Contextual::Return;
funzione(); # Contesto void
my $valore = funzione(); # Contesto scalare
print "ciao" if (funzione()); # Contesto scalare/booleano
my @valori = funzione(); # Contesto lista
sub funzione {
return qw( ciao a tutti ) if LIST;
return 'scalare' if SCALAR;
print "void!\n" if VOID;
return;
}
Le classiche cose che uno dice "bastava pensarci!". Con la differenza
che poi si attende che ci pensi qualcun altro.
Io mi sarei fermato a questo punto, perché sono un tipo un po' grezzo
che non sta troppo lì a rifinire. Conway, che invece il Perl lo sa
davvero, è andato avanti e ci permette di scrivere una cosa del
genere:
use Contextual::Return;
sub funzione {
return (
LIST { return qw( ciao a tutti ); }
SCALAR { return 'scalar'; }
VOID { print "void!\n"; }
);
}
Molto d'effetto, non c'è che dire. Vi starete chiedendo: ma come fa questa
roba a compilare correttamente?!? Semplice, se diamo un'occhiata
al codice sorgente, ad esempio per la funzione LIST:
sub LIST (;&$) {
my ($block, $crv) = @_;
# ecc. ecc.
return $crv;
}
La funzione LIST può essere chiamata senza parametri, ma anche con
due parametri opzionali! Il primo è un riferimento ad una subroutine,
il secondo uno scalare. Questo ci dice che quello che segue la chiamata
a LIST è un blocco che rappresenta una funzione a tutti gli effetti:
LIST { return qw( ciao a tutti ); }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
questa e` una funzione anonima
Poiché stiamo specificando nel prototipo di LIST che ci aspettiamo
un riferimento ad una sub (è questo il significato della lettera
& nel prototipo), non c'è bisogno di scrivere sub.
E lo scalare? Beh, prima di tutto è opzionale, per cui possiamo anche
non metterlo, come nella chiamata finale a VOID:
VOID { print "void!\n"; }
Il fatto che ciascuna funzione possa restituire uno scalare è invece
utile per le concatenazioni: il valore proveniente dalla chiamata a
VOID è lo scalare in ingresso a SCALAR, il cui valore
restituito è lo scalare della chiamata a LIST. Ecco spiegato
come può funzionare tutta quella strana sintassi!
Per me, che ho sempre vissuto in una località di mare, già con
questo zucchero sintattico siamo arrivati sull'Everest. Conway,
a questo punto, accende i razzi e punta dritto sulla Luna. Per
il momento vediamo se e dove arriva, poi eventualmente lo
seguiamo.
Mentre i blocchi relativi ai contesti lista e void sono valutati subito,
i blocchi relativi alla parte della funzione SCALAR sono calcolati
in maniera pigra. Che vuol dire?
Come programmatori Perl, sicuramente esercitate quotidianamente la
vostra pigrizia. Perché fare oggi qualcosa che si può rimandare a
domani? O, meglio, non fare mai? No, non è un'esortazione a lasciare il letto
come una cuccia perché, tanto, stasera lo disferete di nuovo; piuttosto
è una riflessione su come sia inutile mettersi a cucinare se abbiamo
una mezza idea di andare a mangiare fuori con gli amici.
In molti casi, è bene ritardare il più possibile l'esecuzione di un
calcolo - potrebbe darsi che alla fine non ci sia bisogno del risultato!
Questo è quello che si dice avere un approccio pigro alla
valutazione di una funzione.
In pratica, quello che restituisce SCALAR è un oggetto di magia
PerlVoodoo, che effettua il calcolo nel blocco solo quando andiamo a
stuzzicarlo. La cosa non è priva di conseguenze; Conway riporta
infatti il seguente esempio:
sub make_counter {
my $counter = 0;
return SCALAR { $counter++ }
}
my $idx = make_counter();
print "$idx\n"; # Stampa 0
print "$idx\n"; # Stampa 1
print "$idx\n"; # Stampa 2
In pratica $idx si comporta come se incrementasse di valore ad ogni
tentativo di utilizzo. Non ci addentreremo nei meandri della spiegazione
del perché - l'importante è sapere di questo effetto collaterale.
Superata l'atmosfera, Conway entra nella parte finale del viaggio
sulla Luna. Il suo ragionamento suona più o meno così: a volte non
abbiamo a disposizione abbastanza modi differenti di restituire
un valore da una funzione.
Come? Avete capito bene: wantarray non gli basta, vuole di più! Devo
ammettere che già mi stupiva che Perl supportasse tre possibili valori
in uscita da una funzione, a seconda di questo contesto; ma non
farseli nemmeno bastare mi è sembrata pura follia!
Dunque: se il valore restituito viene utilizzato come numero devo fare
una cosa; come stringa ne devo fare un'altra; in un test, ossia
in un contesto booleano, un'altra ancora e così via. E viene fuori
che possiamo scrivere mostri di questo tipo:
sub get_server_status {
my ($server_ID) = @_;
# Acquire server data somehow...
my %server_data = _ascertain_server_status($server_ID);
# Return different components of that data
# depending on call context...
return (
LIST { @server_data{ qw(name uptime load users) } }
BOOL { $server_data{uptime} > 0 }
NUM { $server_data{load} }
STR { "$server_data{name}: $server_data{uptime}" }
VOID { print "$server_data{load}\n" }
DEFAULT { croak q{Bad context! No biscuit!} }
);
}
Sono comparse nuove funzioni:
- BOOL: imposta la funzione chiamata se il valore viene trattato come un
booleano, ad esempio nel test di una
if;
- NUM: imposta la funzione che viene chiamata quando il valore è trattato come un numero;
- STR: avete indovinato? Si, è quella funzione che viene chiamata quando
trattiamo il valore restituito come una stringa.
Come fa a funzionare? In fondo se dico:
my $valore = get_server_status('pinco');
la variabile $valore non lascia trasparire niente di come sarà
utilizzata in seguito! È a questo punto che entra in gioco la
valutazione pigra: proprio perché si aspetta a valutare finché
non si cerca di utilizzare il valore stesso, possiamo tranquillamente
andare avanti finché non troviamo un utilizzo pratico. Quindi
possiamo scrivere:
if ( my $status = get_server_status() ) { # True if uptime > 0
$load_distribution[$status]++; # Evaluates to load value
print "$status\n"; # Prints name: uptime
}
Lo stesso $status viene utilizzato prima in ``contesto booleano''
(all'interno dell'if), poi in ``contesto numerico'' (come indice
di un array) ed infine in ``contesto stringa'' (all'interno delle
virgolette). Ad ognuno di questi utilizzi viene chiamato il blocco
di codice (ossia, la funzione anonima) impostata in
get_server_status() ed il gioco è fatto.
Parecchi, parecchi. Conway ci rende la vita ``semplice'' con il seguente
grafico:
DEFAULT
^
|
|--< VOID
|
`--< NONVOID
^
|
|--< VALUE
| ^
| |
| |--< SCALAR
| | ^
| | |
| | |--< BOOL
| | |
| | |--< NUM
| | |
| | `--< STR
| |
| `--< LIST
|
|
`--- REF
^
|
|--< ARRAYREF
|
|--< SCALARREF
|
|--< HASHREF
|
|--< CODEREF
|
|--< GLOBREF
|
`--< OBJREF
Tutte le etichette rappresentano un possibile differente contesto. Il
padre di tutti i possibili valori restituiti è DEFAULT, che in pratica
viene utilizzato quando neppure Contextual::Return sa che pesci
prendere. Gli scalari ``normali'' sono suddivisi nelle tre sottoclassi
che abbiamo già visto (BOOL, NUM e STR), ma da notare
la nutrita famiglia di riferimenti. Ad esempio, potremmo avere una
funzione che restituisce una lista o un riferimento ad array:
sub funzione {
my @dati = qw( ciao a tutti );
return (
LIST { return @dati; } # Buon vecchio contesto lista
ARRAYREF { return \@dati; } # Se vogliamo un riferimento
DEFAULT { die 'spiacente!' } # Imprevisto?!?
);
}
my @array = funzione();
my $a_ref = funzione();
my @secondo_array = @{ $a_ref }; # Stiamo utilizzando $a_ref come
# riferimento ad array!
Questo ci può togliere dall'imbarazzo di progettare un'interfaccia che
restituisca una lista potenzialmente lunga oppure un riferimento
ad un array che contiene la lista - più efficiente ma anche più
complicato da utilizzare.
La mia professoressa di disegno alle superiori diceva sempre che
``Chi tace... sta zitto''. In questo caso, invece, Conway ci legge nella
testa ed impone dei comportamenti di riserva nel caso non siamo
abbastanza espliciti nel dire cosa vogliamo fare.
Ad esempio, supponiamo di avere la seguente funzione:
sub funzione {
return (
LIST { return qw( ciao a tutti ); }
NUM { return 42; }
DEFAULT { die 'spiacente!'; }
);
}
e di avere il seguente utilizzo:
my $valore = funzione(); # Contesto scalare => pigrizia!
print "Il valore e` $valore\n"; # Uhmm... STR ci starebbe bene!
Purtroppo STR non è specificata! In questo caso, come si suol dire,
Perl fa la cosa giusta: trasforma il numero 42 nella stringa
'42' e la usa per la stampa. Qual è dunque l'idea generale di
Conway su questa ``cosa giusta'' da fare? È presto detto osservando
il grafico che segue:
DEFAULT
^
|
|--< VOID
|
`--< NONVOID
^
|
|--< VALUE <..............
| ^ :
| | :
| |--< SCALAR <.....:..
| | ^ :
| | | :
| | |--< BOOL :
| | | :
| | |--< NUM <..:..
| | | : ^ :
| | | v : :
| | `--< STR <....:..
| | :
| `--< LIST :
| : ^ :
| : : :
`--- REF : : :
^ : : :
| v : :
|--< ARRAYREF :
| :
|--< SCALARREF ..........:
|
|--< HASHREF
|
|--< CODEREF
|
|--< GLOBREF
|
`--< OBJREF
Normalmente i fallback seguono i percorsi tratteggiati, altrimenti
vanno su nel percorso con le linee dritte. Complicato? Ad esempio,
nel nostro caso c'è una freccetta che collega STR a NUM: questo
vuol dire che se abbiamo bisogno di STR e non è specificata, ma
è specificata NUM, andremo ad utilizzare quella. Come si può
vedere è anche vero il viceversa.
Lo SCALARREF è quello che bussa a più porte di tutti prima di
arrendersi. Quando
un valore restituito viene utilizzato come se fosse un riferimento
ad uno scalare (ad esempio in $$valore), se non c'è un blocco
specifico si va a vedere, nell'ordine, se ne esiste uno
associato a STR, NUM, SCALAR e VALUE. Se va buca con tutti
niente paura! Si riparte da SCALARREF e si percorre l'albero
con le righe dritte, passando dunque nell'ordine a REF,
NONVOID ed infine DEFAULT. Che camminata!
È interessante la relazione fra LIST e ARRAYREF, che sono
una il fallback dell'altra: praticamente potevamo risparmiarci
la funzione di prima! In poche parole, se si specifica il comportamento
per LIST, ma si chiama la funzione in un contesto scalare per ottenere
un valore utilizzato come un riferimento ad array, ho lo stesso
identico comportamento che nella funzione di prima:
sub funzione {
my @dati = qw( ciao a tutti );
return (
LIST { return @dati; } # Buon vecchio contesto lista
DEFAULT { die 'spiacente!' } # Imprevisto?!?
);
}
my @array = funzione();
my $a_ref = funzione();
my @secondo_array = @{ $a_ref }; # Stiamo utilizzando $a_ref come
# riferimento ad array!
my $morte_certa = $a_ref + 1; # va a finire su DEFAULT -> die!
Credo che non si possa negare che Contextual::Return sia un gran
bel pezzo di codice, di quelli da approfondire se si ha un'oretta da
impegnare in un po' di studio. Non solo l'autore è una garanzia: anche
l'implementazione rivela dei ``trucchi'' che è quantomeno interessante
imparare a riconoscere.
Il rendere esplicito il test sul contesto ``vecchia maniera'', ossia
quello realmente implementato in Perl, è a mio avviso la cosa veramente
utile dentro questo modulo. Va ammesso che la funzione wantarray
fa bene il suo
lavoro, però - per così dire - se ne va in giro vestita in maniera
troppo stravagante, c'è il rischio che qualcuno non capisca cosa vuole
fare. LIST, SCALAR e VOID, invece, sono degli abiti che le
vanno a pennello e, soprattutto, la rendono molto più presentabile.
A mio modesto parere, però, gli aspetti utili di questo modulo finiscono
qui; in particolare, trovo piuttosto inquietante la pletora di
variazioni sul tema messe a disposizione per il caso scalare. In
``Perl Best Practices'', Conway delinea il seguente scenario di utilizzo
tipico: una funzione che normalmente restituisce un array con vari
elementi può essere utilizzata in contesto scalare, ed utenti differenti
possono aspettarsi di ricevere dati differenti. Nel suo esempio, in
particolare, la funzione potrebbe restituire parametri statistici di
una macchina quando chiamata in contesto lista, mentre potrebbe dare
``il più significativo'' quando chiamata in contesto scalare. La
definizione di ``più significativo'' cambia ovviamente da utente
potenziale ad altro utente, per cui Conway individua in
Contextual::Return la panacea per accontentare tutti.
Qui risiede la vera debolezza di tutto questo sistema.
Stiamo concentrando in una sola funzione troppe varianti; cosa più grave,
non stiamo imponendo almeno di poter distinguere queste varianti in
maniera esplicita (ad esempio mediante il passaggio di parametri
modificatori opportuni) in fase di chiamata, ma in maniera del
tutto implicita in fase di utilizzo. Si applicano, cioè, tutte le
critiche che possono essere mosse all'utilizzo di tie, ossia in
sostanza di nascondere troppe funzionalità dietro un'unica
interfaccia. Ed un'altra raccomandazione di ``Perl Best Practices'', lo
ricordo per chi avesse letto il libro, è quella di ``non usare tie''.
Va infine riconosciuto a Conway che, nel raccomandare Contextual::Return
all'interno del libro, devia dal solito approccio imperativo perentorio
(``usa questo'',
``non fare quest'altro'') per assumere un tono più conciliante (``tieni
in considerazione l'utilizzo di Contextual::Return se dovessi aver bisogno
di...''). Forse un'ammissione a mezza bocca della possibilità di fare
più danno (per la leggibilità e la manutenibilità) che altro.
Concludendo? Personalmente credo che eviterò di utilizzare questo
modulo, riservandomi di includerlo solamente per rimpiazzare
wantarray con LIST, SCALAR e VOID ed aumentare la
leggibilità. Quando sia veramente necessario.
La pagina su CPAN contenente i moduli di Damian Conway si trova
all'indirizzo http://search.cpan.org/~dconway/; per trovare il
modulo di questa recensione si può andare all'indirizzo
http://search.cpan.org/search
(già che ci siete, date anche un'occhiata a Return::Value).
Il libro citato in questa recensione, ``Perl Best Practices'', è molto
interessante ed anche piuttosto consistente (nonostante le critiche di
questa recensione). Molti consigli possono apparire banali, si potrebbe
rimanere delusi dal fatto che non si trova niente di veramente
strano o inusuale, ma è un libro su come scrivere codice manutenibile
(possibilmente anche da altri) e segue la filosofia del ``fare cose
semplici, non fare cose 'troppo intelligenti'''. La stessa filosofia che
viene tradita da questo modulo, secondo me. Il libro è edito da
O'Reilly, e si può trovare un po' di materiale sul loro sito, in
particolare su http://www.oreilly.com/catalog/perlbp/ (dove troverete
anche un capitolo di esempio gratuito).
Buona lettura!
Ti è piaciuto questo articolo? Iscriviti al feed!
|