Flavio Poletti
Contextual::Return - una recensione
http://www.perl.it/documenti/articoli/2008/01/contextualretur-1.html
© Perl Mongers Italia. Tutti i diritti riservati.

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.

vucumpr*cough* wantarray?

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.

Zucchero sintattico

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.

Valori scalari pigri

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.

C'è scalare e scalare

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.

Ma quanti sono questi contesti?

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.

Che succede se non dico niente?

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!

Nonostante tutto, non sono convinto

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.

Dove andare a cercare

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!