Perl 5 non prevede un meccanismo di gestione del flusso delle eccezioni, pur prevedendo queste ultime, come altri linguaggi fanno, spesso ereditando da C++ i blocchi e gli operatori try/catch e similari.
In Perl5 le eccezioni sono riportate come variabili globali, in particolare:

  • $! (errno) contiene la versione numerica o stringa della variabile globale C-Unix errno(3);
  • $? (child error) lo stato riportato dalla exit dell'ultimo sottoprocesso creato per pipe, backtick, chiamata a system (simile in questo alla stessa variabile della bourne shell);
  • $@ (eval error) lo stato di errore dell'ultimo blocco eval eseguito.

Unitamente a questo, Perl 5 prevede la funzione speciale "die" che solleva un'eccezione: se questo avviene nel programma principale questo termina (muore, da qui il nome), se invece si è in un blocco eval il blocco termina e la variabile globale $@ viene impostata a quanto specificato nell'eccezione. Da notare che è possibile passare anche un riferimento a "die", quindi anche un oggetto (eccezione) che viene poi assegnato alla variabile $@.

L'uso di variabili globali per la gestione delle eccezioni non è certo ottimale, almeno nella programmazione OOP. Si rischia infatti che il sollevamento di una eccezione, in blocco annidato, faccia sollevare una nuova eccezione e quindi che la propagazione sia a piu' livelli e con cause differenti. Essendo $@ unica, solo l'ultimo livello di propagazione viene riportato. Non solo: essendo $@ globale si rischia che il suo valore sia resettato in punti inaspettati e quindi è necessario "ricordarsi" di analizzare $@ quanto prima possibile.


Sono quindi nati molti moduli su CPAN per la gestione delle eccezioni, e uno del quale parlare è Try::Tiny.
Questo è un modulo molto semplice, non gestisce alcuni casi particolari (es. return in eval), ma fornisce comunque una sintassi "carina" e che ogni programmatore riesce a comprendere:

try{
die "Argh!";
}
catch { say "Eccezione: $_"; };

In particolare Try::Tiny imposta la variabile topic a $@ appena si entra nel blocco catch, permettendo quindi di dimenticarsi di $@ stessa. Ma come riesce a fare la magia della sintassi e a introdurre try-catch? Beh, ancora una volta entra in gioco la magia e la flessibilità di Perl 5.

Anzitutto si noti il ';' al termine del blocco catch: questo indica che in realtà si sta eseguendo una istruzione Perl 5 valida, o per meglio dire una funzione. E infatti Try::Tiny esporta tre funzioni che si chiamano "try", "catch" e "finally".
Si consideri un estratto di "catch" prima:

sub catch (&;@) {
  my ( $block, @rest ) = @_;

  croak 'Useless bare catch()' unless wantarray;

...
  return (
   bless(\$block, 'Try::Tiny::Catch'),
        @rest,
  );
}


Anzitutto la funzione accetta come primo parametro un blocco di codice o una sub (il carattere & del prototipo), e si aspetta di essere chiamato in contesto di lista, quindi ad esempio come

my ($catch) = ( catch { say "Eccezione: $_"; } );

La fine della funzione catch converte il blocco di codice in un oggetto di tipo 'Try::Tiny::Catch', e questo serve per dei controlli interni.

Vediamo brevemente la funzione "try", sicuramente piu' interessante:


sub try (&;@) {
  my ( $try, @code_refs ) = @_;
  my $wantarray = wantarray;
  my ( $catch, @finally ) = ();


  foreach my $code_ref (@code_refs) {

   if ( ref($code_ref) eq 'Try::Tiny::Catch' ) {
      croak 'A try() may not be followed by multiple catch() blocks'
     if $catch;
    $catch = ${$code_ref};
  } elsif ( ref($code_ref) eq 'Try::Tiny::Finally' ) {
     push @finally, ${$code_ref};
  } else {
    croak(
    'try() encountered an unexpected argument ('
       . ( defined $code_ref ? $code_ref : 'undef' )
.      ') - perhaps a missing semi-colon before or'
    );
  }
}

...

  my $failed = not eval {
  $@ = $prev_error;

    # evaluate the try block in the correct context
    if ( $wantarray ) {
      @ret = $try->();
    } elsif ( defined $wantarray ) {
      $ret[0] = $try->();
   } else {
      $try->();
  };

  return 1; # properly set $failed to false
};


  $error = $@;
  $@ = $prev_error;


  if ( $failed ) {
   ...
    if ( $catch ) {

     for ($error) {
      return $catch->($error);
    }

 }

  return;
 } else {
  return $wantarray ? @ret : $ret[0];
 }
}

Anche qui la funzione si aspetta come primo argomento un blocco o una sub. Gli altri argomenti vengono trattati a loro volta come argomenti o sub, e si controlla nel primo loop che siano di tipo 'Try::Tiny::Catch', o finally o vengono considerati come alieni.
Successivamente si esegue l'eval del blocco passato come primo argomento e, qualora questo abbia impostato un errore, si esegue anche il blocco catch passanto la variabile di errore come primo argomento.

Chiarito come avviene la magia sintattica di try-catch è possibile anche scrivere il blocco esplicitando le funzioni e i loro argomenti:

try( sub { die "Eccezione!" }, 
     catch( sub { say "Presa: $_" } ) );

Ma come vengono gestiti i blocchi finally? Il meccanismo è abbastanza elegante: all'interno della funzione "try" vengono creati degli oggetti (blessed reference) per ogni blocco finally incontrato. Tali oggetti hanno un metodo DESTROY che invoca il code ref (blocco o funzione) specificato. Quando la funzione try termina gli oggetti vanno fuori visibilità e quindi vengono distrutti, e di conseguenza viene eseguito DESTROY che a sua volta richiama il codice specificato dall'utente.

The article Perl 5 e le eccezioni: come funziona Try::Tiny has been posted by Luca Ferrari on January 19, 2017

Tags: perl