-+  Documenti
 |-  Bibliografia
 |-  Articoli
 |-  Perlfunc
 |-  F.A.Q.
 |-  F.A.Q. iclp
-+  Eventi
-+  Contatti
-+  Blog
-+  Link



 

Versione stampabile.


Emanuele Zeppieri
Dirige una piccola impresa che si occupa di sviluppo e integrazione di sistemi informativi aziendali. Nella sua attività professionale, Perl si rivela uno strumento insostituibile.

Quest'articolo si propone di illustrare in maniera dettagliata e approfondita la tecnica dell'overloading delle costanti in Perl, tentando di sopperire a quella che è forse una delle rare carenze della documentazione ufficiale del Perl medesimo.

I fondamenti teorici sull’overloading delle costanti verranno introdotti e presentati in maniera tale da non richiedere al lettore conoscenze preliminari sull'argomento specifico né sull’overloading in generale. L'esposizione teorica verrà intercalata da alcuni esempi applicativi: uno di essi riguarderà l'implementazione di un localizzatore linguistico a basso costo, e potrà forse rivestire un qualche interesse di carattere pratico.

Gli esempi proposti verranno altresì utilizzati come pretesto per richiamare altre tecniche peculiari della programmazione in Perl, quali l'overloading degli operatori, l'uso dei moduli, gli hash multilivello e le closure, potendo quindi costituire anche un'utile introduzione a tali concetti (oppure un valido ripasso, o ancora un'inutile, incompleta e tediosa ripetizione, dipendentemente dal grado di Perl wizardry del lettore).

Introduzione

L'overloading delle costanti è una caratteristica del Perl estremamente potente, che solo in rarissimi casi (peraltro ignoti all'autore) trova corrispettivi in altri linguaggi di programmazione. Per capire di cosa si tratti, vediamo dapprima cos'è, in generale, l'overloading.

Con il termine overloading si è soliti designare alcune tecniche volte a ridefinire (o modificare) la semantica (ovvero il comportamento) di alcuni costrutti di un linguaggio di programmazione. La più nota delle tecniche di overloading è probabilmente il cosiddetto overloading degli operatori che, pur esulando dagli scopi di quest'articolo, richiameremo ed utilizzeremo a scopo esemplificativo.

L'overloading degli operatori (che, al contrario dell'overloading delle costanti, è comune a svariati linguaggi di programmazione orientati agli oggetti, con la scontata eccezione di linguaggi rozzi e primitivi come Java), consiste nella ridefinizione della semantica degli operatori predefiniti del linguaggio di programmazione, ovvero in altre parole nella modificazione del comportamento degli operatori medesimi (o meglio di alcuni di essi, perché non tutti gli operatori possono essere sottoposti ad overloading). L'overloading degli operatori permette altresì di differenziare il comportamento di uno stesso operatore a seconda del contesto nel quale viene utilizzato (ad esempio si può far dipendere il comportamento di un certo operatore dal tipo dei dati ai quali viene applicato).

Facciamo un esempio. Come è noto, l'operatore di addizione (+) in Perl (come del resto nella quasi totalità dei linguaggi di programmazione imperativi), è un operatore binario infisso che restituisce la somma algebrica dei suoi operandi:

    print 12 + 34;    # Stampa 46

Se invece tentatissimo di utilizzare in Perl l'operatore + con due operandi che fossero delle stringhe letterali non convertibili in numeri, otterremmo 0 come risultato, e poi otterremmo anche l'emissione del warning Argument <valore_stringa> isn't numeric in addition (+) (posto ovviamente che sia stata attivata la generazione dei warning):

    use warnings;
    print 'Buona' + 'notte!';    # Stampa 0 ed emette un warning

Il motivo per cui la somma predetta restituisce 0, è dovuto al fatto che perl valuta le stringhe in contesto numerico (che è una specializzazione del contesto scalare), per via della presenza dell'operatore +, che richiede appunto 2 operandi numerici. In questi casi perl tenta di convertire gli operandi in numeri e, quando tale conversione risulta impossibile (come con le stringhe da noi adoperate) restituisce il valore 0 (e se richiesto emette anche il warning), per cui in definitiva nel nostro caso la somma eseguita è 0+0.

Mediante l'overloading dell'operatore +, potremmo invece istruire perl ad eseguire la concatenazione degli operandi nel caso questi siano delle stringhe:

    print 'Buona' + 'notte!';    # Stampa Buonanotte!
    
    # Se abbiamo effettuato l'overloading dell'operatore +
    # nella maniera descritta.

Come appare evidente dall'esempio, l'operatore modificato mediante overloading conserva la sua sintassi predefinita, e varia automaticamente la sua semantica a seconda del tipo degli operandi a cui viene applicato.

Detto questo, è doveroso precisare che, nel caso specifico del Perl, ridefinire l'operatore + come suggerito poc'anzi sarebbe una pessima idea! (L'esempio proposto aveva scopi puramente esemplificativi.) Infatti se ridefinissimo l'operatore + nel modo che abbiamo ipotizzato, dovremmo rinunciare alla capacità di tale operatore, quando viene applicato alle stringhe, di eseguire la somma utilizzando l'interpretazione numerica delle stringhe medesime. Peraltro il Perl già offre un operatore binario infisso predefinito che esegue la concatenazione dei suoi operandi: l'operatore . (punto), il quale, assieme all'operatore di addizione + e assieme alla conversione automatica di stringhe in numeri e viceversa, copre ogni esigenza nella maniera più intuitiva, corretta e prevedibile:

    my $op_num =  12 ;
    my $op_str = '34';
    print $op_num + $op_str;    # Stampa 46
    print $op_num . $op_str;    # Stampa 1234

per cui l'eventuale ridefinizione di + sopra descritta sarebbe di scarsa utilità e rischierebbe invece di generare confusione e incertezza.

Quest'ultimo esempio mostra anche come l'overloading vada utilizzato in maniera accorta e oculata perché, modificando la semantica predefinita del linguaggio, potrebbe portare a risultati controintuitivi, con le ovvie conseguenze negative sulla leggibilità del codice. Per inciso, è proprio per timore di queste conseguenze che Java non offre la possibilità di eseguire l'overloading degli operatori (e tanto meno l'overloading delle costanti). I rischi connessi ad un abuso dell'overloading, sono però ampiamente compensati dai benefici che ne possono derivare nelle mani di un programmatore avveduto. Il linguaggio Java non tiene conto di questo perché, come è noto, fa delle assunzioni piuttosto offensive sulle capacità di chi se ne serve.

Avendo presentato, sia pure sommariamente, i concetti alla base dell'overloading, possiamo passare a quello che è l'oggetto principale della presente trattazione: l'overloading delle costanti.

Overloading delle Costanti

L'overloading delle costanti consiste nella ridefinizione del modo in cui perl interpreta i valori letterali presenti nel codice. Perl consente altresì di ridefinire le costanti in maniera selettiva, ovvero in base al loro tipo e al contesto nel quale vengono utilizzate.

Prima di procedere oltre, soffermiamoci un momento ad esaminare le modalità con cui perl interpreta le costanti. I valori letterali presenti nel codice vengono letti ed interpretati da perl durante la fase di compilazione, quindi prima che il codice medesimo venga eseguito. L'interpretazione delle costanti consiste nel riconoscimento del tipo della costante e nella successiva generazione dell'effettivo valore che verrà utilizzato durante l'esecuzione del programma in luogo della costante medesima.

(A rigore, esiste un'eccezione a quanto detto. Essa consiste nell'interpretazione delle espressioni con operandi costanti: in questo caso perl, durante la fase di compilazione, esegue un passo di ottimizzazione del codice noto col nome di constant folding, che consiste nella completa valutazione dell'espressione e nella sostituzione della stessa con il risultato ottenuto: in questo caso perl applica l'interpretazione delle costanti sopra descritta al solo risultato dell'espressione, e non ad ogni suo singolo operando).

Facciamo un semplice esempio:

    $a = 333_222_111;

In questo caso perl, durante la fase di compilazione, legge il valore letterale 333_222_111 (che chiameremo valore originale), lo riconosce come costante di tipo numerico intero (in Perl si può utilizzare nei numeri il simbolo _ (underscore) come separatore delle migliaia) e lo trasforma nel valore numerico 333222111 (che chiameremo valore interpretato). Nella successiva fase di esecuzione del codice, il valore interpretato 333222111 verrà assegnato alla variabile $a.

Nei dettagli le cose sono molto più complesse di quanto qui sommariamente descritto, ma per i nostri scopi non è necessario approfondire oltre (per ogni ulteriore approfondimento si rimanda a perlguts)

Ciò posto, possiamo definire con precisione dove avviene temporalmente l'overloading delle costanti: esso avviene durante la fase di compilazione, subito dopo che perl ha letto e interpretato la costante. Dopo questa fase, se abbiamo attivato l'overloading delle costanti, verrà eseguita una subroutine definita dal programmatore (usualmente chiamata handler) che può modificare arbitrariamente l'interpretazione predefinita della costante, generando un nuovo valore interpretato, che va a sostituire il valore interpretato generato in precedenza da perl per quella specifica costante. Anticipiamo che non è possibile attivare l'overloading delle costanti (né disattivarlo) durante la fase di run-time.

Il modulo overload

Per attivare l'overloading delle costanti, bisogna utilizzare la funzione overload::constant del modulo standard overload. Questa funzione prende come argomento un hash, le cui chiavi sono predefinite ed indicano il tipo delle costanti di cui si vuole effettuare l'overloading. Dette chiavi sono le seguenti:

  • integer

    identifica le costanti numeriche intere;

  • float

    identifica le costanti numeriche floating-point;

  • binary

    identifica le costanti numeriche binarie, ottali ed esadecimali;

  • q

    identifica le stringhe costanti, ovvero le stringhe quotate con gli apici singoli o con l'operatore q, nonché le porzioni letterali delle stringhe quotate con i doppi apici o con gli operatori qq e qx e le porzioni letterali degli here-document;

  • qr

    identifica le stringhe costanti nelle regex.

I corrispondenti valori dell'hash sono invece dei reference a subroutine. Tali subroutine (gli handler), vengono eseguite automaticamente da perl per ogni singola costante del tipo corrispondente incontrata nel codice durante la compilazione. I valori che esse restituiscono vengono poi utilizzati come nuovi valori interpretati per le costanti corrispondenti (i valori restituiti dalle subroutine verranno interpretati in contesto scalare). Le predette subroutine ricevono automaticamente in input i seguenti 3 parametri (nel medesimo ordine di seguito elencato):

  • il valore originale della costante;

    ovvero il valore letterale così come appare nel codice;

  • il valore interpretato della costante;

    ovvero il valore risultante dall'interpretazione della costante effettuata da perl (che può differire dal valore originale: si pensi al caso esemplificato in precedenza della presenza del simbolo _ quale separatore delle migliaia in una costante numerica, oppure al caso di un carattere speciale quale il newline (\n) in contesto interpolativo);

  • il contesto nel quale viene utilizzata la costante. Questo parametro è definito soltanto per le costanti di tipo stringa (ovvero per le costanti corrispondenti alle chiavi dell'hash q e qr citate in precedenza), e può assumere i seguenti valori:
    • qq

      se la costante è utilizzata in contesto interpolativo (ovvero con gli operatori "", qq, qx etc.)

    • q

      Se la costante è utilizzata in contesto non interpolativo (ovvero con gli operatori '', q etc).

    • s

      Se la costante è utilizzata all'interno della parte destra dell'operatore s///;

    • tr

      Se la costante è utilizzata come argomento degli operatori tr/// e y///.

L'overloading delle costanti può essere attivato e disattivato selettivamente anche a livello di scope lessicale, ovvero in un certo file ma anche soltanto in un certo blocco (ossia in una porzione di codice racchiusa tra le parentesi {}). Per attivare l'overloading delle costanti in un certo scope, bisogna prima definire un modulo esterno che invochi la funzione overload::constant all'interno del suo metodo import.

Prima di andare avanti, facciamo un rapido richiamo sull'esecuzione del metodo import di un modulo. import è un metodo che, se definito all'interno di un certo modulo, viene invocato automaticamente da perl quando si carica (o, appunto, si importa) il modulo medesimo all'interno di uno scope mediante l'operatore use. È possibile caricare un modulo anche mediante l'operatore require, ma in questo caso il metodo import del modulo non viene eseguito automaticamente, per cui si rende necessario invocarlo esplicitamente. Si noti infine che, se per l'importazione di un modulo utente che definisce l'overloading delle costanti si desidera utilizzare l'operatore require, è necessario utilizzare tale operatore (ed anche la successiva chiamata al metodo import) all'interno di un blocco BEGIN, dal momento che, come abbiamo già detto, l'overloading delle costanti può essere attivato soltanto durante la fase di compilazione. Concludiamo questa breve e sommaria digressione dicendo che se un modulo dispone anche di un metodo chiamato unimport, esso viene invocato automaticamente quando si disabilita il modulo mediante l'operatore no.

Una volta creato il predetto modulo esterno, per attivare l'overloading delle costanti in un determinato scope, sarà sufficiente importare il modulo in tale scope mediante use (o mediante require, fatte salve le precisazioni di cui sopra).

Se si vuole poi disattivare l'overloading in un dato scope (ripristinando così l'interpretazione predefinita delle costanti), bisogna disabilitare il modulo all'interno di tale scope mediante la direttiva no, secondo le usuali regole. Per permettere la disattivazione dell'overloading, è necessario però definire nel modulo esterno anche il metodo unimport, invocando al suo interno la funzione overload::remove_constant, la quale accetta gli stessi parametri della funzione overload::constant. Negli esempi che seguono avremo modo di analizzare in maggiore dettaglio quest'aspetto che, purtroppo, nella documentazione ufficiale di Perl è riportato in maniera assai sommaria e imprecisa.

Detto tutto questo, ci affrettiamo però a smentirlo (beh, almeno in parte: del resto in Perl per ogni regola c'è sempre un'eccezione, e in molti casi almeno un paio). La smentita riguarda il fatto che per attivare l'overloading delle costanti non è in realtà strettamente necessario utilizzare il metodo import né creare un modulo esterno, benché questo sia di gran lunga il metodo più corretto, flessibile e pulito per farlo. L'esplorazione di metodi alternativi per attivare l'overloading delle costanti viene lasciata come esercizio al lettore (suggerimento: da quanto abbiamo detto, dovrebbe apparire chiaro che è sufficiente eseguire la funzione overload::constant durante la fase di compilazione, quindi...) Ripetiamo però che ogni metodo per utilizzare l'overloading delle costanti che differisce da quello qui presentato è altamente scoraggiato (anche dalla documentazione ufficiale di Perl).

Esempi applicativi

Quanto detto finora non può che aver confuso le idee, per cui passiamo subito a degli esempi, con la speranza che questi abbiano invece un effetto chiarificatore.

Prima di procedere oltre, premettiamo che ognuno degli esempi che seguono consiste in un semplice programma (o script) Perl e in un modulo esterno che viene importato dallo script medesimo (mediante l'operatore use). Per eseguire tali esempi, sarà sufficiente salvare lo script Perl ed il modulo esterno nella stessa directory (utilizzando i nomi dei file che verranno indicati) e quindi lanciare lo script. In questo modo non dovreste avere alcun problema nell'eseguire gli script, a meno che non abbiate un perl configurato in maniera molto eretica (in quest'ultimo caso vi preghiamo di consultare la sezione relativa a @INC di perlvar).

Supponiamo di avere un semplicissimo programma che stampa a video alcune righe, che chiameremo, al culmine di un incontenibile impeto di fantasia visionaria, esempio.pl:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    print 123, "\n";
    print 456, "\n";
    print 789, "\n";
    print 'Ho stampato tre numeri interi.'

Questo programma, se eseguito, stampa semplicemente:

    123
    456
    789
    Ho stampato tre numeri interi.

Proponiamoci ora di impedirgli di stampare i numeri interi (che, si noti, appaiono nel codice come valori letterali), senza modificarne (troppo) il codice medesimo. A questo scopo creiamo un modulo esterno che, in preda ad un'altra botta di creatività, chiameremo EsempioCO.pm:

    # File EsempioCO.pm
    package EsempioCO;
    
    use strict;
    use overload;
    
    sub import {
        overload::constant(
            integer => sub { 'Io non stampo costanti intere!' }
        )
    }
    
    1;

Nel codice sopra esposto dovremmo essere in grado di individuare facilmente tutto quanto descritto in precedenza: anzitutto il caricamento del modulo overload, quindi la definizione del metodo import, che contiene la chiamata alla funzione overload::constant, la quale prende come parametro un hash con la chiave integer ed il corrispondente valore che è un reference ad una subroutine (sotto forma di subroutine anonima). Ora essendo il reference all'handler (ovvero alla subroutine) relativo alla chiave integer, quello che ci aspettiamo è che il valore restituito da tale handler (ossia la stringa Io non stampo costanti intere!) vada a sostituire ogni costante numerica intera (o meglio il suo valore interpretato) presente nello scope ove andremo a caricare il modulo medesimo. E infatti, importando EsempioCO.pm all'interno del programma esempio.pl, in questo modo:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    # Importazione del modulo che definisce l'overloading.
    use EsempioCO;
    
    print 123, "\n";
    print 456, "\n";
    print 789, "\n";
    print 'Ho stampato tre numeri interi.'

otteniamo un programma schizofrenico, visto che produce il seguente output:

    Io non stampo costanti intere!
    Io non stampo costanti intere!
    Io non stampo costanti intere!
    Ho stampato tre numeri interi.

Figo eh? ;-)

Ora andiamo avanti con l'applicazione di quanto in precedenza enucleato a livello teorico, proponendoci di modificare in esempio.pl anche l'interpretazione della costante di tipo stringa utilizzata nell'ultima print. Per farlo, potremmo essere tentati di aggiungere in EsempioCO.pm un handler per le costanti di tipo stringa scarno e semplice come quello che abbiamo già utilizzato per le costanti intere, in questo modo:

    # File EsempioCO.pm
    package EsempioCO;
    
    use strict;
    use overload;
    
    sub import {
        overload::constant(
            integer => sub { 'Io non stampo costanti intere!' },

            # Nuovo handler per l'overloading delle stringhe costanti,
            # ma è troppo brutale!
            q       => sub { 'E neanche le costanti stringa!' }
        )
    }
    
    1;

ma se eseguissimo di nuovo esempio.pl dopo quest'ultima modifica di EsempioCO.pm, otterremmo il seguente output:

Io non stampo costanti intere!E neanche le costanti stringa!Io non stampo costanti intere!E neanche le costanti stringa!Io non stampo costanti intere!E neanche le costanti stringa!E neanche le costanti stringa!

Perché? Perché a causa di un handler così rozzo, abbiamo ridefinito tutte le costanti di tipo stringa presenti nel codice, compresi i newline: "\n"). Per applicare l'overloading alla sola stringa che ci interessa, abbiamo invece bisogno di un handler per le stringhe un po' più sofisticato, che lascia inalterati i newline, come questo:

    # File EsempioCO.pm
    package EsempioCO;
    
    use strict;
    use overload;
    
    sub import {
        overload::constant(
            integer => sub { 'Io non stampo costanti intere!' },
            q       => \&handler_stringhe
        )
    }
    
    # Nuovo handler per le stringhe, che filtra i newline.
    sub handler_stringhe {
        my ($valore_originale, $valore_interpretato, $contesto) = @_;
        return
            $valore_originale eq '\n'
                ? $valore_interpretato    # Lascia inalterata la costante.
                : 'E neanche le costanti stringa!'

    }
    
    1;

E con questo nuova versione di EsempioCO.pm, l'output di esempio.pl sarebbe proprio quello che cercavamo:

    Io non stampo costanti intere!
    Io non stampo costanti intere!
    Io non stampo costanti intere!
    E neanche le costanti stringa!

Si noti come nel nuovo handler, per verificare se la stringa oggetto dell'overloading è un newline, effettuiamo un test sul suo valore originale e, in caso di successo del test, restituiamo il suo valore interpretato. È altresì opportuno osservare che l'handler utilizzato è adeguato a gestire solo il nostro semplice esempio, mentre per gestire casi più complessi (ad esempio per filtrare i caratteri speciali solo quando vengono utilizzati in contesto interpolativo) è necessario ricorrere ad handler ancora più sofisticati, che tengano conto anche del parametro $contesto.

Per concludere con gli esempi introduttivi, verifichiamo che l'overloading delle costanti può essere attivato/disattivato selettivamente a livello di scope lessicale. Siccome ora vogliamo permettere anche la disattivazione dell'overloading, dobbiamo anzitutto modificare EsempioCO.pm, aggiungendovi anche il metodo unimport, in accordo con quanto detto in precedenza:

    # File EsempioCO.pm
    package EsempioCO;
    
    use strict;
    use overload;
    
    sub import {
        overload::constant(
            integer => sub { 'Io non stampo costanti intere!' },
            q       => \&handler_stringhe
        )
    }
    
    sub handler_stringhe {
        my ($valore_originale, $valore_interpretato, $contesto) = @_;
        return
            $valore_originale eq '\n'

                ? $valore_interpretato    # Lascia inalterata la costante.
                : 'E neanche le costanti stringa!'
    }
    
    sub unimport {
        # Rimuoviamo l'overloading di *tutte* le costanti.
        overload::remove_constant(
            integer => undef,
            q       => undef
        )
    }
    
    1;

Ciò posto, per vedere se l'attivazione e la disattivazione selettive dell'overloading a livello di scope funzionano, scriviamo quest'ultima versione di esempio.pl:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    use EsempioCO;
    # Qui l'overloading è attivo.
    print 123, "\n";
    print 456, "\n";

    {
        no EsempioCO;
        # In questo blocco l'overloading è stato disattivato.
        print 666, "\n";
        print 'Ho stampato un intero: overloading disabilitato!', "\n";
    }
    
    # Dopo il blocco l'overloading torna attivo.
    print 789, "\n";
    print 'Ho stampato tre numeri interi.'

e verifichiamo che, se eseguita, essa stampa:

    Io non stampo costanti intere!
    Io non stampo costanti intere!
    666
    Ho stampato un intero: overloading disabilitato!
    Io non stampo costanti intere!
    E neanche le costanti stringa!

che è esattamente quello che ci aspettavamo (se viceversa il lettore rileva risultati inaspettati, vuol dire che quest'articolo ha fin qui fallito in maniera miseranda in ogni sua aspirazione di chiarezza; può darsi comunque che le cose migliorino in seguito: concedeteci un ulteriore credito).

Prima di passare alle altre applicazioni, vi dobbiamo il chiarimento promesso riguardo gli handler da utilizzare come valori dell'hash passato alla funzione overload::remove_constant (la documentazione ufficiale del perl non dice nulla al riguardo, per cui ci è toccato andare a sbirciare all'interno del codice sorgente del modulo overload). Sembra (dico, sembra) sia sufficiente utilizzare come handler un qualsiasi valore scalare: nell'esempio precedente abbiamo utilizzato undef, ma anche utilizzando, ad esempio, 0 o 1 o 2 o 'aaa' o \'un reference' o anche (1, 2, 3) (che, pur essendo una lista, in quanto valore di un hash verrebbe valutata in contesto scalare, restituendo 3), il risultato sarebbe stato il medesimo. In definitiva, per disattivare l'overloading delle costanti di un certo tipo, sembra sia importante fornire soltanto la chiave dell'hash corrispondente a quel determinato tipo.

Un localizzatore linguistico a basso costo

È probabile che qualche volta vi sia capitato di trovare un programma, redatto da altri, che soddisfaceva tutte le vostre esigenze del momento, tranne quella di parlare la vostra lingua (cioè il programma produceva report e messaggi utente di vario tipo in una lingua straniera). Ad esempio vi sarà capitato di trovare programmi che parlano soltanto in inglese e, benché conosciate bene questa lingua, la cosa vi ha impedito di diffondere il programma stesso tra i vostri amici, colleghi o clienti. Oltre a parlare esclusivamente l'inglese, l'ipotetico programma sembrava poi non essere predisposto per la localizzazione, nel senso che produceva tutto il suo output mediante print di stringhe costanti distribuite quà e là nel codice, cosa che vi ha dissuaso dall'impelagarvi in una lavoro di traduzione, perché sarebbe stato difficilmente portabile alle versioni successive del programma e perché sarebbe stato anche difficilmente estensibile ad altre lingue, nel caso aveste necessitato di una completa internazionalizzazione del programma.

In questo caso l'overloading delle costanti si rivela uno strumento prezioso (almeno per i programmi più brevi e semplici: per una soluzione più corretta e professionale al problema della localizzazione dei programmi, si consulti la sezione "Riferimenti"), permettendoci di localizzare un intero programma con l’aggiunta di una sola linea di codice.

Vediamo come, tramite l’esempio che segue. Supponiamo di avere questo semplice programma Perl, che chiameremo english.pl:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    print "This program\n";
    print "speaks\n";
    print "English!\n";
    print "(Copyright 2006 John Smith)"

il quale, se eseguito, stampa:

    This program
    speaks
    English!
    (Copyright 2006 John Smith)

Ora l'idea è quella di intervenire il meno possibile sul codice di english.pl, riuscendo nondimeno a tradurre ogni stringa che necessita di essere localizzata. L'idea è quella di sfruttare l'overloading delle costanti (ovviamente!) fornendo come nuovo valore interpretato per ogni stringa costante la traduzione in italiano della stringa medesima. Per stabilire la corrispondenza tra le stringhe in lingua inglese e le stringhe tradotte in italiano, utilizzeremo un hash.

Il codice del modulo esterno che implementa l'overloading delle costanti, che chiameremo Traduttore.pm, è il seguente:

    # File Traduttore.pm
    package Traduttore;
    
    use strict;
    use overload;
    
    # Frasario Inglese-Italiano.
    my %italiano = (
        'This program\n' => "Questo programma\n",
        'speaks\n'       => "parla\n",
        'English!\n'     => "italiano!\n"    
    );
    
    sub import {
        overload::constant( q => \&traduci_stringhe )
    }
    
    sub traduci_stringhe {
        my ($valore_originale, $valore_interpretato, $contesto) = @_;
        
        return
            exists $italiano{$valore_originale} # La stringa deve essere tradotta?
            ?      $italiano{$valore_originale} # Se sì, traducila.
            :      $valore_interpretato         # Se no, restituiscila inalterata.
    }
    
    1;

Si osservi la struttura dell'hash che implementa il frasario inglese-italiano: come chiavi sono state adoperate le stringhe originali in contesto non interpolativo (visto che vengono confrontate con il valore originale delle costanti), mentre come valori dell'hash sono state adoperate le stringhe (tradotte) in contesto interpolativo, ovvero nel medesimo contesto in cui le stringhe appaiono nel codice, allo scopo di ottenere gli stessi ritorni a capo del programma originale. Si esamini altresì il test utilizzato per restituire il nuovo valore interpretato delle costanti: nell'hash facciamo apparire come chiavi soltanto le stringhe che vogliamo tradurre, in modo tale che con un semplice test di esistenza della chiave medesima, possiamo lasciare inalterate le stringhe che non devono essere tradotte (come il copyright, nel nostro caso).

Come al solito, importiamo il nostro modulo all'interno del programma originale, mediante use:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    # Attiviamo la traduzione, con una sola riga!
    use Traduttore;
    
    print "This program\n";
    print "speaks\n";
    print "English!\n";
    print "(Copyright 2006 John Smith)"

eseguiamo il programma, e verifichiamo che tutto funzioni:

    Questo programma
    parla
    italiano!
    (Copyright 2006 John Smith)

e il tutto aggiungendo una sola riga al codice originale! Ma possiamo fare ancora di meglio, si veda oltre.

Il localizzatore diventa multilingue

L'appetito vien mangiando, per cui ora ci proponiamo di attivare la localizzazione verso una qualsiasi lingua, in maniera parametrica. Rispetto al modulo precedente, si rendono necessarie due modifiche:

  • per implementare il frasario, stavolta dobbiamo utilizzare un hash a 2 livelli, ovvero un hash nel quale il valore corrispondente ad ogni frase inglese originale è un reference ad un altro hash, che a sua volta ha i codici dei paesi come chiavi e le frasi tradotte nella lingua corrispondente come valori.
  • Per effettuare la traduzione, stavolta l'handler deve eseguire la ricerca nel frasario sia in base alla frase originale che alla lingua target. Pertanto ci toccherà definire dinamicamente il codice dell'handler a seconda della lingua richiesta, non avendo la possibilità di passare parametri all'handler medesimo (ricordiamoci che la funzione overload::constant richiede soltanto il reference all'handler, senza darci alcuna possibilità di passargli parametri).

Quest'ultima necessità può essere risolta in modo molto elegante con una closure (chiusura), che, detto informalmente, è una subroutine che rimane legata allo scope lessicale nel quale viene definita. Siccome questa definizione non è particolarmente evocativa, ricorriamo a termini più pratici: quello che faremo è utilizzare una subroutine che definisce un'altra subroutine (che sarebbe il nostro handler) e ne restituisce il reference. La subroutine più esterna, essendo una normale subroutine, può accettare il codice della lingua come parametro d'ingresso, col quale costruirà dinamicamente l'handler specializzato per la traduzione verso quella specifica lingua.

Infine vogliamo poter indicare la lingua di destinazione all'interno del programma principale, e nella maniera più semplice possibile: per far questo possiamo passare il codice del paese come secondo parametro all'operatore use, e tale parametro verrà automaticamente passato da perl (sempre al secondo posto) al metodo import del nostro modulo.

Ecco il nuovo modulo, che chiameremo TraduttoreMultilingue.pm, completo:

    package TraduttoreMultilingue;
    
    use strict;
    use overload;
    
    # Frasario implementato mediante hash a 2 livelli.
    my %frasario = (
        'This program\n' => {
            it => "Questo programma\n",
            fr => "Ce programme\n",
            de => "Dieses programm\n"

        },
        'speaks\n' => {
            it => "parla\n",
            fr => "parle\n",
            de => "spricht\n"
        },
        'English!\n' => {
            it => "italiano!\n",
            fr => "français!\n",
            de => "Deutsches!\n"

        }
    );
    
    # Subroutine che definisce l'handler e ne restituisce il reference.
    sub genera_traduci_stringhe {
        my $cod_paese = shift;
    
        return
            # Handler che utilizza il parametro $cod_paese che è stato
            # passato in ingresso alla subroutine genera_traduci_stringhe()
            sub {
                my ($valore_originale, $valore_interpretato, $contesto) = @_;
                return
                    exists $frasario{$valore_originale}
                    ?      $frasario{$valore_originale}{$cod_paese}
                    :      $valore_interpretato
            }
    
    }
    
    sub import {
        # genera_traduci_stringhe() prende in ingresso il codice del paese,
        # passato ad import() come secondo parametro in fase di caricamento
        # del modulo nel programma english.pl
        overload::constant( q => genera_traduci_stringhe( $_[1] ) )
    }
    
    1;

Volendo ora ottenere, ad esempio, la traduzione del programma in francese, sarà sufficiente modificare english.pl con questa semplice aggiunta:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    # Il codice del paese va passato a use come secondo parametro.
    use TraduttoreMultilingue 'fr';
    
    print "This program\n";
    print "speaks\n";
    print "English!\n";
    print "(Copyright 2006 John Smith)"

che produce il seguente output:

    Ce programme
    parle
    fran├žais!
    (Copyright 2006 John Smith)

mentre invece utilizzando in english.pl la seguente direttiva:

    use TraduttoreMultilingue 'de';

avremmo ottenuto la traduzione dell'output in tedesco:

    Dieses programm
    spricht
    Deutsches!
    (Copyright 2006 John Smith)

Bello eh? :-)

Anche se il lettore più attento potrà osservare che, se l'overloading delle costanti può essere attivato soltanto in fase di compilazione, la traduzione multilingue che abbiamo presentato non è poi così dinamica, nel senso che non si può saltare da una lingua all'altra ad esempio passando all'operatore use il parametro che identifica il codice del paese, durante l'esecuzione del programma medesimo. In realtà questo si può fare, sia pure con un trucco, che serve a forzare la ricompilazione del codice. Il trucco consiste nell'utilizzare un cosiddetto thin wrapper, ovvero un (semplice) programma esterno che richiama il nostro programma english.pl, in modo da forzarne la ricompilazione. Questo si può fare molto facilmente, ad esempio mediante la funzione system (oppure mediante fork, se vogliamo maggiore controllo). Se siete tentati di attivare l'overloading delle costanti mediante il costrutto eval '...', sappiate che non si può fare, almeno fino all'attuale versione di perl (5.8.8). Il wrapper predetto sarebbe, nella sua forma più scheletrica, una cosa di questo tipo:

    #!/usr/bin/perl
    use strict;

    my $cod_paese = 'fr';
    system 'perl', 'english.pl', $cod_paese;

    # ...

    $cod_paese = 'de';
    system 'perl', 'english.pl', $cod_paese;

    # etc.

mentre la direttiva che attiva l'overloading delle costanti in english.pl dovrebbe diventare:

    use TraduttoreMultilingue $ARGV[0];

per prendere il parametro $cod_paese passatogli dall'esterno da system.

Bene, per concludere direi che come applicazione non è male ma, per correttezza deontologica (e anche perché tanto lo avevamo già anticipato), va precisato che l'approccio più corretto alla localizzazione del codice (specialmente quando i programmi da localizzare sono molto complessi), è quello promosso da strumenti quali quelli elencati nella sezione "Riferimenti".

Far parlare i numeri!

Supponiamo che abbiate un programma che inizializza i valori di alcune variabili con delle costanti numeriche, quindi esegue dei calcoli su tali variabili, e alla fine ne stampa il risultato in forma numerica, normalmente (ad esempio 33). Sapevate che, con l'irrisoria aggiunta della solita unica riga, possiamo fare in modo che il programma stampi i numeri in forma testuale estesa (ad esempio trentatre), peraltro preservando anche i calcoli? Con l'overloading delle costanti si fa banalmente (in realtà gran parte del merito va al pregevolissimo Lingua::IT::Numbers di Leo Cacciari).

Stavolta partiamo dal modulo che implementa il nostro overloading (che chiameremo NumeriTestuali.pm), il quale, se importato in un programma, ne prende tutte le costanti di tipo intero e di tipo float e le trasforma in oggetti della classe Lingua::IT::Numbers:

    # File NumeriTestuali.pm
    package NumeriTestuali;
    
    use strict;
    use overload;
    
    use Lingua::IT::Numbers;
    
    sub import {
        overload::constant(
            integer => \&in_forma_testuale,
            float   => \&in_forma_testuale
        )
    }
    
    sub in_forma_testuale {
        my ($valore_originale, $valore_interpretato, $contesto) = @_;
        return Lingua::IT::Numbers->new($valore_interpretato)
    }
    
    1;

E questo è il nostro programmino di test (numeri.pl) che, oltre a dei banalissimi calcoli, ha soltanto l'importazione del nostro modulo NumeriTestuali.pm:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    use NumeriTestuali;
    
    my $x     = 33.15;
    my $y     = 3_200_033;
    my $somma = $x + $y;
    
    print "x   = $x\n";
    print "y   = $y\n";
    print "x+y = $somma";

E questo è infine l'output che esso produce:

    x   = trentatre virgola quindici
    y   = tre milioni duecentomilatrentatre
    x+y = tre milioni duecentomilasessantasei virgola quindici

Ora non so tutto questo a chi possa mai servire (forse ai notai: mi sembra di ricordare che redigano i loro atti scrivendo i numeri sempre in forma testuale), ma non venitemi a dire che non è bello! ;-)

Conclusioni

Al di là dei risvolti applicativi (molto discutibili, se non del tutto assenti), mi auguro che questo articolo sia servito a fornire qualche spunto di riflessione, ed anche a rendere omaggio (sia pure indegnamente) alla grandezza di Perl.

Riferimenti

Versione

Ver. 0.8 (2006-03-20)

Autore

Emanuele Zeppieri <emazep(at)tiscali.it>

Copyright

Overloading delle Costanti: teoria e applicazioni - Copyright (c) 2006 Emanuele Zeppieri.


Ti è piaciuto questo articolo? Iscriviti al feed!











Devo ricordare i dati personali?






D:
Annunci Google