Gestire Programmi Reali

Un libro deve anzitutto insegnarvi a scrivere piccoli programmi per risolvere piccoli problemi d'esempio. In questo modo potete imparare bene la sintassi. Tuttavia, per scrivere programmi reali che risolvono problemi reali dovete anche imparare a gestire il codice scritto nel vostro linguaggio. Come dovete organizzare tale codice? Come potete verificare che funziona? Come potete renderlo robusto rispetto agli errori? Che cosa rende il vostro codice conciso, chiaro e manutenibile?

Il Perl moderno fornisce molti strumenti e tecniche per scrivere programmi reali.

Testing

Il testing è un processo che consiste nella scrittura ed esecuzione di piccoli pezzi di codice al fine di verificare che il vostro software funzioni come vi aspettate. Un testing efficace è essenzialmente l'automatizzazione di un processo che avete sicuramente già eseguito innumerevoli volte a mano: scrivere del codice, eseguirlo e controllare se funziona. L'automazione è il punto più importante. Anziché dover contare su delle persone per ripetere in modo affidabile i controlli a mano, lasciate questo compito al calcolatore.

Perl 5 fornisce degli ottimi strumenti per aiutarvi a scrivere i test nel modo giusto.

Test::More

Per iniziare a fare testing in Perl dovete usare il modulo Test::More distribuito con Perl 5, e in particolare la sua funzione ok(). ok() riceve due parametri, un valore booleano e una stringa che descrive lo scopo del test:

    ok(   1, 'il numero uno dovrebbe essere vero'            );
    ok(   0, '... mentre lo zero non dovrebbe'               );
    ok(  '', 'la stringa vuota dovrebbe avere valore falso'  );
    ok( '!', '... mentre una stringa non vuota non dovrebbe' );

    done_testing();

Qualunque condizione vogliate testare nel vostro programma può essere ridotta a un valore binario. Ogni asserzione di test è una semplice domanda a cui si può rispondere sì o no: questo pezzettino di codice funziona come mi aspetto? Un programma complesso potrebbe avere migliaia di tali condizioni e, in generale, più ne considerate meglio è. Isolare comportamenti specifici in singole asserzioni vi permette di restringere il campo d'azione di bug ed equivoci, specialmente quando modificherete il codice in futuro.

La funzione done_testing() dice a Test::More che il programma ha eseguito con successo tutte le asserzioni di test previste. Se il programma è incappato in una eccezione o è comunque terminato in modo inatteso prima di chiamare done_testing(), il framework di test vi notificherà che qualcosa è andato storto. Senza un meccanismo come done_testing(), come potreste accorgervene? È vero che il codice di questo esempio è troppo semplice per poter fallire, ma è altrettanto vero che il codice troppo semplice per poter fallire fallisce molto più spesso di quanto ci si aspetti.

Eseguire Test

Il programma risultante è a tutti gli effetti un programma Perl 5 che produce il seguente output:

    ok 1 - il numero uno dovrebbe essere vero
    not ok 2 - ... mentre lo zero non dovrebbe
    #   Failed test '... mentre lo zero non dovrebbe'
    #   at valori_booleani.t line 4.
    not ok 3 - la stringa vuota dovrebbe avere valore falso
    #   Failed test 'la stringa vuota dovrebbe avere valore falso'
    #   at valori_booleani.t line 5.
    ok 4 - ... mentre una stringa non vuota non dovrebbe
    1..4
    # Looks like you failed 2 tests of 4.

Questo formato è conforme a uno standard per l'output dei test chiamato TAP, il Test Anything Protocol (http://testanything.org/). Il fallimento di test TAP genera dei messaggi diagnostici per facilitare il debugging.

L'output di un file di test che contiene molte asserzioni (e specialmente molte asserzioni fallite) può essere verboso. Nella maggior parte dei casi, siete interessati soltanto a sapere se tutti i test sono passati e, in caso contrario, i dettagli dei fallimenti. Il modulo Test::Harness distribuito con Perl interpreta il formato TAP e il suo programma associato prove esegue i test e visualizza solo le informazioni più pertinenti:

    $ prove valori_booleani.t
    valori_booleani.t .. 1/?
    #   Failed test '... mentre lo zero non dovrebbe'
    #   at valori_booleani.t line 4.

    #   Failed test 'la stringa vuota dovrebbe avere valore falso'
    #   at valori_booleani.t line 5.
    # Looks like you failed 2 tests of 4.
    valori_booleani.t .. Dubious, test returned 2
        (wstat 512, 0x200)
    Failed 2/4 subtests

    Test Summary Report
    -------------------
    valori_booleani.t (Wstat: 512 Tests: 4 Failed: 2)
      Failed tests:  2-3

È un output molto lungo che riporta qualcosa che dovrebbe essere gíà ovvio: il secondo e il terzo test falliscono perché lo zero e la stringa vuota hanno valore falso. Si possono facilmente evitare tali fallimenti invertendo il senso delle condizioni con l'uso della coercizione booleana (Coercizione Booleana):

    ok(   ! 0, '... mentre lo zero non dovrebbe'              );
    ok(  ! '', 'la stringa vuota dovrebbe avere valore falso' );

Con queste modifiche, prove visualizza ciò che segue:

    $ prove valori_booleani.t
    valori_booleani.t .. ok
    All tests successful.

Migliorare i Confronti

Sebbene al cuore di ogni test automatico ci sia la condizione booleana "questo è vero o falso?", ridurre ogni cosa a tale condizione booleana è noioso e offre scarse possibilità di diagnostica. Test::More fornisce svariate altre funzioni di asserzione che possono essere più convenienti.

La funzione is() confronta due valori usando l'operatore eq. Se tali valori sono uguali il test passa. Altrimenti il test fallisce con un messaggio diagnostico:

    is(           4,   2 + 2, 'la somma dovrebbe funzionare' );
    is( 'frittella',   100,   'le frittelle sono numeri'     );

Come vi aspettavate, il primo test passa e il secondo fallisce:

    t/test_is.t .. 1/2
    #   Failed test 'le frittelle sono numeri'
    #   at t/test_is.t line 8.
    #          got: 'frittella'
    #     expected: '100'
    # Looks like you failed 1 test of 2.

Mentre ok() fornisce solo il numero di linea del test fallito, is() visualizza il valore atteso e quello ricevuto.

is() applica implicitamente il contesto scalare ai propri valori (Prototipi). Pertanto potete ad esempio controllare il numero di elementi di un array senza valutarlo esplicitamente in contesto scalare:

    my @cugini = qw( Riccardo Ale Carlo
                     Enrico Camilla Maria );
    is( @cugini, 6, 'Dovrei avere solo sei cugini' );

... anche se alcuni preferiscono scrivere scalar @cugini per essere più chiari.

La funzione duale isnt() di Test::More confronta due valori usando l'operatore ne e ha successo se essi non sono uguali. Anche in questo caso gli operandi sono valutati in contesto scalare.

Sia is() che isnt() effettuano dei confronti stringa con gli operatori eq e ne di Perl 5. Quasi sempre questa è la cosa giusta da fare, ma per valori complessi come gli oggetti con overloading (Overloading) o le variabili doppie (Dualvar) può essere preferibile effettuare il test con un confronto esplicito. La funzione cmp_ok() vi permette di specificare l'operatore di confronto:

    cmp_ok( 100, $saldo_corrente, '<=',
           'Dovrei avere almeno 100€' );

    cmp_ok( $pastore_scozzese, $collie, '==',
           'I numeri dei parenti di Lassie dovrebbero essere gli stessi' );

Classi e oggetti hanno dei propri modi peculiari di interagire con i test. Potete verificare se una classe o un oggetto estendono un'altra classe (Ereditarietà) con isa_ok():

    my $gorzilla = RobotScimmia->new();
    isa_ok( $gorzilla, 'Robot' );
    isa_ok( $gorzilla, 'Scimmia' );

isa_ok() fornisce un proprio messaggio diagnostico in caso di fallimento.

can_ok() verifica che una classe o un oggetto possano eseguire uno o più metodi specificati:

    can_ok( $gorzilla, 'mangia_banana' );
    can_ok( $gorzilla, 'trasformati', 'distruggi_tokyo' );

La funzione is_deeply() confronta due riferimenti per garantire che abbiano contenuti uguali:

    use Clone;

    my $numeri     = [ 4, 8, 15, 16, 23, 42 ];
    my $numclonati = Clone::clone( $numeri );

    is_deeply( $numeri, $numclonati,
         'clone() dovrebbe generare elementi identici' );

Se il confronto fallisce, Test::More fa del suo meglio per fornire una ragionevole diagnostica che indichi la posizione della prima differenza tra le strutture. Vedete i moduli CPAN Test::Differences e Test::Deep per test maggiormente configurabili.

Test::More contiene molte altre funzioni, ma quelle descritte sono le più utili.

Organizzazione dei Test

Le distribuzioni CPAN dovrebbero includere una directory t/ con uno o più file di test i cui nomi terminano col suffisso .t. Per default, quando fate il build di una distribuzione con Module::Build o ExtUtils::MakeMaker, il passo di testing esegue tutti i file t/*.t, sintetizza i loro output e ha successo o fallisce a seconda dei risultati dell'intera suite di test. Non ci sono linee guida specifiche su come gestire i contenuti dei singoli file .t, ma due strategie sono piuttosto comuni:

Un approccio ibrido vi dà più flessibilità; un test potrebbe verificare che tutti i vostri moduli siano compilabili, mentre gli altri test potrebbero verificare che ogni modulo si comporti come previsto. Gestire i test in termini di funzionalità diventa più opportuno man mano che le distribuzioni diventano più grandi; dei file di test troppo grandi sono più difficili da manutenere.

L'uso di file di test separati può anche accelerare lo sviluppo. Se state aggiungendo la capacità di sputare fuoco al vostro RobotScimmia, potreste volere eseguire il solo file di test t/sputa_fuoco.t. Quando la nuova funzionalità si comporta in modo soddisfacente, eseguite l'intera suite di test per verificare che le modifiche locali non abbiano effetti globali indesiderati.

Altri Moduli di Testing

Test::More si basa su un modulo sottostante di nome Test::Builder. Tale modulo gestisce il piano di test e coordina l'output del test col protocollo TAP. Questa separazione permette a diversi moduli di test di condividere lo stesso Test::Builder. Di conseguenza, CPAN contiene centinaia di moduli di test—e tutti possono convivere nello stesso programma.

Vedete il progetto Perl QA (http://qa.perl.org/) per maggiori informazioni sul testing in Perl.

Gestione dei Warning

Anche se c'è più di un modo di scrivere un programma funzionante in Perl 5, alcuni di tali modi possono essere poco eleganti, poco chiari o anche insidiosamente sbagliati. Il sistema di warning opzionali di Perl 5 può aiutarvi a identificare e evitare queste situazioni.

Generare i Warning

Usate la funzione predefinita warn per emettere un warning:

    warn 'Qualcosa e` andato storto!';

warn stampa una lista di valori sul filehandle STDERR (Input e Output). Perl aggiunge il nome del file e il numero di linea in cui è stata chiamata warn a meno che l'ultimo elemento della lista termini con un carattere di a capo.

Il modulo Carp distribuito con Perl 5 offre altri meccanismi per generare dei warning. La sua funzione carp() emette un warning dal punto di vista del codice chiamante. Dato il seguente codice di validazione dei parametri di una funzione:

    use Carp 'carp';

    sub solo_due_argomenti
    {
        my ($ops, $opd) = @_;
        carp( 'Ho ricevuto troppi argomenti' ) if @_ > 2;
        ...
    }

... il warning di arità (Arità) includerà il nome del file e il numero di linea relativi al codice chiamante, e non quelli relativi alla funzione solo_due_argomenti(). Analogamente, la funzione cluck() di Carp genera una traccia di tutte le chiamate di funzione effettuate fino a quella corrente.

Il modo verbose di Carp aggiunge le tracce delle chiamate a tutti i warning generati da carp() e croak() (Gestione degli Errori), dovunque ci si trovi all'interno del programma:

    $ perl -MCarp=verbose mio_prog.pl

Quando scrivete dei moduli (Moduli), usate Carp al posto di warn e die.

Attivare e Disattivare i Warning

Potrebbe capitarvi di incontrare l'argomento da linea di comando -w nel codice poco recente. Tale argomento attiva i warning in tutto il programma, inclusi i moduli esterni scritti e manutenuti da altre persone. Il suo effetto è del tipo "tutto o niente", ma potrebbe esservi utile se intendete eliminare i warning attuali e potenziali nell'intera codebase.

L'approccio moderno è quello di usare la direttiva warnings ...o un suo equivalente come use Modern::Perl;.. In questo modo si attivano i warning all'interno di scope lessicali e si indica che gli autori del codice ritengono che esso non debba normalmente produrre dei warning.

Flag Globali per i Warning

Il flag -W attiva i warning nell'intero programma in modo unilaterale, indipendentemente dall'attivazione o disattivazione lessicale con la direttiva warnings. Il flag -X disattiva i warning nell'intero programma in modo unilaterale. Nessuno dei due è usato frequentemente.

I flag -w, -W e -X influenzano il valore della variabile globale $^W. Il codice scritto prima dell'introduzione della direttiva warnings (avvenuta con Perl 5.6.0 nella primavera del 2000) potrebbe localizzare $^W per sopprimere certi warning in un dato scope.

Disabilitare Categorie di Warning

Per disabilitare i warning in uno scope in modo selettivo, usate no warnings; con una lista di argomenti. Omettendo la lista di argomenti disattiverete tutti i warning all'interno dello scope.

perldoc perllexwarn elenca tutte le categorie di warning riconosciute dalla direttiva warnings della vostra versione di Perl 5. La maggior parte di essi rappresenta condizioni interessanti, ma alcuni possono essere peggio che inutili in certe situazioni. Per esempio, il warning recursion viene generato quando Perl si accorge che una funzione ha chiamato se stessa più di cento volte. Se confidate nella vostra capacità di scrittura delle condizioni di terminazione della ricorsione, potete disattivare questo warning nello scope della ricorsione (anche se forse è meglio usare le chiamate di coda; Chiamate di Coda).

Se state facendo generazione di codice (Generazione di Codice) o state ridefinendo dei simboli localmente, potete disattivare i warning redefine.

Alcuni programmatori Perl esperti disattivano i warning di uninitialized (valori non inizializzati, NdT) nel codice di elaborazione di stringhe che concatena valori con molte origini diverse. Con un'attenta inizializzazione delle variabili potete evitare di disattivare questi warning, ma potreste invece considerarli dei semplici fastidi per questioni di stile e di concisione.

Rendere Fatali i Warning

Se il vostro progetto considera i warning gravi come gli errori, potete renderli fatali. Per promuovere tutti i warning a eccezioni, usate:

    use warnings FATAL => 'all';

Potete anche rendere fatali solo alcune categorie specifiche di warning, come l'uso di costrutti deprecati:

    use warnings FATAL => 'deprecated';

Con una buona dose di disciplina, questo meccanismo vi consente di scrivere codice molto robusto—ma fate attenzione. Molti warning derivano da condizioni che si verificano durante l'esecuzione. Se la vostra suite di test non tiene conto di tutti i warning in cui potete imbattervi, il vostro programma potrebbe terminare inaspettatamente a causa di un'eccezione non catturata.

Catturare i Warning

Analogamente a come potete catturare le eccezioni, potete anche catturare i warning. La variabile %SIG Vedete perldoc perlvar. contiene i gestori dei segnali fuori banda sollevati da Perl o dal vostro sistema operativo. Per catturare un warning, assegnate un riferimento a funzione a $SIG{__WARN__}:

    {
        my $warning;
        local $SIG{__WARN__} = sub { $warning .= shift };

        # fai qualcosa di pericoloso
        ...

        say "Ho catturato il warning:\n$warning" if $warning;
    }

Il primo argomento del gestore dei warning è il messaggio di warning. In effetti questa tecnica è meno utile della disattivazione lessicale dei warning—ma può diventare rilevante in moduli di test come il modulo CPAN Test::Warnings, dove il testo del warning è importante.

State attenti al fatto che %SIG è globale. localizzatela nello scope più limitato possibile, ma ricordate che è comunque una variabile globale.

Registrare i Vostri Warning

La direttiva warnings::register vi permette di creare i vostri warning lessicali in modo che gli utenti del vostro codice possano attivarli e disattivarli. Fate la use della direttiva warnings::register in un modulo:

    package Scimmia::Pericolosa;

    use warnings::register;

Questo codice crea una nuova categoria di warning con il nome del package Scimmia::Pericolosa. Attivate questi warning con use warnings 'Scimmia::Pericolosa' e disattivateli con no warnings 'Scimmia::Pericolosa'.

Usate warnings::enabled() per verificare se una data categoria di warning è attiva nello scope lessicale chiamante. Usate warnings::warnif() per generare un warning solo se i warning sono attivi. Per esempio, per generare un warning della categoria deprecated:

    package Scimmia::Pericolosa;

    use warnings::register;

    sub import
    {
        warnings::warnif( 'deprecated',
            'l'importazione senza parametri da ' . __PACKAGE__ .
            ' e` deprecata' )
        unless @_;
    }

Vedete perldoc perllexwarn per maggiori dettagli.

File

La maggior parte dei programmi interagisce in qualche modo con il mondo reale e, in particolare, effettua letture, scritture e manipolazioni in genere sui file. Le origini di Perl come strumento per gli amministratori di sistema ne ha fatto un linguaggio adatto all'elaborazione del testo.

Input e Output

Un filehandle rappresenta lo stato corrente di uno specifico canale di input o di output. Ogni programma Perl 5 ha a disposizione tre filehandle standard, STDIN (l'input del programma), STDOUT (l'output del programma) e STDERR (l'output degli errori del programma). Per default, ogni cosa che stampate con print e say va su STDOUT, mentre gli errori, i warning e tutto ciò che generate con warn() va su STDERR. Questa separazione dell'output vi permette di redirezionare l'output utile e gli errori in due posti diversi—per esempio, un file di output e un log degli errori.

Usate la funzione predefinita open per ottenere un filehandle. Per aprire un file in lettura scrivete:

    open my $fh, '<', $nomefile
        or die "Non posso leggere da '$nomefile': $!\n";

Il primo operando è un lessicale che conterrà il filehandle risultante. Il secondo operando è la modalità del file, che determina il tipo delle operazioni permesse sul filehandle. L'ultimo operando è il nome del file. Se open fallisce, la clausola die solleva un'eccezione in cui il valore di $! fornisce le ragioni del fallimento.

Potete aprire i file in scrittura, append, lettura e scrittura e in altri modi ancora. Alcune delle modalità di file più importanti sono:

Modalità di File
SimboloSpiegazione
< Apri in lettura
> Apri in scrittura, sovrascrivendo il contenuto precedente se il file esiste già e creando il file in caso contrario.
>> Apri in scrittura, aggiungendo in coda al contenuto precedente o creando un nuovo file.
+< Apri in lettura e scrittura.
-| Apri una pipe in lettura verso un processo esterno.
|- Apri una pipe in scrittura verso un processo esterno.

Potete anche creare dei filehandle che leggono e scrivono su un semplice scalare Perl, usando una qualunque modalità di file:

    open my $fh_leggi,  '<', \$falso_input;
    open my $fh_scrivi, '>', \$output_catturato;

    fai_qualcosa_di_straordinario( $fh_leggi, $fh_scrivi );

Vi Ricordate autodie?

Tutti gli esempi di questa sezione hanno la use autodie; attivata, in modo che si possa evitare senza problemi la gestione degli errori. Se non volete usare autodie, potete farlo—ma ricordatevi di controllare i valori restituiti da tutte le chiamate di sistema per gestire gli errori in modo appropriato.

perldoc perlopentut contiene molti più dettagli sugli usi più esotici di open, inclusa la possibilità di lanciare e controllare altri processi e l'uso di sysopen per un controllo più fine dell'input e dell'output. perldoc perlfaq5 contiene del codice d'esempio per molti casi d'uso comuni dell'IO.

open a Due Argomenti

Il codice meno recente usa la forma di open() a due argomenti, che fonde la modalità e il nome del file:

    open my $fh, "> $un_file"
        or die "Non posso scrivere su '$un_file': $!\n";

Il fatto che Perl debba estrarre la modalità del file dal nome del file è causa di potenziali problemi. Ogni volta che Perl deve indovinare le vostre intenzioni, correte il rischio che faccia la scelta sbagliata. Peggio ancora, se $un_file proviene da dell'input utente inaffidabile c'è un potenziale problema di sicurezza, dato che la presenza di caratteri indesiderati potrebbe modificare il comportamento del vostro programma.

La open() a tre argomenti è un sostituto più sicuro di questo codice.

I Molti Nomi di DATA

Il filehandle di package speciale DATA rappresenta il file corrente. Quando Perl termina la compilazione del file, lascia DATA aperto alla fine dell'unità di compilazione se il file ha una sezione __DATA__ o __END__. Tutto il testo che segue tale token diventa disponibile tramite lettura da DATA. Questo meccanismo è utile per brevi programmi autocontenuti. Vedete perldoc perldata per maggiori dettagli.

Lettura da File

Potete leggere da un filehandle aperto in lettura con la funzione predefinita readline, scritta anche <>. Un idioma molto comune consiste nella lettura di una linea per volta in un ciclo while():

    open my $fh, '<', 'un_file';

    while (<$fh>)
    {
        chomp;
        say "Ho letto la linea '$_'";
    }

In contesto scalare, readline itera su tutte le linee del file finchè non arriva alla fine del file (eof()). Ogni iterazione restituisce la linea successiva. Quando si arriva alla fine del file, viene restituito il valore undef. L'idioma di esempio con while controlla esplicitamente che la variabile di iterazione sia definita, in modo che il ciclo termini soltanto quando si arriva alla fine del file. In altre parole, il codice dell'idioma è una versione abbreviata di:

    open my $fh, '<', 'un_file';

    while (defined($_ = <$fh>))
    {
        chomp;
        say "Ho letto la linea '$_'";
        last if eof $fh;
    }

Perchè usare un while e non un for?

for impone il contesto lista ai suoi operandi. Nel caso di readline, Perl leggerebbe l'intero file prima di elaborarne una parte qualunque. while invece itera leggendo una linea per volta. Quando la quantità di memoria può essere un problema, usate il while.

Ogni linea letta da readline contiene il carattere o i caratteri di marcatura di fine linea. Nella maggior parte dei casi, tale marcatura è una sequenza di caratteri che varia da piattaforma a piattaforma e consiste in un carattere di a capo (\n), un carriage return (\r) o una combinazione dei due (\r\n). Usate chomp per rimuoverla.

Il modo più indicato per leggere un file linea per linea in Perl 5 è il seguente:

    open my $fh, '<', $nomefile;

    while (my $linea = <$fh>)
    {
        chomp $linea;
        ...
    }

Per default, Perl accede ai file in modo testo. Se state leggendo dei dati binari, come un file multimediale o un file compresso—usate binmode prima di effettuare qualunque operazione di IO. In questo modo Perl preserverà i dati senza modificarli in alcun modo Le possibili modifiche includono invece la conversione di \n nella sequenza di a capo specifica della piattaforma.. Anche se le piattaforme in stile Unix non sempre necessitano di binmode, i programmi portabili dovrebbero usare più cautela (Stringhe e Unicode).

Scrittura su File

Potete scrivere su un filehandle aperto in scrittura con print e say:

    open my $fh_out, '>', 'file_di_output.txt';

    print $fh_out "Ecco una linea di testo\n";
    say   $fh_out "... ed eccone un'altra";

Notate l'assenza della virgola tra il filehandle e l'operando successivo.

Disambiguazione di Filehandle

Perl Best Practices di Damian Conway raccommanda l'abitudine di racchiudere il filehandle tra parentesi graffe. Ciò è necessario per disambiguare il parsing di un filehandle contenuto in una variabile aggregata e non fa danni negli altri casi.

Sia print che say ricevono una lista di operandi. Perl 5 usa la variabile globale predefinita $, come separatore tra i valori di una lista e il valore di $\ come ultimo argomento di print e say. Pertanto, queste due linee di codice producono lo stesso risultato:

    my @principi = qw( Corwin Eric Random ... );

    print @principi;
    print join( $,, @principi ) . $\;

Chiusura dei File

Quando avete terminato di manipolare un file, fate una close esplicita sul suo filehandle oppure lasciate semplicemente che il programma esca dal suo scope. In quest'ultimo caso Perl chiuderà il file al vostro posto. Il vantaggio di chiamare esplicitamete close è che potete controllare—e ripristinare—specifiche situazioni di errore, come l'esaurimento dello spazio su un dispositivo di memoria di massa o l'indisponibilità di una connessione di rete.

Come al solito, autodie può gestire questi controlli al vostro posto:

    use autodie;

    open my $fh, '>', $file;

    ...

    close $fh;

Variabili Speciali di Gestione dei File

Ad ogni linea letta Perl 5 incrementa il valore della variabile $., che serve quindi da contatore di linee.

readline usa il contenuto corrente di $/ come sequenza di fine linea. Per default, il valore di questa variabile è la sequenza di caratteri di fine linea più appropriata per i file di testo sulla vostra piattaforma. A dire il vero, la parola linea è fuorviante. Potete impostare il contenuto di $/ ad una qualunque sequenza di caratteri ... anche se, sfortunatamente, non con un'espressione regolare. Perl 5 non supporta questo caso.. Ciò può essere utile per dati fortemente strutturati da cui volete leggere un record alla volta. Dato un file i cui record sono separati da due linee vuote, impostate $/ a \n\n per leggere un record alla volta. Eseguendo chomp su un record letto dal file rimuoverete la sequenza di due caratteri di a capo.

Per default Perl fa il buffering del proprio output, ed effettua operazioni di IO solo quando la quantità di output pendente supera una determinata soglia. In questo modo è possibile aggregare tra loro un certo numero di costose operazioni di IO invece di scrivere separatamente delle piccole quantità di dati. Tuttavia alcune volte potreste desiderare di inviare i dati non appena sono disponibili, estromettendo il buffering—specialmente se state scrivendo un filtro a linea di comando collegato ad altri programmi oppure un servizio di rete che opera su singole linee.

La variabile $| controlla il buffering sul filehandle di output corrente. Quando è impostato a un valore diverso da zero, Perl fa il flush dell'output dopo ciascuna scrittura sul filehandle. Quando è impostato al valore zero, Perl adotta la propria strategia di buffering di default.

Flush Automatico

Per default i file adottano una strategia con buffering completo. Quando è connesso a un terminale attivo—e non a un altro programma—STDOUT adotta una strategia di buffering a linee, secondo la quale Perl fa il flush di STDOUT ogni volta che incontra un carattere di a capo nell'output.

Su un filehandle lessicale, usate il metodo autoflush() al posto della variabile globale:

    open my $fh, '>', 'nocciole.log';
    $fh->autoflush( 1 );

    ...

A partire da Perl 5.14, potete invocare su un filehandle tutti i metodi forniti da IO::File senza dover caricare esplicitamente IO::File. In Perl 5.12 dovete caricare manualmente IO::File e in Perl 5.10 e versioni precedenti dovete caricare invece FileHandle.

I metodi input_line_number() e input_record_separator() di IO::File vi permettono un accesso per singolo filehandle alle funzionalità per cui normalmente dovreste usare le variabili globali predefinite $. e $/. Vedete la documentazione di IO::File, IO::Handle e IO::Seekable per maggiori informazioni.

Directory e Path

Manipolare le directory è simile a manipolare i file, eccetto che non è possibile scrivere sulle directory Mentre potete salvare, spostare, rinominare e rimuovere i file.. Per aprire un directory handle usate la funzione nativa opendir:

    opendir my $dirh, '/home/scimmiotto/compiti/';

La funzione predefinita readdir legge da una directory. Analogamente a readline, potete iterare sul contenuto delle directory un elemento alla volta o potete assegnarlo a una lista in un'unica operazione:

    # iterazione
    while (my $file = readdir $dirh)
    {
        ...
    }

    # appiattimento in una lista
    my @listafile = readdir $altrodirh;

In Perl 5.12 è stata aggiunta una funzionalità per cui all'interno di un while readdir imposta $_:

    use 5.012;

    opendir my $dirh, 'compiti/circo/';

    while (readdir $dirh)
    {
        next if /^\./;
        say "Ho trovato il compito $_!";
    }

La strana espressione regolare in questo esempio permette di ignorare i cosiddetti file nascosti su sistemi Unix e simili, in cui per default la presenza di un punto iniziale fa sì che essi non compaiano negli elenchi del contenuto delle directory. Vengono ignorati anche i due file speciali . e .., che rappresentano rispettivamente la directory corrente e la sua directory genitore.

I nomi restituiti da readdir sono relativi alla directory stessa. In altre parole, se la directory compiti/ contiene tre file con i nomi mangiare, bere e scimmiottare, readdir restituisce mangiare, bere e scimmiottare e non compiti/mangiare, compiti/bere e compiti/scimmiottare. Invece, un path assoluto è un path completamente qualificato del filesystem.

Per chiudere un directory handle lasciate che il programma esca dal suo scope oppure invocate la funzione predefinita closedir.

Manipolazione di Path

Perl 5 offre una visione "alla Unix" del vostro filesystem e interpreta i path in stile Unix nel modo appropriato al vostro sistema operativo e al vostro filesystem. In altre parole, se siete su Microsoft Windows, potete usare sia il path C:/Miei Documenti/Robot/Bender/ che il path C:\Miei Documenti\Robot\Caprica Sei\.

Sebbene le operazioni di Perl seguano la semantica dei file Unix, l'uso di un modulo può rendere molto più facile la manipolazione multi-piattaforma di file. La famiglia di moduli File::Spec distribuiti con Perl fornisce astrazioni che vi permettono di manipolare path di file in modo sicuro e portabile. È venerando e consolidato, ma è anche piuttosto macchinoso.

La distribuzione CPAN Path::Class fornisce un'interfaccia migliore. Usate la funzione dir() per creare un oggetto che rappresenta una directory e la funzione file() per creare un oggetto che rappresenta un file:

    use Path::Class;

    my $pasti = dir( 'compiti', 'cucina' );
    my $file  = file( 'compiti', 'salute', 'robot.txt' );

Potete ottenere oggetti File da directory e viceversa:

    my $pranzo     = $pasti->file( 'calzone_ai_funghi' );
    my $dir_robot  = $lista_robot->dir();

Potete anche aprire filehandle di directory e file:

    my $fh_dir   = $dir->open();
    my $fh_robot = $lista_robot->open( 'r' )
                        or die "Open non riuscita: $!";

Sia Path::Class::Dir che Path::Class::File offrono altre funzionalità interessanti—ma ricordate che se usate un oggetto Path::Class di un qualche tipo con un operatore o una funzione Perl 5 che si aspettano una stringa contenente un path di file, dovete effettuare voi stessi la conversione in stringa dell'oggetto. Si tratta comunque di un piccolo difetto, anche se di lunga data.

    my $contenuti = leggi_da_nomefile( "$pranzo" );

Manipolazione di File

Oltre a leggere e scrivere sui file, potete anche manipolarli come fareste direttamente da linea di comando o da un gestore di file. Gli operatori di test di file, chiamati collettivamente "operatori -X" dato che consistono in un trattino seguito da un'unica lettera, esaminano gli attributi di file e directory. Per esempio, per verificare se un file esiste:

    say 'Presente!' if -e $nomefile;

L'operatore -e ha un unico operando, ovvero il nome di un file, un filehandle o un directory handle. Se il file esiste, l'espressione valuta a un valore vero. perldoc -f -X elenca tutti gli altri test di file; i più comuni sono:

A partire da Perl 5.10.1, potete accedere alla documentazione di ciascuno di questi operatori, per esempio con perldoc -f -r.

La funzione predefinita rename vi permette di ridenominare un file o di spostarlo da una directory ad un'altra. Riceve due operandi, ovvero il vecchio nome e il nuovo nome del file:

    rename 'stella_morta.txt', 'blocco_di_carbonio.txt';

    # oppure se volete essere raffinati:
    rename 'stella_morta.txt' => 'blocco_di_carbonio.txt';

Non c'è alcuna funzione predefinita per copiare un file, ma il modulo File::Copy distribuito con Perl fornisce sia una funzione copy() che una funzione move(). Usate la funzione predefinita unlink per cancellare uno o più file (la funzione predefinita delete cancella un elemento da un hash, e non un file dal filesystem.). Tutte queste funzioni restituiscono un valore vero in caso di successo e impostano $! in caso di errore.

Meglio di File::Spec

Path::Class fornisce metodi convenienti per controllare alcuni attributi di file e per rimuovere file in modo portabile.

Perl tiene traccia della directory di esecuzione corrente. Per default, essa è la directory da cui avete lanciato il programma. La funzione cwd() del modulo Cwd distribuito con Perl restituisce il nome della directory di esecuzione corrente. La funzione predefinita chdir tenta di cambiare la directory di esecuzione corrente. Eseguire il codice nella directory corretta è essenziale per manipolare i file con path relativi.

Moduli

Molte persone considerano CPAN (CPAN) come il principale punto di forza di Perl 5. Essenzialmente CPAN è un sistema per trovare e installare moduli. Un modulo è un package contenuto in un proprio file caricabile con use o require. Ogni modulo deve contenere codice Perl 5 valido e deve terminare con un'espressione che valuta ad un valore vero, in modo che il parser Perl 5 possa sapere se il caricamento e la compilazione del modulo hanno avuto successo. Non ci sono altri requisiti, ma solo delle convenzioni molto consolidate.

Quando caricate un modulo, Perl spezza il nome del package in componenti separati da doppi due punti (::) e li usa per ricavare un path di file. In pratica, l'istruzione use StranaBestia; fa sì che Perl cerchi un file di nome StranaBestia.pm in ogni directory elencata in @INC, nell'ordine, fino a che lo trova oppure esaurisce la lista.

Analogamente, use StranaBestia::Persistenza; fa sì che Perl cerchi un file di nome Persistenza.pm in ogni directory di nome StranaBestia/ presente in una qualunque directory elencata in @INC. use StranaBestia::UI::SmartPhone; fa sì che Perl cerchi un path relativo di file StranaBestia/UI/SmartPhone.pm in ogni directory in @INC, e così via.

Il file risultante può contenere o meno una dichiarazione di package corrispondente al suo nome—non vi è un requisito tecnico in questo senso—ma ovvie considerazioni di manutenibilità raccomandano di seguire tale convenzione.

Trucchi con perldoc

perldoc -l Nome::Modulo visualizza il path completo del file .pm corrispondente, purché tale file contenga della documentazione per il modulo. perldoc -lm Nome::Modulo visualizza il path completo del file .pm, indipendentemente dall'esistenza di un file .pod corrispondente. perldoc -m Nome::Modulo visualizza il contenuto del file .pm.

Uso e Importazione

Quando caricate un modulo con use, Perl lo carica dal disco e poi chiama il suo metodo import(), passandogli gli eventuali argomenti che avete specificato. Per convenzione, il metodo import() di un modulo riceve una lista di nomi e esporta funzioni e altri simboli nel namespace chiamante. Si noti che questa è meramente una convenzione; un modulo potrebbe non fornire import() oppure il suo import() potrebbe avere un comportamento diverso. Le direttive (Direttive) come strict usano gli argomenti per modificare il comportamento dello scope lessicale chiamante anziché per esportare simboli:

    use strict;
    # ... chiama strict->import()

    use CGI ':standard';
    # ... chiama CGI->import( ':standard' )

    use feature qw( say switch );
    # ... chiama feature->import( qw( say switch ) )

L'istruzione predefinita no chiama il metodo unimport() di un modulo, se esiste, e gli passa gli eventuali argomenti. Viene usata solitamente con le direttive che modificano qualche comportamento con import():

    use strict;
    # riferimenti simbolici e bareword non ammessi
    # richiesta la dichiarazione delle variabili

    {
        no strict 'refs';
        # riferimenti simbolici ammessi
        # strict 'subs' e 'vars' ancora attive
    }

Sia use che no hanno effetto durante la compilazione, per cui:

    use Nome::Modulo qw( lista di argomenti );

... equivale a:

    BEGIN
    {
        require 'Nome/Modulo.pm';
        Nome::Modulo->import( qw( lista di argomenti ) );
    }

Analogamente:

    no Nome::Modulo qw( lista di argomenti );

... equivale a:

    BEGIN
    {
        require 'Nome/Modulo.pm';
        Nome::Modulo->unimport(qw( lista di argomenti ));
    }

... inclusa la require del modulo.

Nessun Problema di Metodi Mancanti

Se import() o unimport() non esistono nel modulo, Perl non genera un messaggio di errore. Questi metodi sono realmente opzionali.

Se volete, potete chiamare direttamente import() e unimport(), sebbene abbia poco senso farlo al di fuori di un blocco BEGIN; dopo il termine della compilazione, import() e unimport() potrebbero non avere alcun effetto.

Le istruzioni use e require di Perl 5 distinguono tra maiuscole e minuscole, ma il fatto che Perl sappia distinguere tra strict e Strict non significa che lo stesso valga per la vostra combinazione di sistema operativo e file system. Se scrivete use Strict;, Perl non troverà strict.pm su un filesystem che distingue tra maiuscole e minuscole. Se invece il filesystem non distingue tra maiuscole e minuscole, Perl non avrà obiezioni a caricare Strict.pm, ma quando verrà invocato Strict->import() non accadrà nulla (strict.pm dichiara un package di nome strict).

I programmi portabili sono restrittivi sulla distinzione tra maiuscole e minuscole anche quando non è necessario.

Esportazione

Un modulo può rendere alcuni simboli globali disponibili ad altri package attraverso un processo noto come esportazione—un processo avviato chiamando import() implicitamente o direttamente.

Il modulo Exporter distribuito con Perl fornisce un meccanismo standard per esportare simboli da un modulo. Exporter dipende dalla presenza di variabili globali di package—in particolare @EXPORT_OK e @EXPORT—che contengono una lista di simboli da esportare su richiesta.

Considerate un modulo StranaBestia::Servizi che fornisce svariate funzioni indipendenti le une dalle altre e utilizzabili in tutto il sistema:

    package StranaBestia::Servizi;

    use Exporter 'import';

    our @EXPORT_OK = qw( gira traduci gratta );

    ...

Ogni pezzo di codice può ora usare questo modulo e, facoltativamente, importare alcune delle tre funzioni esportate (o tutte e tre). Potete anche esportare variabili:

    push @EXPORT_OK, qw( $ragno $scimmia $scoiattolo );

I simboli esportati per default vanno elencati in @EXPORT anziché in @EXPORT_OK:

    our @EXPORT = qw( danza_bestiale sonno_bestiale );

... in modo che use StranaBestia::Servizi; importi entrambe le funzioni. Ricordate che, se specificate dei simboli da importare, i simboli di default non verranno importati; in tale caso, otterrete solo ciò che chiedete. Per caricare un modulo senza importare alcun simbolo fornite esplicitamente una lista vuota:

    # rendi il modulo disponibile, ma non import()are simboli
    use StranaBestia::Servizi ();

Indipendentemente dalle liste di importazione, potete sempre chiamare le funzioni in un altro package con i loro nomi qualificati:

    StranaBestia::Servizi::gratta();

Esportazione Semplificata

Il modulo CPAN Sub::Exporter fornisce un'interfaccia migliore per esportare funzioni senza usare variabili globali di package. Esso offre anche opzioni più potenti. Tuttavia, mentre Exporter può esportare variabili, Sub::Exporter può esportare solo funzioni.

Organizzare il Codice con i Moduli

Perl 5 non vi richiede di usare i moduli, né i package, né i namespace. Potete scrivere tutto il vostro codice in un unico file .pl o in svariati file .pl di cui fare la require dove necessario. Avete a disposizione abbastanza flessibilità per gestire il vostro codice nel modo più appropriato a seconda del vostro stile di programmazione, del livello di formalità, rischio e valore del progetto, della vostra esperienza e della vostra dimestichezza con la distribuzione del software in Perl 5.

Tuttavia, un progetto con più di un paio di centinaia di linee di codice può avvantaggiarsi in molti modi di un'organizzazione a moduli:

Anche se non adottate un approccio orientato agli oggetti, modellare ogni entità e responsabilità distinta del vostro sistema con il proprio modulo tiene insieme le parti di codice legate tra di loro e le separa dalle altre.

Distribuzioni

Il modo più semplice per gestire la configurazione, il build, l'impacchettamento, il testing e l'installazione del software è quello di seguire le convenzioni per le distribuzioni CPAN. Una distribuzione è una collezione di metadati e di uno o più moduli (Moduli) che formano un'unità redistribuibile, testabile e installabile.

Tutte queste linee guida—per l'impacchettamento di una distribuzione, per la risoluzione delle sue dipendenze, per determinare dove installare software, per verificare che funzioni, per visualizzare la documentazione, per gestire un archivio—sono emerse dal generale consenso di migliaia di collaboratori impegnati a lavorare su decine di migliaia di progetti. Una distribuzione che segue gli standard CPAN può essere testata su svariate versioni di Perl 5 e svariate piattaforme hardware entro poche ore da quando viene caricata—il tutto senza necessità dell'intervento umano e con la segnalazione automatica agli autori di eventuali errori.

Anche se decidete di non rilasciare il vostro codice come distribuzione pubblica su CPAN, potete usare gli strumenti e le convenzioni di CPAN anche per gestire il vostro codice privato. La comunità Perl ha creato un'eccellente infrastruttura; perché non dovreste avvantaggiarvene?

Attributi di una Distribuzione

Oltre ad uno o più moduli, una distribuzione contiene svariati altri file e directory:

Una distribuzione ben-formata deve contenere un nome univoco e un singolo numero di versione (spesso derivato dal suo modulo pricipale). Ogni distribuzione pubblica che scaricate da CPAN dovrebbe rispettare questi standard. Il servizio pubblico CPANTS (http://cpants.perl.org/) valuta ogni distribuzione caricata in base alle linee guida di impacchettamento e alle convenzioni, e suggerisce eventuali migliorie. Se il codice segue le linee guida di CPANTS non significa che funzioni, ma che gli strumenti di impacchettamento di CPAN dovrebbero essere in grado di gestire tale distribuzione.

Strumenti CPAN per Gestire le Distribuzioni

Perl 5 vi mette direttamente a disposizione svariati strumenti per installare, sviluppare e gestire le vostre distribuzioni:

Inoltre, potete installare molti moduli CPAN che rendono più facile la vostra vita di sviluppatori:

Progettazione delle Distribuzioni

Il processo di progettazione di una distribuzione meriterebbe un libro a sé (vedete Writing Perl Modules for CPAN di Sam Tregar), ma alcuni principi possono comunque esservi utili. Avviate il vostro progetto con una utility come Module::Starter o Dist::Zilla. Il costo iniziale per apprenderne la configurazione e le regole può sembrare un investimento faticoso, ma i benefici di avere ogni cosa impostata correttamente (e, nel caso di Dist::Zilla, sempre aggiornata) vi solleva da gran parte del lavoro gestionale più noioso.

A questo punto, considerate le seguenti regole:

Il Package UNIVERSAL

Il package UNIVERSAL predefinito in Perl 5 è l'antenato comune di tutti gli altri package—in un senso orientato agli oggetti (Moose). UNIVERSAL fornisce alcuni metodi che i suoi derivati possono ereditare o sovrascrivere.

Il Metodo isa()

Il metodo isa() riceve una stringa contenente il nome di una classe o di un tipo predefinito. Potete invocarlo come metodo di classe o di istanza su un oggetto. Restituisce un valore vero se il suo invocante è la classe data o una sua derivata, oppure se il suo invocante è un riferimento blessed al tipo dato.

Dato un oggetto $pepe (un riferimento a hash su cui si è fatto il bless alla classe Scimmia, che deriva dalla classe Mammifero):

    say $pepe->isa(   'Scimmia'   );  # stampa 1
    say $pepe->isa(   'Mammifero' );  # stampa 1
    say $pepe->isa(   'HASH'      );  # stampa 1
    say Scimmia->isa( 'Mammifero' );  # stampa 1

    say $pepper->isa( 'Delfino'   );  # stampa 0
    say $pepper->isa( 'ARRAY'     );  # stampa 0
    say Scimmia->isa( 'HASH'      );  # stampa 0

I tipi predefiniti di Perl 5 sono SCALAR, ARRAY, HASH, Regexp, IO e CODE.

Ogni classe può sovrascrivere isa(). Ciò può essere utile quando si usano oggetti emulati (vedete Test::MockObject e Test::MockModule su CPAN) e con il codice che non usa i ruoli (Ruoli). Tenete presente che ciascuna classe che sovrascrive isa() deve avere in genere una buona ragione per farlo.

Il Metodo can()

Il metodo can() riceve una stringa contenente il nome di un metodo. Restituisce un riferimento alla funzione che implementa tale metodo, se esso esiste. Altrimenti, restituisce un valore falso. Potete chiamarlo su una classe, su un oggetto o sul nome di un package. In quest'ultimo caso, viene restituito un riferimento a una funzione anziché a un metodo ... anche se non potete distinguerli, dato tale riferimento..

Questa Classe Esiste?

Anche se UNIVERSAL::isa() e UNIVERSAL::can() sono metodi (Equivalenza Metodi-Funzioni), potete tranquillamente usare quest'ultimo come funzione allo scopo di determinare se una classe esiste in Perl 5. Se UNIVERSAL::can( $nomeclasse, 'can' ) restituisce un valore vero, significa che qualcuno da qualche parte ha definito una classe di nome $nomeclasse. Tale classe potrebbe non essere utilizzabile, ma esiste.

Data una classe di nome ScimmiaRagno con un metodo di nome strilla, potete ottenere un riferimento al metodo con:

    if (my $metodo = ScimmiaRagno->can( 'strilla' )) {...}

    if (my $metodo = $sm->can( 'strilla' )
    {
        $sm->$metodo();
    }

Usate can() per testare se un package implementa una specifica funzione o metodo:

    use Class::Load;

    die "Caricamento di $modulo non riuscito!"
        unless load_class( $modulo );

    if (my $registra = $modulo->can( 'registra' ))
    {
        $registra->();
    }

Module::Pluggable

Mentre il modulo CPAN Class::Load semplifica il caricamento di classi per nome—evitando il balletto dei requireModule::Pluggable vi solleva da gran parte del lavoro necessario per creare e gestire sistemi di plugin. Imparate a usare entrambe le distribuzioni.

Il Metodo VERSION()

Il metodo VERSION() restituisce il valore della variabile $VERSION per il package o la classe appropriata. Se fornite un numero di versione come parametro opzionale, il metodo solleva un'eccezione quando la variabile $VERSION considerata non è uguale o maggiore al parametro.

Dato un modulo ScimmiaUrlatrice con versione 1.23:

    say ScimmiaUrlatrice->VERSION();    # stampa 1.23
    say $su->VERSION();                 # stampa 1.23
    say $su->VERSION( 0.0  );           # stampa 1.23
    say $su->VERSION( 1.23 );           # stampa 1.23
    say $su->VERSION( 2.0  );           # eccezione!

Non ci sono molte ragioni validi per sovrascrivere VERSION().

Il Metodo DOES()

Il metodo DOES() è stato introdotto in Perl 5.10.0. Il suo scopo è quello di supportare l'uso dei ruoli (Ruoli) nei programmi. Passategli un invocante e il nome di un ruolo, e il metodo restituirà vero se la classe appropriata ha in qualche modo quel ruolo—attraverso l'ereditarietà, la delega, la composizione, l'applicazione di ruolo o un qualunque altro meccanismo.

L'implementazione di default di DOES() si riconduce a isa(), in quanto l'ereditarietà è uno dei meccanismi con cui una classe assume un ruolo. Data una scimmia Cappuccina:

    say Cappuccina->DOES( 'Scimmia'      );  # stampa 1
    say $cappy->DOES(     'Scimmia'      );  # stampa 1
    say Cappuccina->DOES( 'Invertebrato' );  # stampa 0

Sovrascrivete DOES() se fornite manualmente un ruolo o un altro comportamento allomorfico.

Estendere UNIVERSAL

Potreste essere tentati di memorizzare altri metodi in UNIVERSAL per renderli disponibili a tutte le altre classi e oggetti Perl 5. Resistete a questa tentazione; un comportamento globale può avere effetti collaterali insidiosi proprio perché non è vincolato.

Detto questo, talvolta può essere accettabile abusare di UNIVERSAL a scopo di debugging o per correggere un comportamento di default improprio. Per esempio, la distribuzione UNIVERSAL::ref di Joshua ben Jore rende usabile l'operatore ref() che normalmente è inutile. Le distribuzioni UNIVERSAL::can e UNIVERSAL::isa possono aiutarvi a fare il debug di errori di anti-polimorfismo (Equivalenza Metodi-Funzioni). Perl::Critic può rilevare questi ed altri problemi.

Al di fuori di codice molto controllato e di esigenze pragmatiche molto specifiche, non c'è ragione di aggiungere direttamente del codice a UNIVERSAL. Quasi sempre ci sono alternative di progettazione migliori.

Generazione di Codice

I programmatori principianti scrivono più codice del necessario, in parte perché non hanno familiarità col linguaggio, le librerie e gli idiomi, e in parte perché mancano di esperienza. Cominciano scrivendo lunghi pezzi di codice procedurale, poi scoprono le funzioni, poi i parametri, poi gli oggetti e—in qualche caso—le funzioni di ordine superiore e le chiusure.

Man mano che migliorate come programmatori, vi troverete a scrivere sempre meno codice per risolvere uno stesso problema. Userete delle astrazioni migliori e scriverete codice più generico. Potrete riutilizzare il codice—e quando arriverete ad aggiungere funzionalità cancellando del codice potrete considerarvi davvero soddisfatti.

La scrittura di programmi che scrivono programmi al vostro posto—detta metaprogrammazione o generazione di codice—offre ulteriori possibilità di astrazione. Anche se in questo modo potete fare enormi pasticci, potete anche creare cose eccellenti. Per esempio, ciò che rende possibile Moose (Moose) sono proprio delle tecniche di metaprogrammazione.

L'uso di AUTOLOAD (AUTOLOAD) per le funzioni e metodi mancanti è un esempio limitato di questa tecnica; il sistema di dispatch di funzioni e metodi di Perl 5 vi permette di controllare che cosa succede quando essi non vengono trovati normalmente.

eval

La tecnica più semplice di generazione di codice consiste nel definire una stringa contenente un pezzetto di codice Perl valido e compilarla con la forma dell'operatore eval che riceve una stringa. Contrariamente alla forma a blocco dell'operatore eval usata per catturare eccezioni, la eval di una stringa compila il suo contenuto nello scope corrente, che include il package corrente e le proprie istanze dei lessicali.

Un utilizzo comune di questa tecnica è quello di fornire una soluzione di ripiego quando non potete (o non volete) caricare una dipendenza opzionale:

    eval { require Scimmia::Rintracciata }
        or eval 'sub Scimmia::Rintracciata::log {}';

Se Scimmia::Rintracciata non è disponibile, questo codice fa in modo che la sua funzione log() esista, anche se non fa nulla. Tuttavia questo esempio è ingannevolmente semplice. Usare eval nel modo giusto richiede degli sforzi; dovete gestire i problemi di quotatura per poter includere delle variabili nel vostro codice passato a eval. Interpolare alcune variabili ma non altre è ancora più complesso:

    sub genera_accessori
    {
        my ($nomemet, $nomeattr) = @_;

        eval <<"FINE_ACCESSORE";
        sub leggi_$nomemet
        {
            my \$self = shift;
            return \$self->{$nomeattr};
        }

        sub scrivi_$nomemet
        {
            my (\$self, \$valore) = \@_;
            \$self->{$nomeattr}  = \$valore;
        }
    FINE_ACCESSORE
    }

Guai a dimenticare un backslash! E buona fortuna a spiegare al vostro evidenziatore di sintassi che cosa state facendo! Peggio ancora, ogni invocazione di eval su stringa genera una nuova struttura dati che rappresenta l'intero codice, e anche compilarla non è certo gratis. Pur con tutti i suoi limiti questa tecnica ha però il vantaggio di essere semplice.

Chiusure Parametriche

Mentre creare semplici accessori e mutatori con eval è banale, le chiusure (Chiusure) vi permettono di aggiungere parametri al codice generato durante la sua compilazione, senza richiedere ulteriori elaborazioni:

    sub genera_accessori
    {
        my $nomeattr = shift;

        my $lettore = sub
        {
            my $self = shift;
            return $self->{$nomeattr};
        };

        my $scrittore = sub
        {
            my ($self, $valore) = @_;
            $self->{$nomeattr} = $valore;
        };

        return $lettore, $scrittore;
    }

Questo codice evita i noiosi problemi di quotatura e compila ogni chiusura una sola volta. Usa anche meno memoria dato che tutte le istanze della chiusura condividono il codice compilato. L'unica cosa che differisce tra due istanze è il lessicale $nomeattr associato. In un processo con un lungo tempo di vita o con molti accessori questa tecnica può rivelarsi molto utile.

Installare delle chiusure nella tabella dei simboli è piuttosto semplice, anche se poco elegante:

    {
        my ($leggi, $scrivi) = genera_accessori( 'torta' );

        no strict 'refs';
        *{ 'leggi_torta' } = $leggi;
        *{ 'scrivi_torta' } = $scrivi;
    }

La strana sintassi con l'asterisco Pensate ad esso come a un sigillo di typeglob, dove un typeglob è un'espressione gergale Perl che sta per "symbol table". che dereferenzia un hash si riferisce alla tabella dei simboli corrente, che è una porzione del namespace corrente contenente i simboli accessibili globalmente come le variabili globali di package, le funzioni e i metodi. Assegnando un riferimento ad un elemento della tabella dei simboli si effettua l'installazione o la sostituzione di tale elemento. Per promuovere una funzione anonima a metodo memorizzate il riferimento a tale funzione nella tabella dei simboli.

Tabelle dei Simboli Facilitate

Il modulo CPAN Package::Stash offre un'interfaccia migliore per la manipolazione delle tabelle dei simboli.

L'assegnamento in una tabella dei simboli tramite stringa, anziché tramite un nome letterale di variabile, è detto riferimento simbolico. Durante questa operazione dovete disattivare il controllo dei riferimenti effettuato da strict. Molti programmi contengono un errore insidioso in pezzi di codice simili a quello sopra, quando generano la chiusura e fanno l'assegnamento in un'unica linea:

    {
        no strict 'refs';

        *{ $nomemet } = sub {
            # errore insidioso: strict refs e` disattivato anche qui
        };
    }

Questo esempio disattiva le restrizioni sia nel blocco esterno che nel corpo stesso della funzione. Dato che a violare le restrizioni dei controlli sui riferimenti è soltanto l'assegnamento, è meglio disattivarle soltanto durante tale operazione.

Se il nome del metodo è un letterale stringa nel vostro codice sorgente, anziché il valore contenuto in una variabile, potete fare direttamente l'assegnamento al simbolo rilevante:

    {
        no warnings 'once';
        (*leggi_torta, *scrivi_torta) =
             genera_accessori( 'torta' );
    }

Assegnare direttamente al glob non viola le restrizioni, ma menzionare ciascun glob una sola volta genera invece un warning di "used only once" (usato una sola volta, NdT) a meno che lo sopprimiate esplicitamente nel relativo scope.

Manipolazioni Durante la Compilazione

Diversamente dal codice scritto esplicitamente, il codice generato con una eval di stringa viene compilato durante l'esecuzione. Mentre potete ragionevolmente assumere che una funzione normale sia disponibile durante l'intera vita del vostro programma, una funzione generata potrebbe non essere disponibile quando vi serve.

Potete forzare Perl ad eseguire il codice che genera altro codice durante la compilazione racchiudendolo in un blocco BEGIN. Quando il parser Perl 5 incontra un blocco etichettato con BEGIN, esegue il parsing dell'intero blocco. Se il blocco non contiene errori di sintassi viene eseguito immediatamente. Al termine dell'esecuzione, il parsing continua come se non fosse stato interrotto.

La differenza tra scrivere:

    sub leggi_eta   { ... }
    sub scrivi_eta  { ... }

    sub leggi_nome  { ... }
    sub scrivi_nome { ... }

    sub leggi_peso  { ... }
    sub scrivi_peso { ... }

... e:

    sub crea_accessori { ... }

    BEGIN
    {
        for my $accessore (qw( eta nome peso ))
        {
            my ($leggi, $scrivi) =
                crea_accessori( $accessore );

            no strict 'refs';
            *{ 'leggi_' . $accessore  } = $leggi;
            *{ 'scrivi_' . $accessore } = $scrivi;
        }
    }

... è essenzialmente una differenza nel grado di manutenibilità.

In un modulo, ogni pezzo di codice al di fuori delle funzioni viene eseguito quando fate la use del modulo, poiché Perl aggiunge implicitamente un BEGIN che racchiude la require e la import (Importazione). Il codice del modulo al di fuori delle funzioni viene eseguito prima della chiamata a import(). Se fate soltanto la require del modulo, non viene generato implicitamente un blocco BEGIN. In tal caso, il codice al di fuori delle funzioni viene eseguito alla fine del parsing.

Fate attenzione alle interazioni tra una dichiarazione lessicale (l'associazione di un nome ad uno scope) e un assegnamento lessicale. La prima avviene durante la compilazione, mentre il secondo avviene al momento dell'esecuzione. Il seguente codice contiene un errore insidioso:

    # aggiunge un metodo require() a UNIVERSAL
    use UNIVERSAL::require;

    # errato; non usatelo
    my $package_desiderato = 'Scimmia::A::Propulsione';

    BEGIN
    {
        $package_desiderato->require();
        $package_desiderato->import();
    }

... dato che il blocco BEGIN viene eseguito prima dell'assegnamento del valore stringa a $package_desiderato. Il risultato è un'eccezione relativa al tentativo di invocare il metodo require() su un valore non definito.

Class::MOP

Contrariamente alla possibilità di installare riferimenti a funzione per popolare dei namespace e creare dei metodi, non esiste un modo semplice per creare delle classi da programma in Perl 5. Fortunatamente, Moose ci viene in soccorso con la sua libreria Class::MOP che fornisce il protocollo di meta-oggetti—un meccanismo grazie al quale si possono creare sistemi ad oggetti che manipolano se stessi.

Anziché scrivere il vostro codice in una stringa potenzialmente sbagliata da passare ad eval oppure tentare di accedere manualmente alle tabelle dei simboli, potete manipolare le entità e le astrazioni del vostro programma attraverso oppurtuni oggetti e metodi.

Per creare una classe, scrivete:

    use Class::MOP;

    my $classe = Class::MOP::Class->create(
                     'Scimmia::Cacciavite'
                 );

Potete aggiungere attributi e metodi alla classe quando la create:

    my $classe = Class::MOP::Class->create(
        'Scimmia::Cacciavite' =>
        (
            attributes =>
            [
                Class::MOP::Attribute->new('$materiale'),
                Class::MOP::Attribute->new('$colore'),
            ]
            methods =>
            {
                stringi  => sub { ... },
                allenta  => sub { ... },
            }
        ),
    );

... oppure attraverso la metaclasse (l'oggetto che rappresenta la classe) dopo la creazione:

    $classe->add_attribute(
        esperienza  => Class::MOP::Attribute->new('$xp')
    );

    $classe->add_method( uccidi_zombie => sub { ... } );

... e potete ispezionare la metaclasse:

    my @attr = $class->get_all_attributes();
    my @met  = $class->get_all_methods();

Analogamente, Class::MOP::Attribute e Class::MOP::Method vi consentono di creare, manipolare e fare introspezione su attributi e metodi.

Overloading

Perl 5 non è un linguaggio orientato agli oggetti in modo pervasivo. I suoi tipi di dati predefiniti (scalari, array e hash) non sono oggetti con metodi di cui si può fare l'overload, ma potete controllare il comportamento delle vostre classi e oggetti, specialmente quando ne fate la coercizione o la valutazione contestuale, con l'overloading.

L'utilità dell'overloading è sottile, ma potente. Un esempio interessante è l'overloading del comportamento di un oggetto in contesto booleano, specialmente quando usate una tecnica come il pattern dell'Oggetto Nullo (http://www.c2.com/cgi/wiki?NullObject). In contesto booleano , un oggetto valuta a vero, a meno che facciate l'overload della sua conversione a booleano.

Potete fare l'overload del comportamento dell'oggetto per quasi tutte le operazioni e le coercizioni: conversione a stringa, numero e booeano, iterazione, invocazione, accesso ad array, accesso ad hash, operazioni aritmetiche, operatori di confronto, match intelligente, operazioni bit-a-bit e persino assegnamenti. La conversione a stringa, numero e booeano sono i casi più importanti e di uso più comune.

Overloading di Operazioni Comuni

La direttiva overload vi permette di associare una funzione a un'operazione di cui potete fare l'overload passando delle coppie di argomenti, in cui la chiave è il tipo di overload e il valore è un riferimento alla funzione da chiamare per effettuare l'operazione. Una classe Nullo che fa l'overload della valutazione booleana in modo che restituisca sempre un valore falso potrebbe essere definita come segue:

    package Nullo
    {
        use overload 'bool' => sub { 0 };

        ...
    }

A questo punto è facile aggiungere la conversione a stringa:

    package Nullo
    {
        use overload
            'bool' => sub { 0 },
            '""'   => sub { '(null)' };
    }

Fare l'overload della conversione a numero è più complesso, perché gli operatori binari sono generalmente binari (Arità). Dati due operandi di cui si è fatto l'overload dei metodi per l'addizione, quale dei due ha la precedenza? La risposta deve essere coerente, facile da illustrare e comprensibile anche alle persone che non hanno letto il codice sorgente dell'implementazione.

perldoc overload tenta di spiegare questo aspetto nelle sezioni Calling Conventions for Binary Operations e MAGIC AUTOGENERATION, ma la soluzione più semplice consiste nel fare l'overload della conversione a numero (denotata con '0+') e specificare a overload di usare altri overload come ripiego ogni volta che è possibile:

    package Nullo
    {
        use overload
            'bool'   => sub { 0 },
            '""'     => sub { '(null)' },
            '0+'     => sub { 0 },
            fallback => 1;
    }

QUando impostate fallback a un valore vero, Perl utilizzerà, se possibile, un qualunque altro overload definito per comporre l'operazione richiesta. Se non è possibile, Perl si comporterà come se non ci fossero degli overload attivi, il che è spesso ciò che desiderate.

Senza fallback, Perl userà soltanto gli overload specifici che avete fornito. Se qualcuno tenta di eseguire un'operazione della quale non avete fatto l'overload viene sollevata un'eccezione.

Overload e Ereditarietà

Le classi derivate ereditano gli overload dalle proprie superclassi. Possono ridefinire questo comportamento in due modi. Se la superclasse usa l'overloading come illustrato sopra, fornendo direttamente dei riferimenti a funzione, la classe derivata deve ridefinire il comportamento di overload della superclasse usando direttamente overload.

Le superclassi possono fornire alle classi discendenti maggiore flessibilità specificando il nome di un metodo da chiamare per implementare l'overloading, anziché condificare direttamente un riferimento a funzione:

    package Nullo
    {
        use overload
            'bool'   => 'conv_bool',
            '""'     => 'conv_stringa',
            '0+'     => 'conv_num',
            fallback => 1;
    }

In questo caso, ciascuna classe derivata può effettuare in modo diverso le operazioni coinvolte nell'overload sovrascrivendo i metodi con nomi appropriati.

Usi dell'Overloading

Potete essere tentati di usare l'overloading come strumento per definire delle abbreviazioni simboliche per nuove operazioni, ma raramente ci sono buone ragioni per farlo in Perl 5. La distribuzione CPAN IO::All sfrutta questa idea al limite per produrre idee intelligenti di scrittura di codice conciso e componibile. Tuttavia, per ogni API raffinata con un brillante uso dell'overloading, ne vengono create una dozzina che sono invece dei pasticci. Talvolta il miglior codice evita gli eccessi di ingegno in favore della semplicità.

Ridefinire l' addizione, la moltiplicazione anche la concatenazione su una classe Matrice ha senso, perché la notazione esistente per tali operazioni è molto diffusa. Il dominio di un nuovo problema per cui non esista una notazione consolidata non è un buon candidato per l'overloading, come non lo è il dominio di un problema per cui dovete fare salti mortali per travare le corrispondenze tra gli operatori esistenti in Perl e una diversa notazione.

Perl Best Practices di Damian Conway suggerisce un altro uso dell'overloading, per prevenire l'abuso accidentale di oggetti. Per esempio, fare l'overloading della conversione a numero in modo da invocare croak() per gli oggetti che non hanno un'unica rappresentazione numerica ragionevole può aiutarvi a trovare e risolvere bug.

Taint

Perl fornisce strumenti per scrivere programmi sicuri. Tali strumenti non sostituiscono un'attenta analisi e pianificazione, ma possono ricompensare la vostra cautela e comprensione dei problemi aiutandovi ad evitare errori insidiosi.

Uso del Modo Taint

Il modo taint (o taint, marchio NdT) aggiunge metadati a tutti i dati che provengono dall'esterno del vostro programma. Ogni dato derivato da dati marchiati è anch'esso marchiato. Potete usare dati marchiati nel vostro programma, ma se li usate in operazioni che hanno effetti sul mondo esterno—ovvero se li usate in modo non sicuro—Perl solleva un'eccezione fatale.

perldoc perlsec spiega il modo taint in grande dettaglio.

Per attivare il modo taint, lanciate il vostro programma con l'argomento a linea di comando -T. Se passate questo argomento nella linea #! di un programma, dovete poi eseguire direttamente il programma; se invece lo lanciate con perl miaapptainted.pl, senza il flag -T, Perl termina con un'eccezione. Infatti, nel momento in cui Perl incontra il flag nella linea #! è già troppo tardi, ad esempio, per marchiare i dati d'ambiente di %ENV.

Origine dei Dati Marchiati

I dati marchiati hanno due origini: i dati provenienti da un qualsiasi canale di input e l'ambiente operativo del programma. Il primo consiste in qualunque cosa leggiate da un file o riceviate dagli utenti nel caso di programmi web o di rete. Il secondo include gli argomenti a linea di comando, le variabili d'ambiente e i dati restituiti da chiamate di sistema. Anche un'operazione come la lettura da un directory handle produce dati marchiati.

La funzione tainted() del modulo Scalar::Util distribuito con Perl restituisce vero se il suo argomento è marchiato:

    die 'Oh no! Dati marchiati!'
        if Scalar::Util::tainted( $valore_sospetto );

Rimuovere il Taint dai Dati

Per rimuovere un taint, dovete estrarre delle porzioni sicure dei dati con una cattura in un'espressione regolare. I dati catturati non sono marchiati. Se il vostro input utente consiste di un numero telefonico americano, potete rimuoverne il taint con:

    die 'Numero ancora marchiato!'
        unless $numero =~ /(\(/d{3}\) \d{3}-\d{4})/;

    my $numero_sicuro = $1;

Più è specifico il pattern che specifica ciò che è ammissibile, più sicuro sarà il vostro programma. L'approccio opposto di rifiutare elementi o formati specifici corre il rischio di trascurare dei pericoli. È molto meglio rifiutare qualcosa che era sicuro ma inatteso che ammettere qualcosa di pericoloso che sembra sicuro. Naturalmente, nessuno vi vieta di effettuare la cattura dell'intero contenuto di una variabile—ma in tal caso, perché usare taint?

Rimuovere il Taint dall'Ambiente

La variabile globale predefinita %ENV rappresenta le variabili d'ambiente del sistema. Tali dati sono marchiati poiché i loro valori sono controllati da forze esterne al programma. Ogni variabile d'ambiente che modifica come Perl o la shell trovano file e directory sono potenziali vettori di attacchi. Un programma attento al taint dovrebbe cancellare svariate chiavi da %ENV e impostare $ENV{PATH} a un path specifico e sicuro:

    delete @ENV{ qw( IFS CDPATH ENV BASH_ENV ) };
    $ENV{PATH} = '/path/ai/file/eseguibili/';

Se non impostate $ENV{PATH} in modo appropriato, riceverete messaggi a proposito della sua scarsa sicurezza. Se tale variabile d'ambiente conteneva la directory d'esecuzione corrente o delle directory relative, oppure se le directory specificate permettono a tutti la scrittura, un attacco intelligente potrebbe dirottare le chiamate di sistema per perpetrare i suoi misfatti.

Per ragioni simili, @INC non contiene la directory di esecuzione corrente quando è attivo il modo taint. Perl ignora anche le variabili d'ambiente PERL5LIB e PERLLIB. Usate la direttiva lib o il flag -I di perl per aggiungere le directory di librerie al programma.

Trappole di Taint

Il modo taint è del tipo "tutto o nulla". O è attivo o non lo è. Talvolta questo induce le persone a usare dei pattern permissivi per rimuovere il taint dai dati, dando un'errata illusione di sicurezza. Considerate attentamente la rimozione del taint.

Sfortunatamente, non tutti i moduli gestiscono i dati marchiati in modo appropriato. Questo è un problema che gli autori CPAN dovrebbero considerare seriamente. Se dovete attivare i controlli di taint in codice preesistente, potete usare il flag -t, che attiva il modo taint ma riduce le violazione di taint da eccezioni a semplici warning. Questo non sostituisce ovviamente il modo taint completo, ma vi permette di mettere in sicurezza programmi esistenti senza la rigidità dell'approccio "tutto o nulla" di -T.