Flavio Poletti
Puoi ripetere?
http://www.perl.it/documenti/articoli/2006/07/puoi-ripetere.html
© Perl Mongers Italia. Tutti i diritti riservati.

Vincitore Contest2005Perl è un linguaggio molto ridondante e trasversale, nel senso che mette a disposizione moltissimi costrutti per ottenere lo stesso risultato finale. Questa caratteristica deriva da un vincolo progettuale posto da Larry Wall - ma non parleremo di questo. Come per altri costrutti, anche per l'iterazione è stato dato libero sfogo alla fantasia.

Non tutti i costrutti di iterazione, però, sono equivalenti fra di loro: molti, al contrario, costituiscono delle specializzazioni che si rivelano assai utili in contesti molto particolari. Anche questa è una conseguenza del vincolo di progetto posto da Wall: se devo andare da un angolo di una stanza a quello opposto di solito percorro la diagonale (se possibile), non mi sposto adiacente alle pareti; in questo caso, alcuni tipi di iterazione sono stati specializzati per consentire di risolvere determinati problemi in maniera rapida e concisa.

while, il sempreverde

Il primo costrutto di iterazione - quello per eccellenza, potremmo dire - è while. Di una semplicità disarmante:

 ETICHETTA while (ESPRESSIONE) BLOCCO

ove:

  • ETICHETTA è qualcosa di cui ci preoccuperemo più tardi;
  • ESPRESSIONE è una qualsiasi espressione che possa essere ricondotta ad un valore vero o falso (quest'ultimo rappresentato dal numero 0 intero, dalla stringa '0', dalla stringa vuota, dal valore undef o da una lista vuota);
  • BLOCCO è qualcosa che comincia e finisce con delle parentesi graffe.

Il significato è semplice: mentre l'ESPRESSIONE è vera, viene eseguito il BLOCCO; quando diventa falsa si procede oltre. Questo vuol dire che, se l'ESPRESSIONE è falsa da subito, il BLOCCO non viene eseguito. Quindi:

 my $indice = 0;
 while ($indice < 3) {
    print("\$indice vale $indice\n");
    ++$indice;
 }
 print("basta!\n");

stampa:

 $indice vale 0
 $indice vale 1
 $indice vale 2
 basta!

mentre;

 my $indice = 3;
 while ($indice < 3) {
    print("\$indice vale $indice\n");
    ++$indice;
 }
 print("basta!\n");

stampa solo basta!, perché la condizione è falsa sin dall'inizio.

Occorre prestare attenzione al fatto che a volte potreste vedere una lista dove, in realtà... non c'è! Normalmente, infatti, la "virgola" è un operatore vero e proprio che serve a costruire liste:

 my $porzione = substr 'ciao_belli', 0, 4;

In questo caso, la sequenza dei parametri dati a substr:

 'ciao_belli', 0, 4

è una lista, e l'operatore "virgola" serve a separare i vari elementi della stessa. È proprio così, per costruire una lista non si usano le parentesi, ma le virgole! L'uso delle parentesi è a volte necessario per risolvere la precedenza degli operatori, come nel seguente esempio:

 my @unico_elemento = 'ciao', 'a', 'tutti';

In questo caso, infatti, il segno di uguale dell'assegnazione ha una precedenza maggiore dell'operatore virgola, quindi è come se avessimo scritto:

 ((my (@unico_elemento) = 'ciao'), 'a', 'tutti');

Una sorpresa più insidiosa la troviamo con il while ( d'ora innanzi metteremo l'output immediatamente dopo il programma, separati da __END__):

 my $indice = 1;
 while (1, 1, $indice) {
         print("\$indice vale $indice\n");
         $indice = 0;
 }
 print("basta!\n");
 
 __END__
 
 $indice vale 1
 basta!

Qui abbiamo una falsa lista di tre elementi: in realtà si tratta di un'espressione composita, che valutata in contesto booleano (come nel caso di while) si riduce al valore dell'ultima sotto-espressione. Nel caso considerato, infatti, quando $indice è posto a zero il ciclo termina.

Diverso è il caso in cui si utilizzi un array:

 my @array = ( 1, 0, 1 );
 while ( @array ) {
         print("\@array vale ( @array )\n");
         pop @array;
 }
 print("basta array!\n");
 
 __END__
 
 @array vale ( 1 0 1 )
 @array vale ( 1 0 )
 @array vale ( 1 )
 basta array!

Da notare che i tre valori (1, 0 ed 1) sono inseriti, questa volta, in una lista: le parentesi "proteggono" infatti le due virgole, costruttrici di lista. Qui il ciclo non si arresta quando l'ultimo elemento è nullo, ma solo quando l'array diventa vuoto. Questo accade perché gli array, valutati in contesto booleano/scalare, restituiscono il numero di elementi presenti, ed una espressione booleana è un caso particolare di valutazione in un contesto scalare.

Un po' di magia Perl

Come detto, Wall ha voluto creare un linguaggio che consentisse di prendere le scorciatoie, per quanto diagonali fossero. Lo scopo è scrivere il meno possibile le cose che si fanno più spesso. Poiché uno dei retroscena più classici di Perl è l'analisi di testi o di file in generale, è allora ovvio che la lettura dei dati sia stata resa il più semplice possibile. while non è rimasto immune da questo processo, ed il seguente costrutto lo dimostra:

 while (<>) {
    s/ciao/CIAO/g;
    print;
 }

È un semplice filtro, che cambia tutti i ciao in ingresso in CIAO. Il problema è: su cosa stanno lavorando l'operatore di sostutuzione e la funzione print? Il manuale ci dà una mano: in mancanza di specifica, questi due lavorano sulla variabile jolly $_. Bene, obietterete: chi l'ha mai inizializzata? La risposta è semplice: ci ha pensato while.

Ogni volta che in Perl scrivete qualcosa tipo:

 while (<HANDLE>) {  # Puo` anche essere lessicale, come in <$handle>
    # Fai qualcosa con $_
 }

in realtà viene letto più o meno come segue:

 while (defined($_ = <HANDLE>)) { # o <$handle>, come sopra
    # Fai qualcosa con $_
 }

Ossia, in presenza di quel particolare modo di scrivere ESPRESSIONE, Perl prende l'iniziativa e:

  • ad ogni iterazione, prende una riga di ingresso (in accordo con il valore della variabile $/, ma questa è un'altra storia) e la assegna a $_;
  • verifica se il valore di $_ è definito, come prova che effettivamente si sia letto qualcosa ed il file non sia finito.

Attenzione: perché il trucco funzioni, ESPRESSIONE deve essere proprio come abbiamo scritto sopra. Se volete aggiungere altri test, come ad esempio:

 while ($pippo > 1 && <>) {
    # Peccato! non vale piu`!
 }

non avrete più il comportamento magico.

Vita reale: iterare su una hash

Avete una hash e dovete visitarla tutta, sia chiavi che valori. Che fate? Presto!

Arrivati a questo punto, avete poco da scegliere. Conoscete solo while! Ma come usarlo? Semplice, con l'aiuto di each:

 my %hash = (a => 'b', c => 'd', e => 'effe');
 while (my ($chiave, $valore) = each(%hash)) {
    print "chiave: $chiave; valore: $valore\n";
 }
 print("fine hash\n");
 
 __END__
 
 chiave: e; valore: effe
 chiave: c; valore: d
 chiave: a; valore: b
 fine hash

Questo ci insegna due cose:

  1. each mantiene un proprio stato interno per poter iterare sull'hash (non potrebbe essere altrimenti). Questo stato è condiviso da TUTTO il programma, e per quanto ce ne sia uno distinto per ciascuna hash, occorre prestare molta attenzione, perché chiamate a keys e values, ad esempio, resettano questo iteratore interno;
  2. non si può mai contare sull'ordine in cui gli elementi sono disposti in una hash. Questi sono disposti secondo un algoritmo che cerca di minimizzare gli accessi necessari per trovare un dato elemento; fare assunzioni sul fatto di trovare un particolare ordine equivale a chiedere guai.

In generale, fossi in voi eviterei ed aspetterei di sapere qualcosa di più su for e foreach.

Il fratellino bastian contrario

Se al posto di while mettete until, il senso dell'ESPRESSIONE booleana viene invertito, ossia si prosegue se ESPRESSIONE è falsa, e si esce dal ciclo se è vera. In tutto e per tutto i seguenti costrutti coincidono:

 ETICHETTA until (  ESPRESSIONE) BLOCCO
 ETICHETTA while (! ESPRESSIONE) BLOCCO

Le radici, inutile dirlo, sono linguistiche; se sia un bene o un male avere tutte queste varianti potete deciderlo solo voi.

I gemelli del giro: for e foreach

In italiano esistono pochi sinonimi in senso stretto, ossia parole che hanno lo stesso, identico significato. "tra" e "fra" sono sinonimi a tutti gli effetti; "bello" e "prestante" si avvicinano, ma vogliono significare due cose leggermente differenti.

Perl, viste le sue radici linguistiche, non fa eccezione. I costrutti di iterazione sono sinonimi in senso ampio: tutti inducono un significato di ripetizione, ognuno però con una sua cifra peculiare. Beh, anche Perl ha i suoi "tra" e "fra": sono for e foreach. Per quanto voi, nella vostra testa, possiate tendere ad assegnare significati differenti (ed in inglese lo hanno), per Perl sono la stessa, identica cosa. Provare per credere.

Se siete pigri, dunque, scrivete sempre for. Se amate un po' più la leggibilità, probabilmente in certi casi sarete più portati a scrivere foreach, ma ricordate: è tutto nella vostra testa (ed in quella di chi legge il vostro codice).

La cosa bella è che queste due parole chiave condividono due significati distinti. Per essere più chiari, for e foreach possono essere utilizzati mediante due stili di chiamata differenti (utilizzeremo nel seguito la convenzione della migliore leggibilità, ma sono identici!):

 ETICHETTA for (ESPRESSIONE1; ESPRESSIONE2; ESPRESSIONE3) BLOCCO
 ETICHETTA foreach VAR (LISTA) BLOCCO

Il primo è un figlio diretto del C, che ha inventato il ciclo for. Stesse modalità di utilizzo: ESPRESSIONE1 è un'inizializzazione, sempre eseguita; ESPRESSIONE2 viene valutata in un contesto booleano, e decide se si va avanti o si è scherzato; BLOCCO viene eseguito ogni volta che ESPRESSIONE2 dà il permesso; ESPRESSIONE3 viene eseguita subito dopo BLOCCO e prima di una nuova valutazione di ESPRESSIONE2. Ad esempio:

 for (my $indice = 0; $indice < 3; ++$indice) {
    print("\$indice vale $indice\n");
 }
 print("basta!\n");
 
 __END__
 
 $indice vale 0
 $indice vale 1
 $indice vale 2
 basta!

è un modo più conciso (ed elegante, e leggibile) di iterare su un indice rispetto a quanto fatto in precedenza con while. Non che while non serva egregiamente allo scopo: solo che per dire questa cosa in particolare è meglio utilizzare for, così come nel linguaggio naturale utilizzate la parola più adatta alle circostanze, se la sapete, altrimenti ripiegate su qualcosa di meno preciso ma che serve allo scopo.

Nell'altra modalità, la variabile in VAR viene di volta in volta resa un alias per il successivo elemento della LISTA, quindi operazioni in BLOCCO su di essa modificano l'elemento corrispondente nella LISTA stessa. Ebbene sì, se utilizzate un array nella LISTA potete modificarne gli elementi (ossia, il valore di tali elementi, ma non quanti sono: il numero di elementi della lista rimarrà invariato). Se la variabile non viene specificata... come al solito a lavorare è una versione localizzata di $_. Proseguendo con un esempio:

 foreach my $indice ( 0 .. 2 ) {
    print("\$indice vale $indice\n");
 }
 print("basta!\n");
 
 __END__
 
 $indice vale 0
 $indice vale 1
 $indice vale 2
 basta!

Gioie e dolori dell'aliasing

L'utilizzo dell'aliasing nasce da due esigenze fondamentali, una legata all'efficienza (evita di fare una copia del valore), l'altra legata invece alla possibilità di fare proprio questo tipo di operazioni in situ.

for/foreach, dunque, si possono utilizzare per modificare i valori di un array "sul posto":

 my @array = (1, 2, 3);
 print("prima: ( @array )\n");
 foreach my $alias (@array) {
    $alias *= 2; # Opera una trasformazione su $alias e, dunque, su @array
 }
 print("dopo : ( @array )\n");
 
 __END__
 
 prima: ( 1 2 3 )
 dopo : ( 2 4 6 )

Bello, eh? Come ogni cosa potente, fate bene attenzione a come la utilizzate, perché potrebbe riverlarsi un'arma a doppio taglio e rovinarvi gli array (oltre alla giornata, ovviamente).

C'è però un altro modo con cui potete soffrire i dolori dell'aliasing, ed è presumendo che tali effetti siano visibili anche al di fuori del ciclo. Cosa pensate che stampi il brano di codice che segue?

 my @array = (1, 2, 3);
 print("prima: ( @array )\n");
 my $alias = 7; # Nota: dichiarato prima!!!
 foreach $alias (@array) {
    $alias *= 2; # Opera una trasformazione su $alias e, dunque, su @array
 }
 print("dopo : ( @array )\n");
 print("alias: $alias\n");

Ma semplice, direte! Stampa esattamente quello che stampava l'altro script di prima, solo che aggiunge una riga in cui stampa:

 alias: 6

alla fine! Sbagliato:

 prima: ( 1 2 3 )
 dopo : ( 2 4 6 )
 alias: 7

Che succede? Niente di particolare: è solo che $alias, al di fuori del ciclo, torna ad essere il buon $alias della porta accanto, privo della sua doppia identità, riacquistando quindi il valore che aveva prima di diventare un agente segreto di sua maestà il ciclo foreach: il valore 7, in poche parole (anzi, in poche cifre). Questo vi insegni dunque ad evitare di dichiarare la variabile di alias al di fuori del ciclo: potreste avere brutte sorprese. Preferite invece sempre la dichiarazione direttamente nel ciclo, come in:

 foreach my $alias (...

Programmatore avvisato è mezzo debuggato...

Vita reale: iterare su un array

Normalmente si desidera iterare sui valori di un array, per cui è sufficiente utilizzare il ciclo che segue:

 my @array = qw( uno due tre quattro cinque );
 foreach (@array) {
    print("numero: $_\n");
 }
 print("numeri finiti\n");
 
 __END__
 
 numero: uno
 numero: due
 numero: tre
 numero: quattro
 numero: cinque
 numeri finiti

A volte, invece, è necessario iterare su un indice che punta nell'array; in questo caso si può fare così:

 my @array = qw( uno due tre quattro cinque );
 foreach (0 .. $#array) {  # $#array contiene l'ultimo indice utile in @array
    print("numero: $array[$_]\n");
 }
 print("numeri finiti\n");
 
 __END__
 
 numero: uno
 numero: due
 numero: tre
 numero: quattro
 numero: cinque
 numeri finiti

Vita reale: iterare su una hash

Ricordate quando prima abbiamo costruito un ciclo di visita di una hash utilizzando while ed each? Eravamo rimasti con un consiglio: cercate di evitare quell'approccio, a meno che l'ambiente in cui lo utilizzate sia veramente controllato.

L'approccio più robusto a mio modesto avviso è il seguente:

 my %hash = (a => 'b', c => 'd', e => 'effe');
 foreach (keys %hash) {
    print("chiave: $_; valore: $hash{$_}\n");
 }
 print("fine hash\n");
 
 __END__
 
 chiave: e; valore: effe
 chiave: c; valore: d
 chiave: a; valore: b
 fine hash

La funzione keys restituisce la lista delle chiavi dell'hash, e questa operazione viene fatta prima di iniziare il ciclo foreach vero e proprio. Come conseguenza, non c'è nessuno stato nascosto da qualche parte che possa essere rovinato per sbaglio. Ovviamente tutto si paga: se l'hash è particolarmente ampia, questa operazione di estrazione della lista delle chiavi è onerosa dal punto di vista del tempo e della memoria richiesti. Non esistono pranzi gratis.

Questo approccio ha però un altro vantaggio: vi dà la possibilità di accedere l'hash con un ordine sulle chiavi. Basta utilizzare sort:

 my %hash = (a => 'b', c => 'd', e => 'effe');
 foreach (sort keys %hash) { # Ora le chiavi sono ordinate
    print("chiave: $_; valore: $hash{$_}\n");
 }
 print("fine hash\n");
 
 __END__
 
 chiave: a; valore: b
 chiave: c; valore: d
 chiave: e; valore: effe
 fine hash

Controllori di ciclo

Quanti di voi conoscono C? Uhmmm, vedo poche mani alzate e mi chiedo se sia un bene o un male. Beh, quelli che lo conoscono saranno lieti di sapere che esistono modificatori di ciclo analoghi a break e continue. Per gli altri, è sufficiente andare avanti.

I cicli visti consentono in generale di essere alterati in maniera immediata dall'interno del BLOCCO che viene eseguito; questo grazie a tre parole chiave:

  • last indica che occorre uscire immediatamente dal BLOCCO e terminare il ciclo (corrisponde al break del C);
  • next indica che occorre uscire immediatamente dal BLOCCO e verificare le condizioni per accedere, eventualmente, all'iterazione successiva (corrisponde al continue del C);
  • redo indica che occorre re-iniziare l'esecuzione del BLOCCO, senza passare per le espressioni di controllo del ciclo (è specifico di Perl).

Vediamo alcuni esempi per fissare le idee.

Nel primo, creaiamo un semplice filtro, dove vengono eliminate tutte le righe di commento puro dallo standard input. Utilizziamo un ciclo while avvalendoci del caso particolare (lettura di un filehandle) descritto in precedenza. Tutte le righe che cominciano con zero o più spazi, seguite da un cancelletto, sono righe di commento pure e pertanto innescano la chiamata a next, saltando il comando print successivo.

 while (<>) {
    next if /^\s*#/; # Vai alla prossima iterazione se e` un commento
    print;           # altrimenti, stampa la riga
 }

Nel secondo esempio, invece, vediamo le potenzialità di last. Supponiamo di scandire un testo in ingresso, e di volerci fermare quando compare la parola STOP nella riga:

 while (<>) {
    last if /STOP/;
    print;
 }

L'uso di redo è piuttosto raro, ma citando dal manuale "è usualmente utilizzato da programmi che vogliono mentire a se stessi su quel che è stato appena immesso". Azzardiamo un esempio, stampando le righe di un file "immerso" con __DATA__ e raddoppiando tutte le righe che contengono la parola RADDOPPIAMI, immettendo il numero della riga letta:

 #!/usr/bin/perl
 use strict;
 use warnings;
 while (<DATA>) {     # Itera sulle righe dopo __DATA__
    print("$. $_");   # Stampa numero di riga letta e la riga stessa
 
    # Ripeti il blocco se la sostituzione va a buon fine. Questo ha anche
    # il compito di eliminare la stringa RADDOPPIAMI, in modo che redo
    # non inizi un ciclo infinito!
    redo if s/RADDOPPIAMI\s+//;
 }
 __DATA__
 Ciao a tutti
 RADDOPPIAMI caro!
 Ariciao
 a
 tutti

stampa

 1 Ciao a tutti
 2 RADDOPPIAMI caro!
 2 caro!
 3 Ariciao
 4 a
 5 tutti

Come potete vedere la riga "2" è ripetuta, ma lavora su un input differente grazie alla modifica fatta con l'operatore di sostituzione "s". Non granché utile come script, eh?

Ok, ad onor del vero quando ho iniziato a scrivere questo articolo l'utilizzo di redo era abbastanza raro. Ora che è uscito il libro "Perl Best Practices" di Damian Conway, però, ci si può aspettare che diventi di moda. Conway suggerisce di utilizzare l'accoppiata for e redo laddove si debba eseguire un ciclo su un numero grosso modo fissato di iterazioni, con qualche sporadica deviazione (ad esempio legata a qualche input anomalo dall'utente). Personalmente non sono molto convinto, ma di nuovo a voi la scelta!

Una delle peculiarità dei controllori di ciclo è che finalmente ci svelano il mistero di quelle benedette ETICHETTE. Ciascuno di essi, infatti, può essere riferito ad una etichetta particolare, il che permette di fare cose belle e pericolose:

 ESTERNO:
 while (<>) {
    my @campi = split /:/; # Dividi l'ingresso in campi separati da :
    my $conteggio = 0;
    INTERNO: foreach my $v (@campi) {
       $conteggio += $v;
       next ESTERNO if $conteggio > 10;
    }
    print("conteggio: $conteggio\n");
 }

Non dimenticate che le etichette sono sempre terminate da un carattere ":"! Nel caso di questo esempio, entrambi i cicli sono stati etichettati, anche se in realtà è stata utilizzata solo l'etichetta ESTERNO. Come si può vedere, il ciclo ESTERNO legge riga per riga dall'input, lo divide in campi in base al carattere separatore ":" ed inizializza una variabile di conteggio.

Il ciclo interno itera sull'array risultante dalla split e somma i vari elementi fra di loro. Se però tale somma risulta maggiore di 10, si passa direttamente alla riga di input successiva, interrompendo il foreach e saltando la print. Niente che non si potesse fare altrimenti, con un opportuno utilizzo di variabili di stato; in questo caso, però, la soluzione risulta particolarmente concisa, leggibile e meno soggetta ad errori.

Riassumendo, se l'uso di questo tipo di scorciatoie aumenti o meno leggibilità e manutenibilità va valutato caso per caso - il consiglio è comunque di non abusarne!

Un ciclo che non sembra tale

Un aspetto molto interessante dei controllori di ciclo next, last e redo consiste nel fatto che agiscono anche a livello di blocco nudo. Per questo motivo, in teoria potremmo risparmiarci completamente le parole chiave per il ciclo!

 my $indice = 0;
 {  # Attenzione: si apre un blocco
    print("\$indice vale $indice\n");
    redo if ++$indice < 3;
 }  # Chiusura del blocco
 print("finito!\n");
 
 __END__
 
 $indice vale 0
 $indice vale 1
 $indice vale 2
 finito!

Si può intuire come redo giochi un ruolo cruciale nel trasformare il blocco in un ciclo, mentre next è pressoché privo di senso, dal momento che ha l'effetto di terminare il blocco all'istante come last (risultando però meno leggibile).

Una curiosità: quanto detto non funziona in generale con i blocchi associati ad if e unless. Se volete utilizzare next e compagni in queste condizioni, basterà inserire un blocco interno raddoppiando le parentesi:

 foreach my $value ( 1 .. 45 ) {
    if ($value % 2) {{ # Doppie parentesi aperte!
       last if $value % 3; # esci subito dal blocco
       next if $value % 5; # anche qui, ma si legge peggio
       print("abbiamo un candidato: $value\n");
    }} # Doppie ne ho aperte, doppie ne chiudo
 }

 __END__

 abbiamo un candidato: 15
 abbiamo un candidato: 45

Si noti come last e next lavorano all'interno del blocco immerso dentro l'if, e non sul foreach.

continue pure

Tutti i tipi di cicli visti fino ad ora, con l'eccezione del for in stile C, possono assumere anche la forma estesa mediante la parola chiave continue:

 ETICHETTA while (ESPRESSIONE) BLOCCO continue BLOCCO2
 ETICHETTA until (ESPRESSIONE) BLOCCO continue BLOCCO2
 ETICHETTA foreach VAR (LISTA) BLOCCO continue BLOCCO2
 ETICHETTA BLOCCO continue BLOCCO

Il BLOCCO2 viene eseguito indistintamente a valle dell'uscita (eventualmente prematura) da BLOCCO, prima della valutazione dell'espressione booleana o di incremento (se presenti). Tale opzione è molto comoda se si stanno utilizzando next e last, poiché questi controllori di flusso interrompono BLOCCO all'istante.

Confrontiamo:

 my $contatore = 0;
 while (<DATA>) {
    next if /SALTAMI/;
    print "$contatore: $_";
    ++$contatore;
 }
 __DATA__
 Ciao
 a
 SALTAMI
 tutti
 quanti

con

 my $contatore = 0;
 while (<DATA>) {
    next if /SALTAMI/;
    print "$contatore: $_";
 }
 continue {
    ++$contatore;
 }
 __DATA__
 Ciao
 a
 SALTAMI
 tutti
 quanti

Nel primo caso, $contatore conta solamente le righe che non contengono la parola SALTAMI: se l'espressione regolare /SALTAMI/ è verificata, infatti, next interrompe l'iterazione corrente e passa alla successiva, saltando sia la stampa che l'incremento. Il risultato finale è:

 0: Ciao
 1: a
 2: tutti
 3: quanti

Nel secondo caso, invece, $contatore conta tutte le righe in ingresso, non solo quelle stampate. Il blocco successivo a continue, infatti, viene eseguito sempre, a prescindere dal fatto che next intervenga o meno. Abbiamo dunque:

 0: Ciao
 1: a
 3: tutti
 4: quanti

Il contatore è stato evidentemente incrementato (si passa da 1 a 3).

Si faccia attenzione al fatto che redo fa ripartire il blocco corrente a prescindere dal ciclo in cui si trova. Questo vuol anche dire che continue non viene eseguita quando si chiama redo.

Un altro po' di linguistica: forme suffisse

Abbiamo già discusso in precedenza sulle radici linguistiche di Perl. Tali radici hanno attecchito per dar vita ad altri modi per impostare dei cicli: le forme suffisse. Vediamo di che si tratta.

Supponiamo che vogliate stampare i valori da 1 a 10, uno per riga. Nonostante esistano molti modi per farlo, il più semplice è probabilmente quello che segue:

 foreach (1 .. 10) {
    print "$_\n";
 }

In linguaggio naturale, suonerebbe qualcosa tipo "per ogni valore da 1 a 10, stampa il valore e vai a capo". Si noti che qui si dà l'accento sull'intervallo dei valori (per ogni valore da 1 a 10), relegando l'operazione da compiere in un piano leggermente più retrostante.

Se volessimo porre l'accento sull'operazione da compiere, invece, probabilmente diremmo "stampa e vai a capo, per ogni valore da 1 a 10". Bene, Perl ci consente di farlo:

 print "$_\n" foreach 1 .. 10;

Il ciclo è divenuto un modificatore dell'istruzione di stampa, che rimane detentrice di un ruolo centrale nella frase. Questo tipo di approccio l'abbiamo già visto in precedenza con if:

 my $contatore = 0;
 while (<DATA>) {
    next if /SALTAMI/;       # if come suffisso
    print "$contatore: $_";
    ++$contatore;
 }
 __DATA__
 Ciao
 a
 SALTAMI
 tutti
 quanti

Le possibili forme suffisse per le iterazioni sono tre:

 ISTRUZIONE while ESPRESSIONE;  # Il sempreverde
 ISTRUZIONE until ESPRESSIONE;  # Il solito bastian contrario
 ISTRUZIONE foreach LISTA;      # Anche for va bene, ovviamente

Si noti che in questo caso non è necessario mettere ESPRESSIONE o LISTA fra parentesi. Va inoltre ricordata una cosa molto importante: la forma suffissa è una forma linguistica, che consente di porre maggiore accento su quello che si fa piuttosto che su quello che si itera. Il fatto che la sezione di controllo del ciclo venga dopo, comunque, non vuol dire che il controllo stesso venga effettuato dopo. In poche parole, anche se compaiono dopo ISTRUZIONE, sia i controlli di while/until che l'avanzamento lungo LISTA di foreach avvengono sempre prima di essa.

Prima mena, poi discuti

Abbiamo visto che i controlli di ciclo suffissi sono, alla fine della fiera, solamente degli accorgimenti di natura linguistica. Che dobbiamo fare per avere un ciclo che esegua i controlli non all'inizio, ma alla fine di ciascun gruppo di istruzioni? Dobbiamo utilizzare una delle tante facce di do:

 # Prima mena, poi discuti :)
 my $contatore = 3;
 do {
    print "\$contatore vale $contatore\n";
 } while $contatore < 3;
 print "ok, basta\n";
 
 __END__
 
 $contatore vale 3
 ok, basta

Analogo discorso vale, ovviamente, per until. Tanto per levarci la curiosità, possiamo usare next, last e redo? In realtà... no, perché quello creato da do non è un blocco nel senso tradizionale del termine. Nel manuale perlsyn <http://perldoc.perl.org/perlsyn.html> trovate cosa fare per poter utilizzare i vostri (possibilmente infidi) nuovi amici.

sort, grep e map: gli specialisti del ciclo

Fino ad ora abbiamo analizzato i costrutti di iterazione basilari di Perl; nonostante sia virtualmente possibile utilizzare sempre un ciclo while scritto opportunamente, la varietà lessicale di Perl ci consente di utilizzare il costrutto che, di volta in volta, meglio si adatta al concetto che vogliamo esprimere (ad esempio utilizzando una forma suffissa).

Come abbiamo detto in apertura, però, Perl cerca anche di rendere particolarmente semplici le operazioni più di routine. Tale semplificazione viene attuata in modo sottile: da una parte viene specializzato un processo di iterazione, dall'altra viene generalizzato dando all'utente la possibilità di intervenire sul cuore della funzione.

Tutte e tre le funzioni lavorano come filtri, ossia producono liste a partire da altre liste. Questo però non deve trarre in inganno o far abbassare la guardia: saremo in grado di fare danni anche sui dati di partenza, state tranquilli!

Metti a posto, Battista!

La prima funzione di specializzazione/generalizzazione che vediamo serve a svolgere un'operazione che spesso risulta necessaria: l'ordinamento di una lista di elementi. Tale operazione presuppone un certo numero di cicli, durante i quali i vari elementi della lista sono confrontati fra di loro per stabilire chi debba avere la precedenza.

Ci sono tre modi per utilizzarla:

 sort LISTA
 sort BLOCCO LISTA
 sort FUNZIONE LISTA

Come al solito, molte cose possono essere date per scontate, per cui la prima modalità consente di scrivere:

 #!/usr/bin/perl
 use strict;
 use warnings;
 
 my @disordinato = qw( eliseo ambrogio levino egis );
 my @ordinato = sort @disordinato;
 print "[ ", (join(", ", @ordinato)), " ]\n";
 
 __END__
 
 [ ambrogio, egis, eliseo, levino ]

Come sempre, però, il default potrebbe trarre in inganno, come dimostra il seguente esempio:

 #!/usr/bin/perl
 use strict;
 use warnings;
  
 my @disordinato = qw( 5 41 31 2 11 3 19 13 7 17 29 37 23);
 my @ordinato = sort @disordinato;
 print "[ ", (join(", ", @ordinato)), " ]\n";
 
 __END__
 
 [ 11, 13, 17, 19, 2, 23, 29, 3, 31, 37, 41, 5, 7 ]

Viste le radici linguistiche del Perl, non deve stupire che l'ordine assunto sia quello lessicografico, che è un modo snob di dire alfabetico. Per avere l'ordine numerico dobbiamo indicarlo esplicitamente utilizzando la seconda opzione:

 #!/usr/bin/perl
 use strict;
 use warnings;
  
 my @disordinato = qw( 5 41 31 2 11 3 19 13 7 17 29 37 23);
 my @ordinato = sort {
    if ($a > $b)    { 1 }
    elsif ($a < $b) { -1 }
    else            { 0 }
 } @disordinato;
 print "[ ", (join(", ", @ordinato)), " ]\n";
 
 __END__
 
 [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 ]

Il BLOCCO ha queste caratteristiche:

  • al suo interno "vede" le variabili $a e $b come alias di due particolari elementi della lista che sono oggetto di confronto;
  • deve restituire un valore coerente con l'ordinamento atteso fra i due alias:
    • 1 se $a è maggiore (ossia, successivo) a $b;
    • 0 se sono uguali (ossia possono potenzialmente essere scambiati nell'ordinamento senza alterarne la bontà);
    • -1 se $a è minore (ossia, precedente) a $b.

Tutto questo traffico per un semplice ordinamento?!? In realtà, visto che è qualcosa che risulta essere spesso utile, non deve sorprendere che nel caso numerico esista una scorciatoia chiamata operatore navicella spaziale:

 #!/usr/bin/perl
 use strict;
 use warnings;
  
 my @disordinato = qw( 5 41 31 2 11 3 19 13 7 17 29 37 23);
 my @ordinato = sort { $a <=> $b } @disordinato;
 print "[ ", (join(", ", @ordinato)), " ]\n";
 
 __END__
 
 [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 ]

Spaziale veramente, no? Come? Ah, navicella spaziale, certo. Beh, se avete visto Guerre Stellari, ricorderete certamente le navicelle da battaglia dell'Impero - esatto, quelle che avevano forma <=>.

L'ultima forma - quella con la FUNZIONE - non aggiunge molta suspance rispetto a quella con BLOCCO, ma sicuramente vi consente di aggiungere leggibilità è vi dà anche un pizzico di flessibilità in più. Tale approccio risulta infatti particolarmente adatto a gestire le seguenti situazioni:

  • se il BLOCCO risulta particolarmente lungo può essere difficile da leggere, poiché sort e LISTA sono distanti fra loro;
  • se volete decidere dinamicamente quale metodo di ordinamento (ad esempio, crescente o decrescente, oppure numerico o alfabetico) utilizzare.

Vediamo un esempio:

 #!/usr/bin/perl
 use strict;
 use warnings;
  
 # L'array @funzioni contiene i riferimenti a quattro possibili funzioni
 # di ordinamento; queste sono definite in fondo
 my @funzioni = qw( alpha_c alpha_d num_c num_d );
 #@funzioni = ( \&alpha_c, \&alpha_d, \&num_c, \&num_d );
  
 # Per rendere leggibile la stampa diretta degli array nelle virgolette
 $" = ", ";
  
 my @disordinato = qw( 5 41 31 2 11 3 19 13 7 17 29 37 23 );
 print("L'array disordinato e` [@disordinato]\n");
  
 menu();
 while (<>) {
    # Controlli iniziali
    next unless /^([1-5])/; # Salta i comandi non validi
    last if $1 == 5;        # Esci se richiesto
     
    # Ordinamento
    my $funzione = $funzioni[$1 - 1];           # Trova funzione
    my @ordinato = sort $funzione @disordinato; # ed usala
  
    # Stampe
    print("===> [@ordinato]\n");
    menu();
 }
 print("E` stato un piacere\n");
 
 sub menu {
     print "
 1. Alfabetico, crescente
 2. Alfabetico, decrescente
 3. Numerico, crescente
 4. Numerico, decrescente
 5. Esci
 ";
 }
  
 sub alpha_c { $a cmp $b } # Alfabetico, crescente
 sub alpha_d { $b cmp $a } # Alfabetico, decrescente
 sub num_c   { $a <=> $b } # Numerico, crescente
 sub num_d   { $b <=> $a } # Numerico, decrescente
 
 __END__
 
 L'array disordinato e` [5, 41, 31, 2, 11, 3, 19, 13, 7, 17, 29, 37, 23]
  
 1. Alfabetico, crescente
 2. Alfabetico, decrescente
 3. Numerico, crescente
 4. Numerico, decrescente
 5. Esci
 2
 ===> [7, 5, 41, 37, 31, 3, 29, 23, 2, 19, 17, 13, 11]
  
 1. Alfabetico, crescente
 2. Alfabetico, decrescente
 3. Numerico, crescente
 4. Numerico, decrescente
 5. Esci
 4
 ===> [41, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2]
  
 1. Alfabetico, crescente
 2. Alfabetico, decrescente
 3. Numerico, crescente
 4. Numerico, decrescente
 5. Esci
 5
 E` stato un piacere

Si faccia attenzione ad un fatto: il nome della funzione può essere contenuto in una variabile scalare, ma solo nativa e non, ad esempio, parte di un array. Per capirci, abbiamo dovuto scrivere:

 # Ordinamento
 my $funzione = $funzioni[$1 - 1];           # Trova funzione
 my @ordinato = sort $funzione @disordinato; # ed usala

non per particolare amore di leggibilità (anche se è stato camuffato come tale), ma semplicemente perché:

 # Ordinamento
 my @ordinato = sort $funzioni[$1 - 1] @disordinato; # ed usala

non avrebbe funzionato! La vita è dura e l'interprete Perl, che ha a cuore la nostra educazione, a volte è lì a ricordarcelo.

Si possono utilizzare sia i nomi delle funzioni che riferimenti ad esse, come avete modo di verificare facilmente eliminando il commento dalla riga:

 #@funzioni = ( \&alpha_c, \&alpha_d, \&num_c, \&num_d );

che imposta l'array, per l'appunto, con i riferimenti alle funzioni al posto dei nomi.

Emanuele Zeppieri mi fa inoltre notare che l'operatore di estrazione di reference - ok, il carattere backslash - è distributivo rispetto ad una lista, e che possiamo pertanto scrivere:

 @funzioni = \( &alpha_c, &alpha_d, &num_c, &num_d );

Ma guarda tu che si inventano!

Una piccola avvertenza finale: per semplificare la stampa dell'array abbiamo modificato il valore della variabile $" impostandola a ", ", ma in generale è meglio evitare di farlo. O, se proprio dovete (o volete), è meglio localizzare la modifica il più possibile utilizzando local all'interno di un blocco, in modo da limitare la portata della modifica ed evitare di generare errori in altre parti del codice. In questo caso, viste le dimensioni dello script, la cosa non crea comunque alcun problema.

Arma', questo me lo cacci via!

La seconda funzione di ciclo specializzato/generalizzato è grep, che consente di filtrare una lista trattenendo i soli elementi (qui sta la specializzazione) che verificano una determinata proprietà (qui la generalizzazione). Il principio ispiratore è, ovviamente, il programma Unix grep.

Ci sono due modi di utilizzarla:

 grep BLOCCO LISTA
 grep ESPRESSIONE, LISTA

La differenza fra i due approcci è sintattica e non ce ne occupiamo; vediamo invece alcuni esempi:

 my @lista                    = qw( 12 ciao 6 7 frodo72 idromele isterica );
 my @inizia_con_i             = grep { /^i/ } @lista;
 my @contiene_cifre           = grep { /\d/ } @lista;
 my @solo_interi_minori_di_10 = grep { /^\d+$/ && $_ < 10 } @lista;
 my @solo_parole              = grep { /^[[:alpha:]]+$/ } @lista;

Ciascun elemento della lista viene analizzato mediante il BLOCCO. In particolare, $_ nel blocco diventa un alias per l'elemento della lista e può essere utilizzato per effettuare il test desiderato; se il test dà esito positivo l'elemento passa in uscita, altrimenti viene bloccato.

Tempi moderni...

Avete mai visto il film di Charlie Chaplin "Tempi Moderni"? Se qualcuno ha detto no, è in buona compagnia: nemmeno io l'ho visto.

In ogni caso, una scena del film che spesso viene "citata" in televisione mostra Charlot in fabbrica, davanti alla catena di montaggio. I pezzi passano, lui applica una trasformazione (un giro con la chiave inglese) ed i pezzi proseguono sul nastro trasportatore.

map è un po' la catena di montaggio delle trasformazioni sulle liste. Ci mettete davanti un blocco di codice Charlot, infilate un po' di pezzi nella LISTA di ingresso, e map mette Charlot al lavoro su ciascun pezzo per rimandarveli sulla LISTA di uscita.. Solo che, per fortuna, il blocco non soffre di alienazione.

I possibili modi di utilizzarlo sono analoghi a quelli visti per grep:

 map BLOCCO LISTA
 map ESPRESSIONE, LISTA

Anche qui, non ci addentriamo in una dissezione delle differenze fra i due approcci, il manuale è sufficientemente chiaro! Alcuni esempi sono però d'obbligo per capire cosa fa veramente.

Nel primo, supponiamo di avere un array di valori numerici e di volerne calcolare i quadrati in un nuovo array. Detto fatto:

 my @input = 1 .. 10;  # Per semplicita`, i numeri interi da 1 a 10
 my @output = map { $_ * $_ } @input;

Esatto, è proprio così semplice. Venghino siori, non c'è trucco e non c'è inganno. È immediato che questa tecnica può essere utilizzata per generalizzare una qualunque funzione scalare per lavorare su un insieme di dati. La funzione matematica sin(), ad esempio, è fatta per lavorare su un solo valore per volta, ma se volete graficarla avete bisogno di calcolarla su un intervallo. Aspetta un attimo: serve pure a calcolare l'intervallo!

 # Calcoliamo la funzione sin(x) fra 0 e 2*PI, dividendo questo
 # intervallo in 100 sotto-intervalli
 my $intervalli = 100;                           # Numero di sotto-intervalli
 my $pi = 3.14159265358979;                      # Pi greco
 my $amp = $pi * 2 / $intervalli;                # Ampiezza sotto-intervallo
 my @x = map { $amp * $_ } 0 .. $intervalli;     # I punti sono uno in piu`
 my @y = map { sin($_) } @x;                     # I valori...

map non serve solo ai malati di matematica! Potreste avere un array di parole, e volerle trasformare in lettere maiuscole:

 #!/usr/bin/perl
 use strict;
 use warnings;
 my @parole = qw( ciao a tutti come al solito );
 my @maiuscole = map { uc($_) } @parole;
 $" = ", ";
 print("parole: [@parole]\n");
 print("maiuscole: [@maiuscole]\n");
 
 __END__
 
 parole: [ciao, a, tutti, come, al, solito]
 maiuscole: [CIAO, A, TUTTI, COME, AL, SOLITO]

Come vedete, la catena di montaggio: le parole entrano sotto forma di $_, viene applicata la funzione ed il valore restituito (ossia quello dell'ultima espressione immediatamente prima dell'uscita dal blocco) è inviato nella lista di uscita, che in questo caso va a popolare @maiuscole.

map, però, può fare di più. In particolare, nessuno impone al blocco di restituire un valore scalare; restituendo una lista è possibile ampliare o accorciare la lista in ingresso. Ad esempio, un rimpiazzo un po' ingenuo per grep potrebbe essere il seguente:

 my @valori = 1 .. 20;
 my @maggiori_di_10 = map {
    if ($_ > 10) { $_ }  # Se verifica la proprieta`, fa' passare
    else         { () }  # Altrimenti, blocca restituendo "niente"
 } @valori;

Capite però bene quanto grep sia più pulito e leggibile! Particolarmente interessante è però la possibilità di aumentare il numero di elementi, che può essere utilizzato molto proficuamente per generare una hash a partire da una lista di valori:

 #!/usr/bin/perl
 use strict;
 use warnings;
 
 # Generiamo una hash in cui le chiavi sono le parole di una lista, ed
 # i valori sono le lunghezze delle parole stesse
 my %lunghezza_di = map { $_ => length($_) } qw( ciao a tutti basta! );
 print("La lunghezza di 'ciao' risulta essere ", $lunghezza_di{'ciao'}, "\n");
 
 __END__
 
 La lunghezza di 'ciao' risulta essere 4

Come darsi la zappa sui piedi

Avrete sicuramente notato che in tutte e tre le funzioni vengono creati degli alias ai dati nella lista originale. Che vuol dire? Ah, ma allora non leggete: abbiamo già parlato degli alias nella parte relativa a for/foreach!

Ok, se non volete andare a rivedervela, ricordiamo semplicemente che le variabili alias (ossia $a e $b in sort, e $_ in grep e map) non sono delle copie, ma agiscono direttamente sugli elementi della lista in ingresso. Con l'ovvia conseguenza che vi consentono di modificare l'elemento della lista in ingresso.

Tale possibilità può lasciare stupiti nelle nostre tre funzioni: in fondo, queste sono pensate per agire come filtri, per cui dovrebbero considerare gli ingressi in sola lettura. Vanno però fatte un paio di considerazioni:

  1. nessuno vi obbliga a modificare i valori della lista in ingresso anche se potete farlo. Perl non è un linguaggio da scimmie, al contrario mette a disposizione del programmatore moltissima "potenza di fuoco", lasciandogli il compito di mettere la sicura quando vuole avventurarsi in sentieri perigliosi;
  2. utilizzare delle copie porterebbe ad un appesantimento dell'eseguibile a scapito dell'efficienza.

Andando a stringere, utilizzate questa caratteristica di aliasing solo se siete molto consapevoli di ciò che state per fare!

Vita reale: ordinare le chiavi di una hash secondo i valori

Tutti, dico tutti sapete benissimo che la funzione keys restituisce le chiavi di una hash. E tutti sapete anche che la lista restituita ha un ordinamento che si perde nelle radici di Perl - come a dire che non ne ha uno.

Come fare ad ordinare le chiavi di una hash secondo i valori puntati da ciascuna chiave? Presto detto, per sort non potrebbe essere più semplice:

 my %hash;
 # ... %hash viene popolata con dati vitali! ...
 
 # Usiamo <=> supponendo che siano valori numerici. Se i valori sono
 # parole, e` meglio usare cmp :)
 my @chiavi_ordinate = sort { $hash{$a} <=> $hash{$b} } keys %hash;

Molto semplicemente il confronto viene effettuato fra i valori associati invece che direttamente fra $a e $b. Ci avevate già pensato, vero?

Vita reale: la trasformazione di Schwartz

Randal L. Schwartz è un punto di riferimento nella comunità Perl, per i contributi dati (Learning Perl è suo, ad esempio). Una speciale tecnica di ordinamento ha avuto talmente tanto successo da prendere il suo nome, vediamo come funziona. Attenzione: alla prima lettura vi sembrerà solo rumore!

L'ipotesi di base è che vogliamo effettuare l'ordinamento di una lista di elementi basandoci su una funzione di costo calcolabile per ciascuno di essi. Il problema, però, è che la funzione è molto lenta, ed una cosa tipo:

 my @ordinati = sort { costo($a) <=> costo($b) } @disordinati;

costringe sort a calcolarla due volte per ciascun confronto deve fare. Se il primo elemento della lista di ingresso viene confrontato con tutti gli altri, dunque, la funzione costo viene calcolata ogni volta! Inaccettabile.

Schwartz, riutilizzando un'idea già diffusa in programmazione, ha pensato bene di pre-calcolare il valore della funzione di costo una volta per tutte, e di utilizzare questo valore calcolato a priori per l'ordinamento. Concettualmente si può fare così:

 # Calcolo i costi in una hash
 my %costo_di = map { $_ => costo($_) } @disordinati;
 
 # Ora ordino @disordinati utilizzando la hash
 my @ordinati = sort { $costo_di{$a} <=> $costo_di{$b} } @disordinati;

Schwartz non era però soddisfatto da questa soluzione, che lo costringeva a fare l'ordinamento in due istruzioni anziché in una. D'altra parte, con una hash non avrebbe potuto fare di meglio. È allora giunto alla seguente formulazione:

 my @ordinati = map  { $_->[0] }
                sort { $a->[1] <=> $b->[1] }
                map  { [ $_, costo($_) ] }
                @disordinati;

Eh? Piano, piano. Ricordiamoci della catena di montaggio: gli ingressi sono a destra, le uscite a sinistra. Bene, i nostri dati @disordinati entrano dentro la map più in basso, che in uscita dà una curiosa trasformazione sul dato: per ciascun elemento, costruisce un array anonimo in cui il primo valore è l'elemento stesso, altrimenti ce lo perderemmo, mentre il secondo è il valore della funzione costo().

A questo punto interviene sort che, ricordiamolo, riceve una lista di riferimenti ad array - quelli generati dalla map descritta. Poiché gli array in questione contengono la funzione di costo all'indice 1, il blocco imposta un confronto fra i corrispondenti campi dei due riferimenti $a e $b. In uscita, sort restituisce gli stessi elementi ricevuti (o, meglio, delle copie): riferimenti ad array di due elementi ciascuno, ordinati in base alla funzione costo.

Il problema è che a noi non servono questi array, a noi servono gli elementi che stavano in @disordinati. È precisamente per questo motivo che c'è la map più in alto: questa restituisce semplicemente il primo elemento di ciascuno di questi array - ossia il valore dell'elemento di interesse, che avevamo prudentemente inserito.

Questa è una delle trasformazioni di ordinamento più popolari in Perl - capito perché tutti dicono che un programma Perl assomiglia al rumore di una linea telefonica?

List::Util

L'approccio specialistico delle funzioni sort, grep e map ha fatto venire l'acquolina in bocca a Graham Barr, autore di List::Util, forse uno dei moduli più trascurati del CORE di Perl. Si, avete capito bene: List::Util ve lo ritrovate in una qualsiasi installazione tipica di Perl.

Come dicevo, a Barr era venuta fame: "Come faccio a trovare il valore massimo di una lista? Ed il minimo? Ed il primo che verifica una certa proprietà? Se voglio sommare tutti gli elementi? O mischiare un po' le carte?". Beh, ha risolto questi problemi comuni e li ha messi in List::Util.

Quasi tutte le funzioni hanno il seguente schema di chiamata:

 min     LISTA   # Trova il minimo (numerico)
 max     LISTA   # Trova il massimo (numerico)
 sum     LISTA   # Somma tutti i valori (numerici)
 
 minstr  LISTA   # Trova il minimo (alfabetico)
 maxstr  LISTA   # Trova il massimo (alfabetico)
 
 shuffle LISTA   # Da` una versione mischiata di LISTA

L'ultima funzione restituisce una lista, mentre tutte le altre restituiscono uno scalare. Ad esempio:

 use List::Util qw( minstr maxstr shuffle ); # Non dimentichiamolo!
 my @lista;
 
 # ... piu` tardi, quando la lista e` bella piena...
 
 # Troviamo la stringa 'minima', nell'ordinamento alfabetico
 my $minima = minstr @lista;
 
 # Ora, la massima
 my $massima = maxstr @lista;
 
 # Diamo una mischiatina
 my @mischiata = shuffle @lista;

Insomma, avete capito.

Ci sono poi altre due funzioni, first e reduce, che funzionano molto più similmente a map:

 first BLOCCO LISTA
 reduce BLOCCO LISTA

La prima è piuttosto semplice: utilizza BLOCCO molto similmente a grep, ossia per effettuare un test sull'elemento $_; la prima volta che tale test va a buon fine l'iterazione si interrompe, restituendo l'elemento corrispondente.

 use List::Util qw( first );
 my @lista;
 
 # ... piu` tardi, quando la lista e` bella piena...
 
 # Troviamo il primo valore numerico intero
 my $primo_numero_intero = first { /^\d+$/ } @lista;

La funzione reduce, invece, va un po' più in là. È talmente generica che farete molto in fretta a scordarvi che esiste, ma se riuscite a dominarla avete la mia ammirazione! Ad ogni iterazione, BLOCCO ha a disposizione due variabili $a e $b, proprio come sort. Come vengono impostate? Qui sta il bello:

  1. alla prima iterazione vengono presi i primi due elementi della lista, il primo è $a ed il secondo potete immaginarvelo;
  2. nelle iterazioni successive $a è il valore restituito da BLOCCO al giro precedente, mentre $b è il successivo valore in LISTA.

Come casi particolari, se la lista è vuota viene restituito undef, mentre se ha un solo elemento vie

List::MoreUtils

Dopo aver fatto venire l'acquolina in bocca a tutti con List::Util, Graham Barr ha deciso bene di metterci a dieta limitandosi a fornire le otto funzioni viste in precedenza. Del resto è piuttosto chiaro nella documentazione, riferendosi alle varie proposte come:

[aggiunte] che sono state richieste, ma che sono stato riluttante ad aggiungere per il fatto che sono molto semplici da implementare in Perl

Ci sono però alcune considerazioni da fare a riguardo:

  • l'intento di List::Util è fornire una raccolta di funzioni che migliorano la leggibilità del codice pur essendo tutte implementabili molto semplicemente in Perl, per cui il suo argomento potrebbe essere parimenti applicato alle funzioni che ha deciso di includere;
  • un altro grande vantaggio fornito da List::Util risiede nel fatto che le funzioni sono implementate in due forme, ossia in Perl puro ed in C. Questo fa sì che, avendo a disposizione un compilatore C, l'utente possa accedere ad una implementazione più efficiente del dato algoritmo, garantendo comunque l'utente più svantaggiato mediante l'implementazione puramente in Perl.

Tutto sommato, è mia opinione che Barr non avesse semplicemente il tempo o la voglia di stare dietro a tutte le proposte! Il testimone è stato però felicemente raccolto da Tassilo von Parseval in List::MoreUtils, che dà appunto accoglienza ad una lunga serie di algoritmi sulle liste che non hanno trovato posto nei locali esclusivi di List::Util. Anche qui, inutile a dirsi, sono presenti le due implementazioni in Perl puro ed in C, in modo da accontentare tutti.

Il modulo contiene veramente tante funzioni, per cui il consiglio per chi volesse vederle tutte è di andare a guardare la documentazione relativa. Alcune funzioni, però, sembra proprio che siano state escluse ingiustamente da List::Util, per cui cercheremo di render loro giustizia.

any ed all: la generalizzazione di or ed and

Può capitare di dover verificare se una determinata lista contiene almeno un elemento con una determinata proprietà. Non sto parlando solo di programmazione, ma anche di vita reale; ad esempio, inserendo canzoni nel nostro lettore MP3, potremmo ad un certo punto chiederci se abbiamo messo qualcosa dei Led Zeppelin, senza di che non saremmo contenti (ognuno si accontenta come può!). In questo caso, quindi, dobbiamo sostanzialmente rispondere alla domanda: "di tutte le canzoni caricate, ne esiste qualcuna il cui artista è 'Led Zeppelin'?". Qualcosa che in Perl suonerebbe più o meno così:

 my @canzoni = seleziona_brani('a caso');
 
 # C'e` qualcosa dei Led Zeppelin?
 if (any { autore($_) eq 'Led Zeppelin' } @canzoni) {
    print "tranquillo, ci sono i Led Zeppelin\n";
 }
 else {
    print "una grave mancanza, a cui rimediamo subito!\n";
    push @canzoni, seleziona_brani('Led Zeppelin');
 }

Beh, se utilizzate List::MoreUtils è proprio quello che dovete scrivere! La funzione any (letteralmente: qualcuno), infatti, incapsula proprio un controllo di esistenza di almeno un elemento della lista che soddisfa il predicato impostato, che in questo caso si risolve in un controllo sull'autore del brano. La ricerca è ovviamente efficiente, nel senso che viene interrotta non appena uno degli elementi verifica la proprietà data; in questo senso, dunque, any può essere visto come una generalizzazione dell'operatore or, che "si ferma" se l'espressione a sinistra è vera:

 if (   autore($canzoni[0]) eq 'Led Zeppelin'
     or autore($canzoni[1]) eq 'Led Zeppelin'
     or autore($canzoni[2]) eq 'Led Zeppelin'
     or autore($canzoni[3]) eq 'Led Zeppelin') { ... }

In questo caso, se la prima canzone (quella di indice 0) soddisfa il requisito, le altre tre condizioni non vengono mai controllate, in quanto sarebbe un'inutile perdita di tempo (la or globale sarebbe vera comunque). I vantaggi nell'uso di any sono però evidenti:

  • è applicabile qualunque sia la lunghezza della lista;
  • evita ripetizione di codice e possibilità di errore;
  • esprime più chiaramente cosa si sta tentando di fare.

Avrete sicuramente riconosciuto che lo stile di chiamata è analogo a quello di map e compagni, in modo da ridurre lo stress per il programmatore e consentirgli di concentrarsi su quello che conta realmente.

Se any risponde alla domanda "ne esiste almeno uno?", dall'altro lato all (letteralmente: tutti) risponde ad una richiesta più stringente "lo fanno tutti?" - costituendo quindi una generalizzazione di and:

 # Diamo una controllata al genere delle canzoni
 if (all { genere($_) eq 'rock' } @canzoni) {
    print "ma ascolti solamente rock?!?\n";
 }

Delle due funzioni esistono anche le rispettive forme negate:

  • none (letteralmente: nessuno) restituisce un valore vero se nessun elemento della lista verifica una data proprietà, costituendo quindi la negazione di any;
  • notall (letteralmente: non tutti) restituisce un valore vero se non tutti gli elementi della lista verificano il predicato nel blocco, ed è dunque il contrario di all come suggerisce il nome.

Quanti sono?

  • Quante monete da 1, 2 o 5 centesimi ho nel borsellino?
  • Quanti libri di informatica ci sono nella libreria?
  • Quanta roba scaduta ho nel frigo?

In tutti questi casi non mi interessa sapere quali sono, solo quanti. Certo, sarebbe forse il caso di sapere anche quali nell'ultimo caso, ma non è il caso di stare a sottilizzare.

Per domande di questo tipo vengono in nostro aiuto true e la sua controparte false, che effettuano proprio questo tipo di conteggi basandosi, come al solito, su un blocco di codice che funge da predicato:

 my $n_spiccetti = true { $_ <= 0.05 } @borsellino;
 
 my $n_libri_informatica = true { argomento($_) eq 'informatica' } @libreria;
 
 my $n_scaduti = false { ancora_buono($_) } @frigo;

Nell'ultimo caso utilizziamo false, trasferendo quindi la negazione fuori dal blocco; si sarebbe potuto scrivere anche:

 my $n_scaduti = true { not ancora_buono($_) } @frigo;

ma forse (e dico forse) la leggibilità sarebbe stata inferiore. Dipende dai gusti.

minmax, o come fare due cose contemporaneamente

Dovete graficare una funzione ed avete due liste, una per asse:

 my @x = 1 .. 20;
 my @y = f(@x);

Vogliamo che il nostro grafico sia posizionato automaticamente dove si trovano i dati, ossia vogliamo scegliere gli intervalli per i due assi in modo da coincidere con i valori minimo e massimo contenuti nei due array. L'intervallo per l'asse delle x è semplice da determinare - basta prendere i due estremi - ma come facciamo per determinare l'intervallo sull'asse delle y? Abbiamo visto che List::Util contiene le due funzioni min e max, per cui potremmo dire:

 my ($min_x, $max_x) = @x[1, -1]; # Prendi il primo e l'ultimo valore da @x
 my $min_y = min @y;
 my $max_y = max @y;

Questo modo di calcolare minimo e massimo non è proprio... il massimo. La lista @y viene infatti scandita due volte, e ad ogni scansione abbiamo un numero di confronti pari al numero di elementi meno uno.

Esiste invece un algoritmo che consente di evitare un po' di confronti nel caso si voglia calcolare contemporaneamente sia il minimo che il massimo (v. ad esempio http://www.perlmonks.org/?node_id=507340); tale algoritmo è stato implementato in C nella funzione minmax, il che mette a disposizione un sistema efficiente ed espressivo per svolgere questa doppia ricerca:

 my ($min_x, $max_x) = @x[1, -1]; # Prendi il primo e l'ultimo valore da @x
 my ($min_y, $max_y) = minmax @y; # Prendi minimo e massimo da @y

Un po' di confronti a questo punto non guastano (esempio preso da http://www.perlmonks.org/?node_id=507414):

 #!/usr/bin/perl
 use strict;
 use warnings;
 use List::Util qw( min max reduce );
 use List::MoreUtils qw( minmax );
 use Benchmark qw( cmpthese );
 
 my @array = map { rand 100 } 1 .. 1_000_000;
 
 my $count = 0;
 cmpthese -10, {
    util_min_max => sub {
       my $min = min(@array);
       my $max = max(@array);
    },
    util_reduce => sub {
       my ( $min, $max ) = @{ (reduce {
          my $r= ref $a ? $a : [($a) x 2];
          if ($b < $r->[0]) {
             $r->[0]= $b;
          } elsif ($b > $r->[1]) {
             $r->[1]= $b;
          }
          $r
       } @array) };
    },
    mu_minmax => sub {
       my ( $min, $max ) = minmax(@array);
    },
 };
 
 __END__
 
                     Rate  util_reduce  util_min_max  mu_minmax
 util_reduce      0.846/s           --          -92%       -96%
 util_min_max      10.5/s        1146%            --       -45%
 mu_minmax         19.2/s        2169%           82%         --

Ok, a parte il fatto che magari dovrei scrivere qualcosa su Benchmark, concentriamoci sulla colonna Rate della tabella finale: come potete vedere, l'utilizzo di minmax consente di eseguire 19.2 ricerche al secondo su un milione di elementi, mentre l'utilizzo di min e max separate ce ne fa fare solo 10.5 al secondo. Come ulteriore candidato è stato utilizzato un approccio utilizzante reduce, che però sconta il fatto che la parte di confronti viene sviluppata in Perl, perdendo dunque i vantaggi derivanti dall'implementazione C delle altre funzioni.

Dimmi dove sono

A volte è necessario lavorare con gli indici piuttosto che direttamente con i valori contenuti nella lista da analizzare. Un caso tipico potrebbe essere quello in cui abbiamo due liste fra loro accoppiate, ossia in cui elementi che si trovano nella stessa posizione sono logicamente associati fra loro, e vogliamo estrarre tutti gli elementi dalla seconda lista per cui il corrispondente elemento nella prima soddisfi una determinata proprietà.

Ok, sento urlare "Esempio! Esempio!" e non indugio oltre. Supponiamo di avere la funzione sin() calcolata su un intervallo dell'asse delle x corrispondente all'angolo giro centrato intorno allo 0:

 my $rapporto = 3.14159265358979 / 180;
 my @x = -179 .. 180;  # 360 gradi, passo 1 grado
 my @y = map { sin( $_ * $rapporto ) } @x;

La funzione seno lavora su gradi radianti, per cui si rende necessaria la conversione. Supponiamo ora che ci interessi avere tutti quei valori di angoli per cui la funzione è, ad esempio, maggiore di 0.5. Mentre è facile estrarre i valori stessi, utilizzando ad esempio grep come visto in precedenza:

 my @y_maggiore_05 = grep { $_ > 0.5 } @y;

non è altrettanto semplice per i corrispondenti valori sull'asse delle x, richiedendo in generale di iterare sugli indici:

 my @x_di_y_maggiore_05;
 for my $indice (0 .. $#x) {
    if ( $y[$indice] > 0.5 ) {
       push @x_di_y_maggiore_05, $x[$indice];
    }
 }

La funzione indexes fa proprio al caso nostro, consentoci di aggiungere una leggibilità senza pari:

 my @indici             = indexes { $_ > 0.5 } @y;
 my @x_di_y_maggiore_05 = @x[@indici];
 my @y_maggiore_05      = @y[@indici];

Algorithm::Loop

Un altro che ha deciso che non aveva abbastanza modi differenti per iterare è Tye McQueen. Il quale ha deciso di mettere tutto - ed un po' di più - in Algorithm::Loop. Questo modulo è molto grande, per cui è meglio evitare una rassegna puntuale in favore di un mordi-e-fuggi.

map non è abbastanza, voglio di più!

Una prima funzione utilissima è Filter, che - lo credereste? - è utile per filtrare liste. Lo so, lo so, abbiamo già map per quello. Però l'autore del modulo sa il fatto suo, per cui è meglio andare avanti.

Supponiamo che abbiate una lista di stringhe di testo, e che abbiate bisogno di sostituire la parola Tizio alla parola Caio in ciascuna di esse. Semplice: utilizziamo l'operatore s/// e siamo a cavallo. Meno semplice: l'operatore s/// modifica ciò su cui agisce (dandoci la zappa sui piedi), e per di più restituisce il numero di match trovati! In poche parole:

 my @filtrate = map { s/Tizio/Caio/g } @stringhe;

è semplicemente, incontrovertibilmente sbagliato. A meno che non vogliate modificare stringhe ed avere un array con il numero di modifiche fatte, ovviamente. Una soluzione è copiare il dato, modificare la copia e restituirla:

 my @filtrate = map {
    my $copia = $_;            # Fai un backup
    $copia =~ s/Tizio/Caio/g;  # Modifica la copia, l'originale e` al sicuro
    $copia;                    # Non dimentichiamoci di restituire $copia!
 } @stringhe;

Wow. Che bello sarebbe se... Ecco, Filter serve appunto a questo:

 use Algorithm::Loops qw( Filter );
 my @stringhe;
 
 # ... piu` tardi, quando tutti dormono...
 my @filtrate = Filter { s/Tizio/Caio/g } @stringhe;

Proprio quello che volevamo intendere con la prima map. Filter può essere utilizzata con tutte quelle funzioni od operatori che effettuano modifiche direttamente sui loro argomenti, come ad esempio chomp o tr///.

Hey, devo iterare su tre array contemporaneamente!

Un altro gruppo di funzioni interessanti in Algorithm::Loops è costituito dalla famiglia MapCar*. Fanno tutte la stessa cosa - iterare su più array contemporaneamente - solo che si comportano in maniera differente quando gli array hanno dimensioni differenti. Vediamo un esempio.

Supponiamo di essere in un museo e di ricevere dati da differenti strumenti di misura, vagamente sincronizzati fra loro. Vi ritrovate dunque con un po' di array, e volete costruire una hash indicizzata con il tempo di ciascuna misura:

 #!/usr/bin/perl
 use strict;
 use warnings;
 use Algorithm::Loops qw( MapCarMin );
 use Data::Dumper;
 
 my @timestamp;    # A che ora e` la misura
 my @temperatura;
 my @umidita;
 my @visitatori;
 
 # Simuliamo l'arrivo dei dati
 while (<DATA>) {
    chomp();
    my @dati = split /:/;
    push @timestamp, $dati[0];
    push @temperatura, $dati[1];
    push @umidita, $dati[2];
    push @visitatori, $dati[3];
 }
 
 # Ora torniamo al mondo reale, calcoliamo le statistiche
 my %statistiche = MapCarMin {
    my ($timestamp, @dati) = @_;  # Siamo in una funzione, di fatto
    $timestamp => \@dati;                 # Stesso trucco di map
 } \@timestamp, \@temperatura, \@umidita, \@visitatori;
 
 # Stampe, saluti e baci
 print(Dumper(\%statistiche));
 
 __DATA__
 9.30:24.5:80%:12
 10.00:25.5:83%:18
 10.30:26.0:85%:23

Come indicato dal commento, ci troviamo in realtà all'interno di una funzione, anche se non è presente la parola chiave sub immediatamente prima dell'apertura del blocco. Se volete sapere come si fa, studiatevi i prototipi in perlsub. Notate che al posto del blocco potete anche mettere un riferimento a sub.

Alla funzione viene passato un array @_, come di consueto. Gli elementi di questo array sono presi dagli array su cui stiamo iterando, uno ad uno nell'ordine (anche se questa regola può cambiare a seconda della particolare varietà di MapCar* che si sta utilizzando). In questo caso stiamo utilizzando MapCarMin, che itera finché non esaurisce almeno uno degli array. La stampata è la seguente:

 $VAR1 = {
           '10.00' => [
                        '25.5',
                        '83%',
                        '18'
                      ],
           '9.30' => [
                       '24.5',
                       '80%',
                       '12'
                     ],
           '10.30' => [
                        '26.0',
                        '85%',
                        '23'
                      ]
         };

Permutazioni

No, non sono modificazioni genetiche. E nemmeno permaturazioni di togniazziana memoria. Una permutazione di una lista è semplicemente un modo per scrivere gli stessi elementi in un altro ordine. L'insieme di tutti i possibili modi (ossia ordini) con cui possiamo scrivere la lista è detto insieme delle permutazioni.

A rigor di logica, le funzioni NextPermute* non consentono di implementare un ciclo; al contrario, consentono di trovare l'elemento successivo nell'insieme delle permutazioni di una lista. Potrebbero servirvi raramente - ma se vi servono? Beh, Algorithm::Loops sarà lì per servirvi! Supponiamo dunque di voler trovare tutte le permutazioni della lista dei primi tre numeri primi:

 #!/usr/bin/perl
 use strict;
 use warnings;
 use Algorithm::Loops qw( NextPermuteNum );
  
 # La lista di partenza
 my @lista = qw( 5 3 2 );
  
 # Creiamo una copia ordinata della lista
 my @lista_iterata = sort { $a <=> $b } @lista;
  
 # Partenza!
 $" = ", ";
 do {
         print("[@lista_iterata]\n");
 } while (NextPermuteNum(@lista_iterata));
 
 __END__
 
 [2, 3, 5]
 [2, 5, 3]
 [3, 2, 5]
 [3, 5, 2]
 [5, 2, 3]
 [5, 3, 2]

Due osservazioni sulla lista iterata:

  • viene modificata sul posto, per cui utilizzate una copia (come nell'esempio) per non rovinare la lista originale;
  • va passata ordinata in ordine crescente (da cui la nostra vecchia conoscenza, sort). Questo requisito è piuttosto blando, poiché la funzione deve trovare tutte le permutazioni.

NestedLoops

Eccoci arrivati alla fine. La soluzione estrema. Quello che avete sempre temuto e rimandato. NestedLoops.

Vi tocca gestire dei cicli annidati. Tocca a voi, avete cercato di evitarlo, avete cercato di farlo fare a qualcun altro ma niente, dovete farlo voi. Tanti cicli annidati, fino ad un livello incredibile. Non voglio sapere perché e percome, io vi dico come fare e basta, ok? Anzi, ve lo dice Algorithm::Loops.

Supponiamo che abbiate quella bella hash degli spettacoli cinematografici di prima, e che vogliate stamparla tutta. La soluzione immediata sarebbe la seguente:

 #!/usr/bin/perl
 use strict;
 use warnings;
   
 my %cinema;
 while (<DATA>) {
         chomp();  # Rimuovi a-capo alla fine
         my ($citta, $sala, $titolo, $orario) = split /:/;
         push @{$cinema{$citta}{$sala}{$titolo}}, $orario;
 }
   
 for my $citta (keys %cinema) {
    for my $sala (keys %{$cinema{$citta}}) {
       for my $titolo (keys %{$cinema{$citta}{$sala}}) {
          my @orari = @{$cinema{$citta}{$sala}{$titolo}};
          print("$citta\t$sala\t$titolo\t@orari\n");
       }
    }
 }
  
 __DATA__
 Roma:Alcazar:Madagascar:18.20
 Roma:Alcazar:Madagascar:20.30
 Roma:Alcazar:Madagascar:22.40
 Milano:Trianon:Madagascar:19.45
 Milano:Trianon:Madagascar:21.45
 Milano:Alcazar:Madagascar:16.00
 Milano:Alcazar:Madagascar:18.00
 Milano:Alcazar:Madagascar:20.00
 Roma:Alcazar:Seven Swords:21.00
 Roma:Alcazar:Seven Swords:23.00

Stampa:

 Milano  Trianon Madagascar      19.45 21.45
 Milano  Alcazar Madagascar      16.00 18.00 20.00
 Roma    Alcazar Seven Swords    21.00 23.00
 Roma    Alcazar Madagascar      18.20 20.30 22.40

Funziona! Peccato che sia bruttino, e soprattutto sia poco leggibile ed un po' complicato da maneggiare quando si decida di inserire altre "colonne". Lo stesso problema può essere risolto con NestedLoops:

 #!/usr/bin/perl
 use strict;
 use warnings;
 use Algorithm::Loops qw( NestedLoops );
 use Data::Dumper;
  
 my %cinema;
 while (<DATA>) {
         chomp();  # Rimuovi a-capo alla fine
         my ($citta, $sala, $titolo, $orario) = split /:/;
         push @{$cinema{$citta}{$sala}{$titolo}}, $orario;
 }
  
 NestedLoops(
    [
       [ ['' => \%cinema] ], # Loop esterno "finto"
       (  # Ciascuna iterazione fa sempre la stessa cosa, ossia
          # iterare sulle chiavi di un riferimento ad hash. Usiamo
          # pero' una coppia passando anche la chiave, in modo
          # da averla a disposizione per la stampa finale
          sub {
             my ($key, $hashref) = @$_;
             [ map { [$_ => $hashref->{$_}]} keys %$hashref ];
          }
       ) x 3, # 3 volte, una per %cinema, le altre per citta e sala
    ],
    sub { # Stampe
       # @_ contiene tutte le coppie
       my $last_ref = $_[$#_]->[1];  # Riferimento agli orari
       my @chiavi = map { $_->[0] } @_; # Estrai le chiavi
       shift @chiavi;  # La prima chiave e` fittizia
       local $, = "\t";
       local $\ = "\n";
       print(@chiavi, "@$last_ref");
    }
 );
  
 __DATA__
 Roma:Alcazar:Madagascar:18.20
 Roma:Alcazar:Madagascar:20.30
 Roma:Alcazar:Madagascar:22.40
 Milano:Trianon:Madagascar:19.45
 Milano:Trianon:Madagascar:21.45
 Milano:Alcazar:Madagascar:16.00
 Milano:Alcazar:Madagascar:18.00
 Milano:Alcazar:Madagascar:20.00
 Roma:Alcazar:Seven Swords:21.00
 Roma:Alcazar:Seven Swords:23.00

Uscita:

 Milano  Trianon Madagascar      19.45 21.45
 Milano  Alcazar Madagascar      16.00 18.00 20.00
 Roma    Alcazar Seven Swords    21.00 23.00
 Roma    Alcazar Madagascar      18.20 20.30 22.40

Non avremmo accettato niente di differente.

Piano con le proteste, lo so che sembra (un po') esoterico. E maledettamente complicato. Ma occorre notare che questo gioiellino è in grado di gestire praticamente qualunque livello di indentazione, in virtù della semplice riga:

      ) x 3, # 3 volte, una per %cinema, le altre per citta e sala

Potente, eh? Ok, ok, passiamo a come funziona.

NestedLoops accetta tre dati in ingresso:

  • la descrizione di ciò su cui deve iterare, sotto forma di array anonimo (come in [ '' => \%cinema ]) o di sub che restituisce un array anonimo (come nella sub ripetuta tre volte). La descrizione di ciascun passo del ciclo viene passata a sua volta come array anonimo, e costituisce il primo dato passato a NestedLoops;
  • opzionalmente, un riferimento ad una hash contenente alcune opzioni di iterazione, che nel nostro caso non abbiamo utilizzato;
  • infine, ma è opzionale anche questo, un riferimento ad una sub da chiamare ad ogni iterazione.

Il primo parametro, dunque, dice a NestedLoop quante e quali iterazioni deve effettuare, mediante un array anonimo. Il primo elemento dell'array descrive il ciclo più esterno, e man mano si trovano i cicli innestati. Nel nostro caso, lo spunto di partenza può sembrare strano:

 [ '' => \%cinema ], #...

ma è solo funzionale alla logica del resto della visita. Poiché stiamo visitando una struttura con hash innestate dentro altre hash, a ciascun passo possiamo estrarre le chiavi al particolare livello in cui ci troviamo. Estendiamo allora questo concetto anche alla prima iterazione, per cui facciamo in modo da passare un riferimento a %cinema.

Perché usare l'array anonimo a due posizioni, però? Ricordiamoci che non vogliamo solo arrivare agli orari, ma vogliamo anche stampare città, sala e titolo del film, per cui dobbiamo portarci dietro queste informazioni. In ciascuna iterazione, dunque, restituiamo un riferimento ad un array (come richiesto) contenente tutte queste coppie chiave/valore inserite all'interno di un array anonimo (in modo che venga passata la coppia tutta insieme). Tale processo viene ripetuto per 3 volte, la prima per entrare dentro %cinema, le altre due per gestire il livello della città e quello della sala. L'ultimo livello, quello del film, non va iterato perché è quello nel quale effettuiamo le stampe.

La sub da eseguire nel ciclo più interno riceve una lista di tutte i dati estratti, a ciascun livello, da ognuna delle descrizioni. Nel nostro caso, dunque, riceve una lista di array anonimi contenenti coppie chiave/valore, una per ciascun livello. Vengono allora estratti il riferimento agli orari, contenuto nell'ultima coppia, e le varie chiavi trovate nei differenti livelli, con l'accortezza che il primo livello viene eliminato perché fittizio.

Esoterismi

Pensavate che Algorithm::Loops fosse già al di là di quello che vi verrà mai in mente di utilizzare, eh? Come? Vi eravate già fermati a map? Datemi retta, quando avrete utilizzato map un paio di volte non vorrete più farne a meno... ma sto divagando.

Passiamo un attimo in rassegna le cose più esoteriche che abbiamo visto:

  • map è stato probabilmente il primo, e ci consente di implementare una piccola catena di montaggio;
  • seguono le varie contorsioni possibili con redo in un blocco;
  • reduce richiede senza dubbio un po' più di un paio di volte per essere compreso a fondo (ed essere abbandon*cough* adottato);
  • le ultra-specializzazioni di List::MoreUtils richiedono una memoria da elefante solo per sapere che esistono...
  • NestedLoops è potentissimo - come negarlo? - ma per raggiungere una certa fluidità nel suo utilizzo bisognerà forse sbatterci la testa contro un po' troppe volte.

Direi che ce n'è abbastanza, ma no! Siamo affamati! Vogliamo succhiare il tempo dei poveri programmatori Perl! Altra magia Perl nera! Ancora! Ancora!

Quantum::Superpositions

A mio modesto avviso, una delle maggiori punte di esoterismo la raggiunge il modulo Quantum::Superpositions. Che c'entra la meccanica quantistica con un tutorial sulle iterazioni in Perl? C'entra, c'entra...

Di Quantum::Superpositions ne parla già abbastanza larsen in questo articolo ( http://www.perlmonks.org/?node_id=98809 ) su Perl Monks. In più, ne so troppo poco per poter essere abbastanza corretto, ma mi addentro comunque...

Sostanzialmente, in meccanica quantistica l'unica cosa che si sa è che non si sa niente. Un po' come predicava Socrate. Ciascuna particella potrebbe trovarsi in uno stato o in un altro, e fino a che non l'abbiamo in qualche modo determinato è come se si trovasse virtualmente in tutti gli stati. Un po' come se il suo stato fosse la sovrapposizione di tutti gli stati possibili.

Oggi come oggi, i computer quantici cercano di sfruttare questa proprietà per implementare operazioni implicitamente parallele. Visto che la particella si trova in tutti gli stati contemporaneamente, possiamo fargli fare i calcoli con i vari stati contemporaneamente. Lo so, è poco chiaro anche a me, ma sono un chiacchierone con velleità di tuttologia, per cui se non fate altre domande siamo amici, ok?

L'ovvia replica a questo punto è: "ma noi non abbiamo un computer quantico!". Nemmeno io, come la maggior parte di noi del resto. Questo non vuol dire che, però, non possiamo pensare /come se l'avessimo/, e lasciare che il nostro computer tradizionale si occupi di emularlo (mettendoci tanto, tanto tempo in più...). È a questo punto che Quantum::Superpositions viene alla riscossa, perché si occupa di fare tutti i cicli necessari per emulare la contemporaneità senza che dobbiamo farlo noi.

Il modulo mette a disposizione due funzioni che assomigliano molto ad un paio di nostre vecchie conoscenze: any ed all, ma ne differiscono profondamente. Sostanzialmente queste due funzioni generano un oggetto che può essere considerato, a tutti gli effetti, un multivalore, una sovrapposizione di stati:

 my $multi_any = any( 1 .. 5 );
 my $multi_all = all( 5 .. 9 );

Fin qui niente di speciale, se non che $multi_any può essere considerato come "uno qualunque fra 1, 2, 3, 4 e 5" mentre $multi_all può vedersi come "5, 6, 7, 8 e 9 contemporaneamente".

Che ci facciamo ora? Cosa c'entrano con le iterazioni? Beh, il bello di queste due funzioni, e degli oggetti che generano, risiede proprio nel fatto che nascondono le iterazioni facendoli sembrare dei veri e propri elementi di un computer quantico. Ad esempio, possiamo scrivere molto concisamente:

 if ( all( 1 .. 5 ) <= all( 5 .. 9 ) ) {
    print 'Tutti gli elementi di 1 .. 5 sono minori o '
          . "uguali agli elementi di 5 .. 9\n";
 }

Magari non è efficiente, ma è d'effetto! E spesso essere leggibili (e divertirsi) è molto più importante che essere efficienti, per cui ce lo mettiamo da parte.

Ammettiamolo: non è un modulo facile da usare. Eppure ci permette di scrivere funzioni in maniera molto elegante (dalla documentazione, con un po' di modifiche per tenere conto dei casi particolari):

 #!/usr/bin/perl
 use strict;
 use warnings;
  
 use Quantum::Superpositions qw( all );
  
 for my $numero ( 1 .. 20 ) {
    print $numero, "\t", (risulta_primo($numero) ? '' : 'NON '), "primo\n";
 }
  
 sub risulta_primo {
    my ($n) = @_;
    return 0 if $n == 1;
    return 1 if $n == 2;
    return $n % all(2 .. sqrt($n) + 1) != 0;
 }

 __END__
 1       NON primo
 2       primo
 3       primo
 4       NON primo
 5       primo
 6       NON primo
 7       primo
 8       NON primo
 9       NON primo
 10      NON primo
 11      primo
 12      NON primo
 13      primo
 14      NON primo
 15      NON primo
 16      NON primo
 17      primo
 18      NON primo
 19      primo
 20      NON primo

Dov'è il ciclo?!? L'operatore %, che normalmente calcola il resto della divisione fra due valori, viene qui sovraccaricato per lavorare anche con la Superposition rappresentata da all(2..sqrt($n)+1), che rappresenta, per l'appunto, tutti i valori interi compresi fra 2 e la radice del numero da verificare più uno. Il sovraccarico consiste nel fatto che, implicitamente, l'operazione di modulo (ossia, %) viene applicata a tutti gli elementi, generando l'oggetto costituito da tutti i resti nella divisione di $n per ciascun elemento, ossia per ciascun valore fra 2 e la radice di $n più uno. Se tutti tali valori sono differenti da 0, allora il numero non è divisibile per questi divisori - quindi è primo!

Ok, anche qui ne avete abbastanza, eh?

Loop Voodoo: sostituzioni arcane

Eccoci arrivati al punto di non ritorno. Abbiamo già fatto le nostre incursioni nel lato oscuro della Perl-forza - devo forse ricordarvi i cicli-non-cicli con un blocco nudo e redo? - ma ce ne siamo tenuti alla larga. Abbiamo esplorato mondi strani ed esotici - Algoritm::Loops ne è un esempio lampante - ma niente che ci facesse diventare veramente cattivi con il lettore dei nostri programmi. Leggete a vostro rischio e pericolo: per sapere che esiste, ed evitarlo!

Temerari! State ancora leggendo?!? Va bene, l'avete voluto voi: si parla dell'operatore di sostituzione s///. Tutti gli operatori cosiddetti quote-like, ossia che lavorano similmente alle virgolette, contengono in realtà un ciclo al proprio interno. L'operatore di matching, m//, effettua una ricerca su tutta (o quasi) la stringa; l'operatore di translitterazione, tr///, sostituisce caratteri in tutta la stringa. Ma mentre in questi due i cicli sono immersi, per l'operatore di sostituzione abbiamo qualche speranza di inserirci nel giro. Vediamo come.

L'operatore di sostituzione viene specificato come segue:

 s/PATTERN/RIMPIAZZO/egimosx

In parole povere, cerca PATTERN e lo sostituisce con RIMPIAZZO. Le lettere minuscole finali, egimosx, sono dei flag che consentono di cambiare il comportamento dell'espressione regolare; ad esempio, impostando il flag i, si fa in modo che PATTERN sia considerato indifferente rispetto a minuscole e maiuscole.

Nel nostro caso sono due le opzioni che ci interessano:

  • g imposta la sostituzione come globale, ossia non si limita ad agire sulla prima occorrenza di PATTERN che viene trovata, ma su tutte quelle eventualmente presenti;
  • e fa sì che RIMPIAZZO sia interpretato come una espressione Perl, piuttosto che come una pura sequenza di caratteri.

Un esempio semplice di utilizzo dell'opzione g è il seguente: supponiamo di avere una stringa "ciao a tutti quanti", e di voler eliminare gli spazi in eccesso. Possiamo impostare una sostituzione per cui ciascuna sequenza di uno o più spazi viene rimpiazzata da un unico spazio, ma senza utilizzare l'opzione abbiamo un problema:

 my $stringa = "ciao   a    tutti quanti";
 $stringa =~ s/ +/ /;
 print $stringa, "\n";

 __END__

 ciao a    tutti quanti

La sostituzione è stata fatta fra ciao ed a, ma non fra quest'ultimo e tutti. Aggiungendo l'opzione g il gioco è fatto:

 my $stringa = "ciao   a    tutti quanti";
 $stringa =~ s/ +/ /g;
 print $stringa, "\n";

 __END__

 ciao a tutti quanti

L'utilizzo del flag e è invece molto, molto più raro, e generalmente indice del fatto che dovremmo scrivere meglio il nostro codice. Un esempio vi farà rendere conto di quanto sia bello e dannato:

 my $stringa = "La radice quadrata di 2 risulta essere SQRT(2)\n";
 $stringa =~ s/SQRT\((\d+)\)/sqrt($1)/e;
 print $stringa;

 __END__

 La radice quadrata di 2 risulta essere 1.4142135623731

Già vedo i lampi di genio nei vostri occhi. D'altronde, il sogno di ogni programmatore Perl è costruire il proprio sistema di template, giusto? Beh, questa opzione vi consente di costruirvene uno molto grezzo - ma evitate accuratamente, ed utilizzate quello che già esiste! Probabilmente il Template Toolkit può riempire il vostro vuoto e farvi utilizzare meglio il vostro tempo.

Cosa c'entra tutto questo con "fare i cicli in Perl"? Beh, un modo molto poco leggibile per scrivere un ciclo qualunque potrebbe essere il seguente:

 # Un ciclo molto bizzarro
 ($_ = 'x' x 5) =~ s/x/++$i; print $i, "\n"/eg;

 __END__

 1
 2
 3
 4
 5

A rimarcare che quanto scritto non si fa si è volutamente evitato di dichiarare la variabile $i: il codice risultante non gira sotto strict, quindi siete avvisati!

La prima parte costruisce una variabile temporanea $_ contenente cinque caratteri 'x'. Il numero di 'x' ci serve per poter iterare un numero uguale di volte: l'operatore di sostituzione, infatti, guarda caso ha un PATTERN uguale a 'x', e l'opzione g attiva, per cui andrà ad operare su ciascuna delle cinque 'x' presenti in $_. In virtù dell'opzione e, infine, la parte RIMPIAZZO è considerata un'espressione Perl, e viene valutata per tutte e cinque le volte: essa non fa altro che incrementare $i e stamparla.

Ora siete pronti a farvi ridere dietro dai vostri amici!

Un po' di riferimenti

I moduli descritti sono disponibili su CPAN (http://search.cpan.org/), prendeteli! Sia su CPAN che su Perl Monks (http://www.perlmonks.org/) potete inoltre trovare Randal L. Schwartz (merlyn), Graham Barr (gbarr), Tassilo von Parseval (vparseval su CPAN e, probabilmente, tassilo su Perl Monks) e Tye McQueen (tyemq in CPAN, tye in Perl Monks). Per tutto il resto, lunga vita al manuale!