Perl moderne : 2014

Prenez le contrôle de votre programmation avec le Perl moderne.


précédentsommairesuivant

VI. Fonctions

Une fonction (ou sous-programme ou subroutine) en Perl est une unité de comportement discrète et encapsulée. Un programme est un ensemble de petites boîtes noires où l'interaction de ces fonctions détermine le flot d'exécution du programme. Une fonction peut avoir un nom. Elle peut consommer de l'information entrante. Elle peut produire de l'information sortante.

En Perl, les fonctions sont un mécanisme privilégié pour l'abstraction, l'encapsulation et la réutilisation de code.

VI-A. Déclarer des fonctions

Utilisez la commande interne sub pour déclarer une fonction :

 
Sélectionnez
sub salue_moi  { ... }

Maintenant, la fonction salue_moi() est disponible et peut être appelée partout ailleurs dans le programme.

Tout comme vous pouvez déclarer une variable lexicale, mais laisser sa valeur non définie, vous pouvez déclarer une fonction sans la définir. Une déclaration préalable indique à Perl d'enregistrer qu'une fonction nommée existe. Vous pouvez la définir plus tard :

 
Sélectionnez
sub salue_soleil;

VI-B. Appeler des fonctions

Utilisez des parenthèses postfixées (FixitéFixité) pour l'appel d'une fonction nommée. Les arguments de la fonction peuvent être placés entre parenthèses :

 
Sélectionnez
salue_moi( 'Jack', 'Tuxie' );
salue_moi( 'Snowy' );
salue_moi();

Bien que ces parenthèses ne soient pas strictement nécessaires pour ces exemples, même avec strict activé, elles offrent de la clarté aux lecteurs humains ainsi qu'à l'analyseur de Perl. En cas de doute, gardez-les.

Les arguments de fonction peuvent être des expressions arbitraires, y compris des variables et des appels de fonction :

 
Sélectionnez
salue_moi( $nom);
salue_moi( @auteurs );
salue_moi( %editeurs );
salue_moi( get_lecteurs() );

… même si la façon par défaut dont Perl traite les paramètres surprend parfois les débutants.

VI-C. Paramètres de fonction

Une fonction reçoit ses paramètres dans un seul tableau, @_ (Les variables tableau par défautLes variables tableau par défaut). Lorsque vous appelez une fonction, Perl aplatit tous les arguments fournis en une liste unique. La fonction doit extraire les paramètres en variables ou opérer directement sur @_ :

 
Sélectionnez
sub salue_un
{
    my ($nom) = @_;
    say "Salut, $nom !";
}

sub salue_tous
{
    say "Salut, $_ !" for @_;
}

@_ se comporte comme un tableau normal. La plupart des fonctions Perl extraient les paramètres avec shift ou utilisent l'affectation de liste, mais il est possible d'accéder aux éléments individuels par indice :

 
Sélectionnez
sub salue_un_shift
{
    my $nom = shift;
    say "Salut, $nom !";
}

sub salue_deux_affectation_liste
{
    my ($heros, $copain) = @_;
    say "Bon si ce n'est pas $heros et $copain. Bienvenue !";
}

sub salue_un_indice
{
    my $nom = $_[0];
    say "Salut, $nom !";

    # ou moins clair
    say "Salut, $_[0] !";
}

Vous pouvez également utiliser unshift, push, pop, splice et slice sur @_. Rappelez-vous que les fonctions internes de manipulation de tableaux utilisent @_ comme opérande par défaut à l'intérieur des fonctions, de sorte que my $nom = shift; fonctionne. Profitez de cet idiome.

L'affectation d'un paramètre scalaire à partir de @_ nécessite shift, l'accès par indice à @_, ou un contexte de liste à gauche de l'affectation (lvalue) — des parenthèses. Autrement, Perl évaluera joyeusement @_ en contexte scalaire et affectera le nombre de paramètres transmis :

 
Sélectionnez
sub mauvais_salue_un
{
    my $nom = @_;  # bogue
    say "Salut, $nom; tu sembles numérique aujourd'hui !"
}

L'affectation d'une liste de paramètres est souvent plus claire que plusieurs lignes de shift. Comparez :

 
Sélectionnez
my $valeur_de_gauche = shift;
my $operation        = shift;
my $valeur_de_droite = shift;

... à :

 
Sélectionnez
my ($valeur_de_gauche, $operation, $valeur_de_droite) = @_;

Ce dernier exemple est plus simple à lire et est même légèrement plus rapide (bien que sa lisibilité améliorée soit beaucoup plus importante que la performance).

Il est parfois nécessaire d'extraire certains paramètres de @_ et de passer le reste à une autre fonction :

 
Sélectionnez
sub methode_deleguee
{
    my $self = shift;
    say 'Appeler methode_deleguee()'

    $self->delegate->methode_deleguee( @_ );
}

Utilisez shift lorsque votre fonction a besoin d'un seul paramètre. Utilisez l'affectation de liste lors de l'accès à plusieurs paramètres.

Signatures réelles de fonction

Plusieurs distributions du CPAN étendent la manipulation de paramètres de Perl avec une syntaxe et des options supplémentaires. signatures et Method::Signatures sont puissantes. Method::Signatures::Simple est basique, mais utile. MooseX::Method::Signatures fonctionne très bien avec Moose (MooseMoose). Function::Parameters mérite d'être explorée. Moops va beaucoup plus loin pour ajouter la syntaxe OO, mais contient aussi de bonnes signatures de fonctions.

VI-C-1. Aplatissement

L'aplatissement de liste en @_ se passe du côté appelant d'un appel de fonction. Le passage d'un hachage comme argument produit une liste de paires clé/valeur :

 
Sélectionnez
my %animaux_noms_et_types = (
    Lucky   => 'chien',
    Rodney  => 'chien',
    Tuxedo  => 'chat',
    Petunia => 'chat',
    Rosie   => 'chien',
);

afficher_animaux( %animaux_noms_et_types );

sub afficher_animaux
{
    my %animaux = @_;
    while (my ($nom, $type) = each %animaux)
    {
        say "$nom est un $type";
    }
}

Lorsque Perl aplatit %animaux_noms_et_types dans une liste, l'ordre des paires clé/valeur du hachage varie, mais la liste contiendra toujours une clé suivie immédiatement par sa valeur. L'affectation du hachage à l'intérieur de afficher_animaux() fonctionne de la même manière que l'affectation explicite à %animaux_noms_et_types.

Cet aplatissement est souvent utile, mais évitez de mélanger des scalaires et des variables composites aplaties en listes de paramètres. Pour écrire une fonction afficher_animaux_par_type() où l'un des paramètres est le type d'animal de compagnie à afficher, passez le type comme premier paramètre (ou utilisez pop pour le supprimer de la fin de @_, si vous aimez dérouter les gens) :

 
Sélectionnez
sub afficher_animaux_par_type
{
    my ($type, %animaux) = @_;

    while (my ($nom, $espece) = each %animaux)
    {
        next unless $espece eq $type;
        say "$nom est un $espece";
    }
}

my %animaux_noms_et_types = (
    Lucky   => 'chien',
    Rodney  => 'chien',
    Tuxedo  => 'chat',
    Petunia => 'chat',
    Rosie   => 'chien',
);

afficher_animaux_par_type( 'chien',   %animaux_noms_et_types );
afficher_animaux_par_type( 'chat',    %animaux_noms_et_types );
afficher_animaux_par_type( 'orignal', %animaux_noms_et_types );

VI-C-2. Gourmandise

L'affectation de liste d'une variable agrégée est toujours gourmande, donc l'affectation à %animaux absorbe (slurps) toutes les valeurs restantes de @_. Si le paramètre $type se trouve à la fin de @_, Perl émettrait un avertissement sur l'affectation d'un nombre impair d'éléments au hachage. Vous pourriez contourner cela :

 
Sélectionnez
sub afficher_animaux_par_type
{
    my $type    = pop;
    my %animaux = @_;

    ...
}

... au détriment de la clarté. Le même principe s'applique lors de l'affectation d'un tableau comme paramètre. Utilisez des références (RéférencesRéférences) pour éviter l'aplatissement global indésirable.

VI-C-3. Gestion des alias

Le tableau @_ contient une subtilité : il crée des alias sur les arguments des fonctions. Autrement dit, vous pouvez modifier les arguments passés à la fonction si vous accédez directement à @_ :

 
Sélectionnez
sub modifier_nom
{
    $_[0] = reverse $_[0];
}

my $nom = 'Orange';
modifier_nom( $nom );
say $nom;

# affiche egnarO

Modifiez directement un élément de @_ et vous allez modifier l'argument initial. Soyez prudent et déballez le contenu de @_ rigoureusement ou documentez soigneusement la modification.

VI-D. Fonctions et espaces de noms

Chaque fonction possède un espace de noms conteneur (PaquetagesPaquetages). Les fonctions se trouvant dans un espace de noms non déclaré — fonctions non déclarées dans la portée d'un package déclaré explicitement — existent dans l'espace de noms main. Vous pouvez également déclarer une fonction au sein d'un autre espace de noms en la faisant précéder par le nom de celui-ci :

 
Sélectionnez
sub Extensions::Math::add { ... }

Cela déclarera la fonction et créera l'espace de noms si nécessaire. Rappelez-vous que les paquetages Perl sont ouverts à la modification à tout moment, même pendant l'exécution de votre programme. Si vous déclarez plusieurs fonctions du même nom dans le même espace de noms, Perl émettra un avertissement.

Vous pouvez vous référer à d'autres fonctions au sein d'un espace de noms par leurs noms courts. Utilisez un nom complet pour appeler une fonction d'un autre espace de noms :

 
Sélectionnez
package main;

Extensions::Math::add( $scalar, $vector );

N'oubliez pas, les fonctions sont visibles à l'extérieur de leur propre espace de noms par leur nom complet. Vous pouvez également importer des noms à partir d'autres espaces de noms.

Fonctions lexicales

Perl 5.18 a ajouté une fonctionnalité expérimentale de déclaration lexicale des fonctions. Après déclaration, elles sont visibles seulement à l'intérieur de la portée lexicale dans laquelle elles sont déclarées. Voir la section « Lexical Subroutines » de perldoc perlsub pour plus de détails.

VI-D-1. Importation

Lors du chargement d'un module avec la commande interne use (ModulesModules), Perl appelle automatiquement une méthode nommée import(). Les modules peuvent fournir leur propre méthode import(), ce qui rend disponibles dans le paquetage appelant tous les symboles définis ou une partie de ceux-ci. Les arguments qui suivent le nom du module dans l'instruction use sont transmis à la méthode import() du module. Ainsi :

 
Sélectionnez
use strict;

... charge le module strict.pm et appelle strict->import() sans arguments, tandis que :

 
Sélectionnez
use strict 'refs';
use strict qw( subs vars );

... charge le module strict.pm, appelle strict->import( 'refs' ), puis appelle strict->import('subs', vars ').

use a un comportement particulier en ce qui concerne import(), mais vous pouvez appeler import() directement. L'exemple use ci-dessus est équivalent à :

 
Sélectionnez
BEGIN
{
    require strict;
    strict->import( 'refs' );
    strict->import( qw( subs vars ) );
}

La commande interne use ajoute un bloc implicite BEGIN autour de ces déclarations afin que l'appel de import() ait lieu immédiatement après que l'analyseur a compilé toute l'instruction use. Cela garantit que l'analyseur connaît tous les symboles importés par strict avant de compiler le reste du programme. Dans le cas contraire, toute fonction importée d'un autre module, mais non déclarée dans le fichier courant ressemblerait à un « mot nu » et enfreindrait strict, par exemple.

Bien sûr, strict est un pragma (PragmasPragmas), alors il a d'autres effets.

VI-E. Rapports d'erreurs

Presque chaque fonction a un appelant. Utilisez la commande interne caller pour inspecter le contexte d'appel d'une fonction. Utilisée sans aucun argument, caller retourne une liste contenant le nom du paquetage appelant, le nom du fichier contenant l'appel et le numéro de la ligne du fichier sur laquelle l'appel a eu lieu :

 
Sélectionnez
package main;

main();

sub main
{
    montrer_information_appelant();
}

sub montrer_information_appelant
{
    my ($package, $file, $ligne) = caller();
    say "Appelée à partir de $package à $file:$ligne";
}

La chaîne complète des appels est disponible pour inspection. Passez un argument entier unique n à caller() pour remonter à l'appelant de l'appelant de l'appelant de rang n. Autrement dit, si montrer_information_appelant() utilise caller(0), elle recevrait des informations sur l'appel depuis la méthode main(). Si elle utilise caller(1), elle recevrait des informations sur l'appel du début du programme.

Cet argument facultatif indique également à caller de fournir des valeurs de retour supplémentaires, y compris le nom de la fonction et le contexte de l'appel :

 
Sélectionnez
sub montrer_information_appelant
{
    my ($package, $file, $ligne, $fonc) = caller(0);
    say "$fonc appelée à partir de $package à $file:$ligne";
}

Le module standard Carp utilise caller pour signaler des erreurs et lancer des avertissements dans les fonctions. Lorsqu'il est utilisé à la place de die dans le code de la bibliothèque, croak() lève une exception du point de vue de l'appelant. carp() signale un avertissement à partir du fichier et le numéro de la ligne de l'appelant (Produire des avertissementsProduire des avertissements).

Utilisez caller (ou Carp) lors de la validation des paramètres ou des conditions préalables d'une fonction pour indiquer que ce qui a appelé la fonction l'a mal fait.

VI-E-1. Valider les arguments

Tout en faisant de son mieux pour faire ce que vous lui demandez, Perl offre quelques façons natives de tester la validité des arguments fournis à une fonction. Évaluez @_ en contexte scalaire pour vérifier si le nombre de paramètres passés à une fonction est correct :

 
Sélectionnez
sub ajouter_nombres
{
    croak 'Attendait deux nombres, a reçu : ' . @_
        unless @_ == 2;

    ...
}

Cette validation signale toute erreur concernant le nombre de paramètres du point de vue de l'appelant, grâce à l'utilisation de croak.

La vérification du type est plus difficile, en raison des conversions de type effectuées par les opérateurs de Perl (ContexteContexte). Si vous voulez une sécurité supplémentaire des paramètres de la fonction, regardez les modules CPAN tels que Params::Validate ou MooseX::Method::Signatures.

VI-F. Fonctions avancées

Les fonctions sont la base de nombreuses fonctionnalités Perl avancées.

VI-F-1. Sensibilisation au contexte

Les fonctions internes de Perl savent si vous les avez invoquées dans un contexte vide, scalaire ou de liste. Vos fonctions peuvent aussi le savoir. La fonction interne malnommée wantarray (voir perldoc -f wantarray) retourne undef pour signaler le contexte vide, une valeur fausse pour signaler un contexte scalaire et une valeur vraie pour signaler un contexte de liste.

 
Sélectionnez
sub sensible_au_contexte
{
    my $contexte = wantarray();

    return qw( Contexte de liste ) if         $contexte;
    say    'Contexte vide'     unless defined $contexte;
    return 'Contexte scalaire' unless         $contexte;
}

sensible_au_contexte();
say my $scalaire = sensible_au_contexte();
say sensible_au_contexte();

Cela peut être utile pour éviter que des fonctions qui pourraient produire des valeurs de retour coûteuses le fassent dans un contexte vide. Certaines fonctions idiomatiques retournent une liste dans un contexte de liste et le premier élément de la liste ou une référence de tableau dans un contexte scalaire. Rappelez-vous, cependant, qu'il n'existe pas d'unique meilleure recommandation pour l'utilisation de wantarray. Parfois, c'est plus clair d'écrire des fonctions distinctes et sans ambiguïté, comme trouver_garnitures_toutes() et trouver_garniture_suivante().

Mise en contexte

Les modules CPAN Want de Robin Houston et Contextual::Return de Damian Conway offrent de nombreuses possibilités pour écrire des interfaces puissantes sensibles au contexte.

VI-F-2. Récursivité

Supposons que vous voulez trouver un élément dans un tableau trié. Vous pouvez itérer sur chaque élément du tableau individuellement à la recherche de la cible, mais vous aurez à examiner en moyenne la moitié des éléments du tableau. Une autre approche consiste à diviser le tableau en deux, choisir l'élément du milieu, comparer, puis répéter l'opération avec la moitié inférieure ou supérieure selon le résultat de la comparaison. Diviser pour régner. Lorsque vous épuisez les éléments à inspecter ou vous trouvez l'élément, arrêter.

Un test automatisé pour cette technique pourrait être :

 
Sélectionnez
use Test::More;

my @elements =
(
    1, 5, 6, 19, 48, 77, 997, 1025, 7777, 8192, 9999
);

ok   elem_existe(     1, @elements ),
        'trouvé le premier élément du tableau';
ok   elem_existe(  9999, @elements ),
        'trouvé le dernier élément du tableau';
ok ! elem_existe(   998, @elements ),
       'pas trouvé, élément absent du tableau';
ok ! elem_existe(    -1, @elements ),
       'pas trouvé, élément absent du tableau';
ok ! elem_existe( 10000, @elements ),
       'pas trouvé, élément absent du tableau';

ok   elem_existe(    77, @elements ),
       'trouvé élément du milieu du tableau';
ok   elem_existe(    48, @elements ),
       'trouvé la fin de la moitié inférieure du tableau';
ok   elem_existe(   997, @elements ),
        'trouvé le début de la moitié supérieure du tableau';

done_testing();

La récursivité est un concept d'une simplicité trompeuse. Chaque appel à une fonction en Perl crée un nouveau contexte d'appel (call frame), une structure de données interne à Perl qui représente le fait d'avoir appelé une fonction. Il contient l'environnement lexical de l'invocation actuelle — les valeurs de toutes les variables lexicales au sein de la fonction invoquée. Le stockage des valeurs des variables lexicales étant séparé de la fonction elle-même, vous pouvez avoir plusieurs appels actifs à une fonction en même temps. Une fonction peut même s'appeler elle-même, récursivement.

Pour passer le test précédent, écrivons la fonction récursive elem_existe() ainsi :

 
Sélectionnez
sub elem_existe
{
    my ($item, @array) = @_;

    # arrêter la recherche récursive si pas d'élément à rechercher
    return unless @array;

    # descendre d'un demi-cran en cas d'un nombre pair d'éléments
    my $milieu = int( (@array / 2) - 0.5 );
    my $element_milieu = $array[ $milieu ];

    # retourner vrai si l'élément a été trouvé
    return 1 if $item  == $element_milieu;

    # retourner faux si un seul élément dans le tableau
    return   if @array == 1;

    # recherche récursive dans la moitié inférieure du tableau
    return elem_existe(
        $item, @array[0 .. $milieu]
    ) if $item < $element_milieu;

    # recherche récursive dans la moitié supérieure du tableau
    return elem_existe(
         $item, @array[ $milieu + 1 .. $#array ]
    );
}

Gardez à l'esprit que les arguments de la fonction seront différents pour chaque appel, sinon la fonction se comporterait toujours de la même façon (elle continuera la récursion jusqu'à ce que le programme plante). C'est pour cela que la condition d'arrêt est si importante.

Tout programme récursif peut être écrit sans récursivité (voir le livre Higher Order Perl à http://hop.perl.plover.com/), mais cette approche consistant à diviser pour régner est un moyen efficace de gérer de nombreux types de problèmes similaires.

VI-F-3. Variables lexicales

Comme impliqué par la récursivité, chaque appel d'une fonction crée sa propre instance de portée lexicale représentée en interne par un contexte d'appel. Même si la déclaration de elem_existe() crée une seule portée pour les lexicales $item, @array, $midpoint et $miditem, chaque appel à elem_existe() — même récursif — stocke les valeurs de ces variables lexicales séparément.

Non seulement elem_existe() peut s’appeler elle-même, mais les variables lexicales de chaque appel sont protégées et séparées :

 
Sélectionnez
use Carp 'cluck';

sub elem_existe
{
    my ($item, @array) = @_;

    cluck "[$item] (@array)";
    ...
}

VI-F-4. Appels de fonctions et récursion terminale

Un inconvénient de la récursivité est que vous devez gérer correctement les valeurs de retour, faute de quoi votre fonction risque de s’appeler indéfiniment. Pour cette raison, la fonction elem_existe() a plusieurs instructions return. Perl offre un avertissement utile Deep recursion on subroutine lorsqu'il soupçonne une récursivité incontrôlée. La limite fixée à 100 appels récursifs est arbitraire, mais souvent utile. Vous pouvez désactiver cet avertissement avec no warnings 'recursion'.

Comme chaque appel à une fonction nécessite un nouveau contexte d'appel et de l'espace de stockage lexical, le code hautement récursif peut utiliser plus de mémoire que le code itératif. L'élimination des récursions terminales peut aider.

Une récursion terminale est un appel à une fonction qui retourne directement les résultats de cette fonction. Ces appels récursifs à elem_existe() :

 
Sélectionnez
# recherche récursive dans la moitié inférieure du tableau
return elem_existe(
    $item, @array[0 .. $milieu]
) if $item < $element_milieu;

# recherche récursive dans la moitié supérieure du tableau
return elem_existe(
     $item, @array[ $milieu + 1 .. $#array ]
);

... sont candidats à l'élimination de la récursion terminale. Cette optimisation éviterait de retourner à l'appel courant, puis à l'appel parent et ainsi de suite. Au lieu de cela, elle retourne directement à l'appel parent d'origine.

Perl n'élimine pas automatiquement les récursions terminales, mais vous pouvez obtenir le même effet en utilisant une forme particulière de la fonction goto. Contrairement à la forme qui produit souvent du code nommé spaghetti (parce que le flot d'exécution est aussi simple et direct qu'un plat de spaghettis), cette forme de la fonction goto remplace l'appel de fonction en cours par l'appel à une autre fonction. Vous pouvez utiliser une fonction par nom ou par référence. Vous pouvez même modifier les arguments passés à la fonction de remplacement en modifiant @_ :

 
Sélectionnez
# recherche récursive dans la moitié inférieure du tableau
if ($item < $element_milieu)
{
    @_ = ($item, @array[0 .. $milieu]);
    goto &elem_existe;
}

# recherche récursive dans la moitié supérieure du tableau
else
{
    @_ = ($item, @array[$milieu + 1 .. $#array] );
    goto &elem_existe;
}

Les optimisations sont parfois laides, mais si l'autre option est un code hautement récursif qui épuise la mémoire, choisissez la laideur et profitez de son côté pratique.

VI-G. Pièges et limitations

Perl prend toujours en charge l'ancien style d'appel de fonction des versions anciennes de Perl. Les versions précédentes de Perl nécessitaient une esperluette (&) devant l'appel de fonction. Perl 1 vous demandait même d'utiliser la commande interne do :

 
Sélectionnez
# style obsolète (Perl 4); évitez
my $resultat = &calculer_resultat( 52 );

# style Perl 1; évitez
my $resultat = do calculer_resultat( 42 );

# méli-mélo fou; à éviter à tout prix
my $resultat = do &calculer_resultat( 42 );

Non seulement cette syntaxe obsolète est visuellement encombrante, mais l'esperluette en début entraîne aussi des comportements surprenants. Tout d'abord, elle désactive toute vérification de prototype. Deuxièmement, elle passe implicitement le contenu de @_ non modifié, sauf si vous avez vous-même passé explicitement les arguments. Oui, c'est une action invisible à distance.

Un dernier piège vient de l'absence des parenthèses de l'appel de fonction. L'analyseur Perl utilise plusieurs heuristiques pour résoudre les « mots nus » ambigus et le nombre de paramètres passés à une fonction. Les heuristiques peuvent être erronées :

 
Sélectionnez
# avertissement; contient un bogue subtil
ok elem_existe 1, @elements, 'trouvé le premier élément';

L'appel à elem_existe() va engloutir la description du test conçu comme deuxième argument de ok(). Parce que elem_existe() utilise un deuxième paramètre gourmand (un tableau), cela peut passer inaperçu jusqu'au moment où Perl produit des avertissements sur la comparaison d'un non-nombre (la description du test, qu'il ne peut pas convertir en nombre) avec l'élément du tableau.

Il est vrai que des parenthèses superflues peuvent nuire à la lisibilité, mais une utilisation réfléchie des parenthèses peut clarifier le code et rendre improbables les bogues subtils.

VI-H. Portée

En Perl, la portée se réfère à la durée de vie et la visibilité des entités nommées. Tout ce qui porte un nom en Perl (une variable, une fonction, un descripteur de fichier, une classe) a une portée. La portée aide à appliquer l'encapsulation — maintenir ensemble des concepts connexes et empêcher la fuite de leurs détails.

VI-H-1. Portée lexicale

La portée lexicale est la portée visible pour vous quand vous lisez un programme. Un bloc délimité par des accolades — que ce soit un bloc nu, le bloc d'une boucle ou d'une déclaration sub, un bloc eval, un bloc package ou tout bloc autre que ceux associés aux opérateurs de citation — crée une nouvelle portée. Le compilateur Perl résout cette portée pendant la compilation.

La portée lexicale décrit la visibilité des variables déclarées avec my — les variables lexicales. Une variable lexicale déclarée à l'intérieur d'une portée est visible dans cette portée et toutes les portées imbriquées intérieures, mais est invisible dans les portées sœurs ou dans les portées extérieures :

 
Sélectionnez
# portée lexicale extérieure
{
    package Robot::Butler

    # portée lexicale intérieure
    my $niveau_batterie;

    sub chambre_propre
    {
        # une autre portée lexicale plus intérieure
        my $minuteur;

        do {
            # la portée lexicale la plus intérieure 
            my $ramassette;
            ...
        } while (@_);

        # la sœur de l'autre portée lexicale plus intérieure
        for (@_)
        {
            # une portée lexicale plus intérieure séparée 
            my $chiffon;
            ...
        }
    }
}

... $niveau_batterie est visible dans les quatre portées. $minuteur est visible dans la méthode, le bloc do et la boucle for. $ramassette n'est visible que dans le bloc do et $chiffon uniquement dans la boucle for.

La déclaration dans une portée interne d'une variable lexicale du même nom que celui d'une variable lexicale déclarée dans une portée externe cache, ou occulte, la variable lexicale externe dans la portée interne.

Par exemple :

 
Sélectionnez
my $nom = 'Jacob';

{
    my $nom = 'Edward';
    say $nom;
}

say $nom;

Dans du code plus long avec des portées plus étendues, ce comportement d'occultation est souvent souhaitable — il est plus facile de comprendre le code lorsque la portée d'une variable lexicale n'est pas plus étendue que quelques dizaines de lignes.

Collisions de nom

L'occultation lexicale peut se produire accidentellement. Limitez la portée des variables et l'imbrication des portées afin de diminuer les risques.

Le petit exemple de programme d'occultation lexicale affiche Edward, puis Jacob parce que la variable lexicale dans la portée interne cache la variable lexicale dans la portée externe. L'occultation d'une variable lexicale est une caractéristique de l'encapsulation. La déclaration de plusieurs variables ayant le même nom et le même type dans la même portée lexicale produit un message d'avertissement.

Certaines déclarations lexicales ont des subtilités, par exemple une variable lexicale utilisée comme variable d'itération d'une boucle for. Sa déclaration se trouve en dehors du bloc, mais sa portée est à l'intérieur du bloc de la boucle :

 
Sélectionnez
my $chat = 'Brad';

for my $chat (qw( Jack Daisy Petunia Tuxedo Choco ))
{
    say "Le chat de l'itération est $chat";
}

say "Le chat statique est $chat";

Les fonctions — nommés et anonymes — fournissent une portée lexicale à leur corps. Cela permet les fermetures (FermeturesFermetures).

VI-H-2. La portée our

À l'intérieur d'une portée donnée, déclarez un alias pointant vers une variable de paquetage avec la fonction our. Comme my, our applique la portée lexicale de l'alias. Le nom complet est disponible partout, mais l'alias lexical n'est visible qu'à l'intérieur de sa portée.

our est surtout utile avec des variables globales de paquetage comme $VERSION et $AUTOLOAD. Vous obtenez un peu de détection typographique (la déclaration d'un paquetage global avec our satisfait la règle vars du pragma strict), mais vous avez toujours affaire à une variable globale.

VI-H-3. Portée dynamique

La portée dynamique ressemble à la portée lexicale en matière de règles de visibilité, mais au lieu de regarder vers l'extérieur dans les portées de compilation, la recherche va en arrière à travers tous les appels de fonction que vous avez faits pour atteindre le code actuel. La portée dynamique s'applique uniquement aux variables globales et variables globales de paquetage (parce que les variables lexicales ne sont pas visibles en dehors de leur portée) et est plus facile à comprendre avec un exemple. Alors qu'une variable globale de paquetage peut être visible à l'intérieur de toutes les portées, sa valeur peut changer en fonction de la localisation et de l'affectation :

 
Sélectionnez
our $portee;

sub interne
{
     say $portee;
}

sub main
{
    say $portee;
    local $portee = 'portée main()';
    milieu();
}

sub milieu
{
    say $portee;
    interne();
}

$portee = 'portée externe';
main();
say $portee;

Le programme commence par déclarer une variable our, $portee, ainsi que trois fonctions. Il se termine par l'initialisation de $portee et l'appel à main().

Dans main(), le programme affiche la valeur actuelle de $portee, portée externe, puis localise la variable. Cela modifie la visibilité du symbole dans la portée lexicale courante ainsi que dans toutes les fonctions appelées à partir de la portée lexicale courante ; c'est cet « ainsi que »qui distingue la portée dynamique de la portée statique. Donc, $portee contient portée main() à la fois dans le corps de milieu() et interne(). Après le retour de main(), lorsque le flot d'exécution atteint la fin de son bloc, Perl restaure la valeur d'origine de $portee qui avait été provisoirement localisée. La dernière instruction say affiche une fois de plus portée externe.

Perl utilise des règles de visibilité et mécanismes de stockage différents pour les variables de paquetage et les variables lexicales. Chaque portée qui contient des variables lexicales utilise une structure de données appelée carnet lexical ou lexpad pour stocker les valeurs des variables lexicales incluses. Chaque fois que le flot d'exécution entre dans l'une de ces portées, Perl crée un autre carnet lexical pour contenir les valeurs des variables lexicales pour cet appel en particulier. Cela fait que les fonctions travaillent correctement, en particulier lors des appels récursifs (RécursivitéRécursivité).

Chaque paquetage a une table de symboles unique qui contient les variables de paquetage ainsi que les fonctions nommées. L'importation (ImportationImportation) fonctionne par l'inspection et la manipulation de cette table de symboles. Il en va de même pour local. Vous pouvez localiser uniquement des variables globales et variables globales de paquetage, jamais des variables lexicales.

local est le plus souvent utile avec des variables magiques. Par exemple, $/, le séparateur d'enregistrements en entrée, définit la quantité de données qu'une opération readline lira à partir d'un descripteur de fichier. $!, la variable d'erreur système, contient des détails d'erreur pour le dernier appel système. $@, la variable Perl d'erreur de eval, contient les erreurs de l'opération eval la plus récente. $|, la variable d'écriture immédiate (autoflush) des tampons attachés au descripteur de fichier, détermine si Perl écrit immédiatement dans le descripteur de fichier sélectionné après chaque opération d'écriture.

La localisation de ceux-ci dans la portée la plus étroite possible limite l'effet de vos modifications. Cela peut éviter des comportements étranges ailleurs dans votre code.

VI-H-4. Portée statique

Perl 5.10 a ajouté une nouvelle portée pour prendre en charge la commande interne state. La portée d'état ressemble à la portée lexicale en matière de visibilité, mais ajoute une initialisation ainsi que la persistance de la valeur :

 
Sélectionnez
sub compteur
{
    state $total = 1;
    return $total++;
}

say compteur();
say compteur();
say compteur();

Lors du premier appel à compteur, Perl initialise $total. Lors des prochains appels, $total conserve sa valeur précédente. Ce programme imprime 1, 2 et 3. Remplacez state par my et le programme affichera 1, 1 et 1.

Vous pouvez utiliser une expression pour définir la valeur initiale d'une variable state :

 
Sélectionnez
sub  compteur
{
    state $total = shift;
    return $total++;
}

say compteur(2);
say compteur(4);
say compteur(6);

Même si une simple lecture du code peut suggérer que la sortie devrait être 2, 4 et 6, elle est en fait 2, 3 et 4. Le premier appel de compteur initialise la variable $total. Les appels suivants ne changeront pas sa valeur.

state peut être utile pour définir une valeur par défaut ou préparer un cache, mais assurez-vous de comprendre son comportement d'initialisation si vous l'utilisez :

 
Sélectionnez
sub compteur
{
    state $total = shift;
    say 'Second arg est : ', shift;
    return $total++;
}

say counter(2, 'deux'  );
say counter(4, 'quatre');
say counter(6, 'six'   );

Le compteur de ce programme imprime 2, 3, et 4 comme prévu, mais les valeurs des seconds arguments destinés à compteur() sont deux, 4 et 6 — parce que le shift du premier argument ne se produit que dans le premier appel de compteur(). Soit changez l'interface pour éviter cette erreur, soit écrivez ceci :

 
Sélectionnez
sub compteur
{
    my ($valeur_initiale, $texte) = @_;

    state $total = $valeur_initiale;
    say "Second arg est : $texte";
    return $total++;
}

say compteur(2, 'deux'  );
say compteur(4, 'quatre');
say compteur(6, 'six'   );

VI-I. Fonctions anonymes

Une fonction anonyme est une fonction sans nom. Elle se comporte exactement comme une fonction nommée : vous pouvez l'invoquer, lui passer des arguments, recevoir ses valeurs de retour et copier des références vers elle. Pourtant, la seule façon de l'utiliser c'est par référence (Références de fonctionsRéférences de fonctions), pas par nom.

Un idiome commun de Perl connu sous le nom de table de distribution (dispatch table) utilise des hachages pour associer des données en entrée à des comportements :

 
Sélectionnez
my %calculer =
(
    plus     => \&additionner_deux_nombres,
    moins    => \&soustraire_deux_nombres,
    fois     => \&multiplier_deux_nombres,
);

sub additionner_deux_nombres { $_[0] + $_[1] }
sub soustraire_deux_nombres  { $_[0] - $_[1] }
sub multiplier_deux_nombres  { $_[0] * $_[1] }

sub calculer
{
    my ($gauche, $op, $droit) = @_;

    return unless exists $calculer{ $op };

    return $calculer{ $op }->( $gauche, $droit );
}

La fonction calculer() prend des arguments de la forme (2, 'fois', 2) et renvoie le résultat de l'évaluation de l'opération. Si vous écrivez une application simple de calculatrice, vous pouvez utiliser calculer pour déterminer le calcul à accomplir sur base d'un nom fourni par un utilisateur.

VI-I-1. Déclarer des fonctions anonymes

L'utilisation de l'instruction sub sans aucun nom crée et retourne une fonction anonyme. Utilisez cette référence de fonction à n'importe quel endroit où vous utiliseriez une référence vers une fonction nommée, comme pour déclarer les fonctions directement dans une table de distribution :

 
Sélectionnez
my %calculer =
(
    plus      => sub { $_[0]  + $_[1] },
    moins     => sub { $_[0]  - $_[1] },
    fois      => sub { $_[0]  * $_[1] },
    diviser   => sub { $_[0]  / $_[1] },
    puissance => sub { $_[0] ** $_[1] },
);

Distribution défensive

Seules les fonctions présentes dans cette table de distribution peuvent être appelées par les utilisateurs. Si votre fonction calculer utilise une chaîne fournie par l'utilisateur comme nom littéral d'une fonction, un utilisateur malveillant pourrait appeler n'importe quelle fonction n'importe où en passant 'Internal::Functions::malicious_function'.

Vous pouvez voir également des fonctions anonymes passées comme arguments de fonction :

 
Sélectionnez
sub invoquer_fonction_anonyme
{
    my $func = shift;
    return $func->( @_ );
}

sub fonction_nommee
{
    say 'Je suis une fonction nommée !';
}

invoquer_fonction_anonyme( \&fonction_nommee );
invoquer_fonction_anonyme( sub { say 'qui suis-je ?' } );

VI-I-2. Noms de fonctions anonymes

Utilisez l'introspection (voir aussi la fonction sub_name du module CPAN Sub::Identify) pour déterminer si une fonction est nommée ou anonyme :

 
Sélectionnez
package ShowCaller;

sub afficher_appelant
{
    my ($paquetage, $fichier, $ligne, $fonction) = caller(1);
    say "Appelée par $fonction à $paquetage:$fichier:$ligne";
}

sub main
{
    my $sub_anonyme = sub { afficher_appelant() };
    afficher_appelant();
    $sub_anonyme->();
}

main();

Le résultat peut être surprenant :

Appelée par ShowCaller::main

à ShowCaller:anoncaller.pl:20

Appelée par ShowCaller::__ANON__

à ShowCaller:anoncaller.pl:17

La __ANON__ sur la deuxième ligne de la sortie montre que la fonction anonyme n'a aucun nom que Perl puisse identifier. Cela peut compliquer le débogage. La fonction subname() du module CPAN Sub::Name vous permet d'attacher des noms aux fonctions anonymes :

 
Sélectionnez
use Sub::Name;
use Sub::Identify 'sub_nom';

my $anonyme  = sub {};
say sub_nom( $anonyme );

my $nommee = subname( 'pseudo-anonyme', $anonyme );
say sub_nom( $nommee  );
say sub_nom( $anonyme );

say sub_nom( sub {}   );

Ce programme affiche en sortie :

__ANON__

pseudo-anonyme

pseudo-anonyme

__ANON__

Sachez que les deux références désignent la même fonction anonyme sous-jacente. L'utilisation de subname() sur une référence de fonction modifiera le nom de cette fonction anonyme, de sorte que toutes les autres références vers elle verront le nouveau nom.

VI-I-3. Fonctions anonymes implicites

Perl vous permet de déclarer des fonctions anonymes comme arguments d'une fonction sans utiliser le mot-clé sub. Bien que cette fonctionnalité existe nominalement pour permettre aux programmeurs d'écrire leur propre syntaxe comme celle pour map et eval (PrototypesPrototypes), un exemple intéressant est l'utilisation des fonctions qui ne ressemblent pas à des fonctions. Ce n'est pas parfait, mais peut rendre le code plus facile à lire.

Examinons le module Test::Fatal du CPAN, qui prend une fonction anonyme comme premier argument de sa fonction exception() :

 
Sélectionnez
use Test::More;
use Test::Fatal;

my $croaker = exception { die 'I croak!' };
my $liver   = exception { 1 + 1 };

like( $croaker, qr/I croak/, 'die() should croak'   );
is(   $liver,   undef,       'addition should live' );

done_testing();

Vous pourriez réécrire cela de façon plus explicite :

 
Sélectionnez
my $croaker = exception( sub { die 'I croak!' } );
my $liver   = exception( sub { 1 + 1 } );

... ou passer par référence des fonctions nommées :

 
Sélectionnez
sub croaker { die 'I croak!' }
sub liver   { 1 + 1 }

my $croaker = exception \&croaker;
my $liver   = exception \&liver;

like( $croaker, qr/I croak/, 'die() should die'     );
is(   $liver,   undef,       'addition should live' );

... mais vous ne pouvez pas les passer comme des références scalaires :

 
Sélectionnez
my $croak_ref = \&croaker;
my $live_ref  = \&liver;

# BOGUE: ne fonctionne pas
my $croaker   = exception $croak_ref;
my $liver     = exception $live_ref;

... parce que le prototype change la façon dont l'analyseur Perl interprète ce code. Il ne peut pas déterminer avec 100 % de clarté que contiendront $croaker et $liver et lèvera une exception.

Type of arg 1 to Test::Fatal::exception must be block or sub {} (not private variable)

Sachez également qu'une fonction qui accepte une fonction anonyme comme le premier de plusieurs arguments ne peut pas avoir une virgule de fin après le bloc de fonction :

 
Sélectionnez
use Test::More;
use Test::Fatal 'dies_ok';

dies_ok { die 'This is my boomstick!' }
        'No movie references here';

C'est un inconvénient parfois déroutant de la syntaxe autrement utile, à cause d'une bizarrerie de l'analyseur Perl. La clarté syntaxique disponible en favorisant des blocs nus dans les fonctions anonymes peut être utile, mais utilisez-la avec parcimonie et documentez l'API avec soin.

VI-J. Fermetures

Chaque fois que le flot d'exécution entre dans une fonction, cette fonction reçoit un nouvel environnement représentant la portée lexicale de cet appel (PortéePortée ). Cela s'applique aussi bien aux fonctions anonymes (Fonctions anonymesFonctions anonymes). L'implication est puissante. Le terme informatique fonctions d'ordre supérieur se réfère aux fonctions qui manipulent d'autres fonctions. Les fermetures mettent en valeur ce pouvoir.

VI-J-1. Création de fermetures

Une fermeture est une fonction qui utilise les variables lexicales d'une portée externe. Vous avez probablement déjà créé et utilisé des fermetures sans vous en rendre compte :

 
Sélectionnez
use Modern::Perl '2014';

my $nom_fichier = shift @ARGV;

sub get_nom_fichier { return $nom_fichier }

Si ce code vous semble simple, tant mieux ! Bien sûr, la fonction get_nom_fichier() peut voir la variable lexicale $nom_fichier. C'est ainsi que la portée fonctionne !

Supposons que vous vouliez parcourir une liste d'éléments sans avoir à gérer vous-même la variable d'itération. Vous pouvez créer une fonction qui retourne une fonction qui, une fois invoquée, retournera l'élément suivant dans l'itération :

 
Sélectionnez
sub creer_iterateur
{
    my @elements = @_;
    my $compteur = 0;

    return sub
    {
        return if $compteur == @elements;
        return $elements[ $compteur++ ];
    }
}

my $cousins = creer_iterateur(qw(
    Rick Alex Kay Eric Corey Mandy Christine Alex
));

say $cousins->() for 1 .. 6;

Même si le retour de creer_iterateur() a eu lieu, la fonction anonyme stockée dans $cousins « s'est fermée » sur les valeurs de ces variables telles qu'elles existaient au sein de l'invocation de creer_iterateur(). Leurs valeurs persistent (Comptage des référencesComptage des références).

Parce que l'appel de creer_iterateur() crée un environnement lexical distinct, la fonction anonyme qu'il crée et retourne se referme un environnement lexical unique pour chaque appel :

 
Sélectionnez
my $tantes = creer_iterateur(qw(
    Carole Phyllis Wendy Sylvia Monica Lupe
));

say $cousins->();
say $tantes->();

Comme creer_iterateur() ne retourne pas ces variables lexicales par valeur ou par référence, aucun code Perl autre que la fermeture ne peut y accéder. Elles sont encapsulées aussi efficacement que n'importe quelle autre lexicale, bien que n'importe quel code qui partage un environnement lexical puisse accéder à ces valeurs. Cet idiome offre une meilleure encapsulation de ce qui serait par ailleurs une variable globale à un fichier ou à un paquetage :

 
Sélectionnez
{
    my $variable_privee;

    sub set_privee { $variable_privee = shift }
    sub get_privee { $variable_privee }
}

Sachez que vous ne pouvez pas imbriquer des fonctions nommées. Les fonctions nommées ont une portée globale de paquetage. Toutes les variables lexicales partagées entre les fonctions imbriquées ne le seront plus lorsque la fonction externe détruit son premier environnement lexical. Si cela vous semble confus, imaginez la mise en œuvre.

Intrusion dans la vie privée

Le module PadWalker du CPAN vous permet de violer l'encapsulation lexicale, mais toute personne qui l'utilise doit corriger les bogues qui en résultent.

VI-J-2. Utilisations des fermetures

L'itération sur une liste de taille fixe avec une fermeture est intéressante, mais les fermetures peuvent faire beaucoup plus, comme itérer sur une liste qui est trop coûteuse à calculer ou trop grande pour la garder intégralement en mémoire. Considérons une fonction pour créer des suites de Fibonacci au fur et à mesure que vous avez besoin de ses éléments. Pour quoi faire ? Pour vérifier vos devoirs en langage de programmation Haskell. Au lieu de recalculer la série de manière récursive, utilisez un cache et créez paresseusement les éléments dont vous avez besoin :

 
Sélectionnez
sub gen_fib
{
    my @fibs = (0, 1);
    return sub
    {
        my $element = shift;
        if ($element >= @fibs)
        {
            for my $calc (@fibs .. $element)
            {
                $fibs[$calc] = $fibs[$calc - 2]
                             + $fibs[$calc - 1];
            }
        }
        return $fibs[$element];
    }
}

# calculer le quarante-deuxième nombre Fibonacci 
my $fib = gen_fib();
say $fib->( 42 );

Chaque appel à la fonction retournée par gen_fib() prend un seul argument, le nième élément de la série de Fibonacci. La fonction génère et met en cache toutes les valeurs précédentes dans la série comme nécessaire, et renvoie l'élément demandé.

Voici où les fermetures et les fonctions de première classe deviennent intéressantes. Ce code fait deux choses ; il existe un modèle spécifique de mise en cache étroitement lié à la série numérique. Qu'advient-il si vous extrayez le code de cache spécifique (initialiser un cache, exécuter le code personnalisé pour charger en cache les éléments et retourner la valeur calculée ou mise en cache) à une fonction gen_caching_closure() ?

 
Sélectionnez
sub gen_caching_closure
{
    my ($calc_element, @cache) = @_;
    return sub
    {
        my $element = shift;
        $calc_element->($element, \@cache)
            unless $element < @cache;
        return $cache[$element];
    };
}

sub gen_fib
{
    my @fibs = (0, 1, 1);
    return gen_caching_closure( sub
        {
            my ($element, $fibs) = @_;
            for my $calc ((@$fibs - 1) .. $element)
            {
                $fibs->[$calc] = $fibs->[$calc - 2]
                               + $fibs->[$calc - 1];
            }
        }, @fibs
    );
}

Le programme se comporte comme précédemment, mais maintenant les références de fonction et les fermetures séparent le comportement d'initialisation de cache du calcul du prochain nombre de la suite de Fibonacci. La personnalisation du comportement du code — dans ce cas, gen_caching_closure() — en passant en argument une fonction permet une grande flexibilité et peut nettoyer votre code.

Réduire, appliquer et filtrer

Les commandes internes map, grep et sort sont elles-mêmes des fonctions d'ordre supérieur.

VI-J-3. Fermetures et application partielle

Les fermetures peuvent également supprimer la généricité non désirée. Prenons le cas d'une fonction qui accepte plusieurs paramètres :

 
Sélectionnez
sub preparer_coupe_glace
{
    my %args         = @_;

    my $creme_glacee = get_creme_glacee( $args{creme_glacee} );
    my $banane       = get_banane(       $args{banane}       );
    my $sirop        = get_sirop(        $args{sirop}        );
    ...
}

Les multiples possibilités de personnalisation pourraient fonctionner très bien dans une glacerie, mais pour un triporteur pour vente ambulante de glaces où vous servez seulement de la glace française à la vanille sur banane Cavendish, chaque appel à preparer_coupe_glace() passe toujours les mêmes arguments.

Une application partielle vous permet de lier maintenant certains des arguments à une fonction afin que vous puissiez fournir les autres plus tard. Enveloppez dans une fermeture la fonction que vous souhaitez appeler et passez les arguments liés. Pour votre triporteur de glaces :

 
Sélectionnez
my $preparer_coupe_triporteur = sub
{
    return preparer_coupe_glace( @_,
        creme_glacee => 'Vanille française',
        banane       => 'Cavendish',
    );
};

Maintenant, chaque fois que vous traitez une commande, invoquez la référence de fonction $preparer_coupe_triporteur et passez uniquement les arguments intéressants.(6) Vous n'allez jamais oublier ou transmettre de manière incorrecte les invariants. Vous pouvez même utiliser Sub::Install de CPAN pour importer la fonction $preparer_coupe_triporteur dans un autre espace de noms.

Ce n'est que le début de ce que vous pouvez faire avec les fonctions d'ordre supérieur. Le livreHigher Order Perl de Mark Jason Dominus est la référence canonique sur les fonctions de première classe et les fermetures de Perl. Vous pouvez le lire gratuitement en ligne à http://hop.perl.plover.com/(7).

VI-K. Variables statiques ou fermetures

Les fermetures (FermeturesFermetures) utilisent la portée lexicale (PortéePortée) pour contrôler l'accès aux variables lexicales — même par des fonctions nommées :

 
Sélectionnez
{
    my $securite = 0;

    sub activer_securite    { $securite = 1 }
    sub desactiver_securite { $securite = 0 }

    sub entreprendre_action_geniale
    {
        return if $securite;
        ...
    }
}

Ces fonctions encapsulent toutes les trois cet état partagé sans exposer directement la variable lexicale au code externe. Cet idiome fonctionne bien pour les cas où des fonctions multiples accèdent à cette variable lexicale, mais c'est un peu maladroit quand il n'y a qu'une seule fonction qui le fait. Supposons que chaque centième client de la glacerie reçoive un saupoudrage de granulés de chocolat gratuit :

 
Sélectionnez
my $compteur_client = 0;

sub servir_client
{
    $compteur_client++;
    my $commande = shift;

    ajouter_granules($commande) if $compteur_client % 100 == 0;
    ...
}

Cette approche fonctionne, mais la création d'une nouvelle portée lexicale externe pour une seule fonction est un peu bruyante. Le déclarateur state vous permet de déclarer une variable de portée lexicale ayant une valeur qui persiste entre chaque appel :

 
Sélectionnez
sub servir_client
{
    state $compteur_client = 0;
    $compteur_client++;

    my $commande = shift;
    ajouter_granules($commande) if ($compteur_client % 100 == 0);

    ...
}

Vous devez activer explicitement cette fonctionnalité en utilisant un module tel que Modern::Perl, le pragma feature (PragmasPragmas), ou en exigeant les caractéristiques d'une version Perl égale ou supérieure à la 5.10 (use 5.010;, use 5.012; et ainsi de suite).

state fonctionne également dans les fonctions anonymes :

 
Sélectionnez
sub comptage
{
    return sub
    {
        state $compteur = 0;
        return $compteur++;
    }
}

... bien qu'il y ait peu d'avantages évidents de cette approche.

VI-L. Statique ou pseudostatique

Dans les anciennes versions de Perl, une fonction nommée pourrait se fermer sur sa portée lexicale précédente en abusant d'une bizarrerie de mise en œuvre. L'utilisation d'un modificateur d'instruction conditionnel postfixé ayant la valeur fausse avec une déclaration my évitait de réinitialiser une variable lexicale à undef ou à sa valeur d'initialisation.

Dans les versions modernes de Perl, toute utilisation d'un modificateur d'expression conditionnel postfixé qui modifie la déclaration d'une variable lexicale produit un avertissement d'obsolescence. Il est trop facile d'écrire par inadvertance du code bogué avec cette technique ; utilisez state là où c'est possible, ou une vraie fermeture sinon. Réécrivez cet idiome lorsque vous le rencontrez :

 
Sélectionnez
sub state_inattentif
{
    # my $compteur  = 1 if 0; # OBSOLETE; ne pas utiliser
    state $compteur = 1;      # préférable

    ...
}

Vous pouvez initialiser une variable statique uniquement avec une valeur scalaire. Si vous avez besoin de garder la trace d'une variable composite, utilisez une référence de hachage ou de tableau (RéférencesRéférences).

VI-M. Attributs

En Perl, les entités nommées — variables et fonctions — peuvent avoir des métadonnées supplémentaires. Ces métadonnées prennent la forme d'attributs, noms et valeurs arbitraires utilisés avec certains types de métaprogrammation (Génération de codeGénération de code).

La syntaxe de déclaration d'attributs est étrange et l'utilisation efficace des attributs est plus un art qu'une science. La plupart des programmes ne les utilisent jamais, mais lorsqu'ils sont bien utilisés, ils offrent des avantages de clarté et de maintenance.

Un attribut simple est un identifiant précédé par un caractère deux-points, attaché à une déclaration :

 
Sélectionnez
my $forteresse      :cachee;

sub eruption_volcan :ProjetScientifique { ... }

Lorsque Perl analyse ces déclarations, il appelle les gestionnaires d'attributs nommés cachee et ProjetScientifique, s'ils existent pour les types appropriés (scalaires et fonctions, respectivement). Ces gestionnaires peuvent tout faire. Si les gestionnaires appropriés n'existent pas, Perl lancera une exception à la compilation.

Les attributs peuvent inclure une liste de paramètres. Perl traite ces paramètres comme listes de chaînes constantes. Le module Test::Class de CPAN utilise tels arguments paramétriques à bon escient (voir le framework Web Catalyst pour un autre exemple, différent) :

 
Sélectionnez
sub setup_tests          :Test(setup)    { ... }
sub test_monkey_creation :Test(10)       { ... }
sub shutdown_tests       :Test(teardown) { ... }

L'attribut Test identifie des méthodes qui incluent des assertions de test et éventuellement identifie le nombre d'assertions que la méthode a l'intention d'exécuter. Alors que l'introspection (RéflexionRéflexion) de ces classes pourrait découvrir les méthodes de test appropriées, compte tenu des heuristiques solides bien conçues, l'attribut :Test est sans ambiguïté. Test::Class fournit des gestionnaires d'attributs qui gardent une trace de ces méthodes. Lorsque la classe a fini l'analyse, Test::Class peut effectuer une boucle sur la liste des méthodes de test et les exécuter.

Les paramètres setup et teardown permettent aux classes de test de définir leurs propres méthodes de mise en œuvre sans se soucier de conflits avec d'autres méthodes du même nom dans d'autres classes. Cela sépare l'idée de ce que cette classe doit faire de la façon dont les autres classes font leur travail. Sinon, une classe de test pourrait avoir une méthode nommée setup et une nommée teardown et devrait tout faire dans ces méthodes, ensuite appeler les méthodes parentes et ainsi de suite.

VI-M-1. Inconvénients des attributs

Les attributs ont leurs inconvénients. Le pragma canonique pour travailler avec des attributs (le module attributes) répertorie son interface comme expérimentale depuis de nombreuses années, et à juste titre. Le module standard Attribute::Handlers de Damian Conway est beaucoup plus facile à utiliser et Attribute::Lexical de Andrew Main est une approche plus récente. Préférez l'un des deux à attributes lorsque c'est possible.

Le pire aspect des attributs, c'est qu'ils le rendent facile à déformer la syntaxe de Perl de façon imprévisible. Il n'est pas toujours facile de prédire ce que fera le code contenant des attributs. Une bonne documentation aide, mais si une déclaration apparemment innocente d'une variable lexicale stocke quelque part une référence vers cette variable, vos attentes sur sa durée de vie peuvent être erronées. De même, un gestionnaire peut envelopper une fonction dans une autre fonction et la remplacer dans la table de symboles à votre insu — imaginez un attribut :memoize qui appelle automatiquement le module standard Memoize.

Les attributs peuvent vous aider à résoudre des problèmes difficiles ou rendre une API plus facile à utiliser. Lorsqu'ils sont utilisés correctement ils sont puissants, mais la plupart des programmes n'en ont jamais besoin.

VI-N. AUTOLOAD

Perl ne vous oblige pas à déclarer chaque fonction avant de l'appeler. Perl tentera volontiers d'appeler une fonction, même si elle n'existe pas. Examinons le programme :

 
Sélectionnez
use Modern::Perl;

cuire_tarte( fruit => 'pomme' );

Lorsque vous l'exécutez, Perl lancera une exception en raison de l'appel de la fonction non définie cuire_tarte().

Maintenant, ajoutez une fonction appelée AUTOLOAD() (« autochargement ») :

 
Sélectionnez
sub AUTOLOAD {}

À présent, lorsque vous exécutez le programme il ne se passera rien d'évident. Perl appellera une fonction nommée AUTOLOAD() d'un paquetage — si elle existe — chaque fois que la recherche normale d'une méthode dans l'arbre d'héritage échoue. Modifiez AUTOLOAD() pour afficher un message qui démontre qu'elle est appelée :

 
Sélectionnez
sub AUTOLOAD { say 'Dans AUTOLOAD() !' }

La fonction AUTOLOAD() reçoit les arguments passés à la fonction non définie dans @_ et le nom complet de la fonction non définie (ici, main::cuire_tarte) dans la variable globale de paquetage $AUTOLOAD :

 
Sélectionnez
sub AUTOLOAD
{
    our $AUTOLOAD;

    # afficher les arguments
    local $" = ', ';
    say "Dans AUTOLOAD(@_) for $AUTOLOAD !"
}

Extrayez le nom de la méthode avec une expression régulière (Expressions régulières etExpressions régulières et correspondancecorrespondanceExpressions régulières et correspondance) :

 
Sélectionnez
sub AUTOLOAD
{
    my ($nom) = our $AUTOLOAD =~ /::(\w+)$/;

    # afficher les arguments
    local $" = ', ';
    say "Dans AUTOLOAD(@_) for $nom !"
}

Jusqu'ici, ces exemples ont simplement intercepté les appels à des fonctions non définies. Vous avez d'autres options.

VI-N-1. Renvoyer des méthodes dans AUTOLOAD()

Un modèle commun dans la programmation OO (MooseMoose) est de déléguer ou d'envoyer par proxy certaines méthodes d'un objet à un autre objet en quelque sorte accessible au premier. Un proxy de journalisation peut faciliter le débogage :

 
Sélectionnez
package Proxy::Log;

# le constructeur lie une référence à un scalaire

sub AUTOLOAD
{
    my ($nom) = our $AUTOLOAD =~ /::(\w+)$/;
    Log::method_call( $nom, @_ );

    my $self = shift;
    return $$self->$nom( @_ );
}

Cette AUTOLOAD() extrait le nom de la méthode non définie. Ensuite, elle déréférence l'objet envoyé par proxy à partir d'une référence scalaire liée, enregistre l'appel de méthode, puis appelle cette méthode sur l'objet proxy avec les paramètres fournis.

VI-N-2. Générer du code dans AUTOLOAD()

Cette double recherche est facile à écrire, mais inefficace. Chaque appel de méthode sur le proxy doit d'abord échouer dans sa recherche de méthode dans l'arbre d'héritage avant de se retrouver enfin dans AUTOLOAD(). Payez cette pénalité juste une seule fois par l'installation de nouvelles méthodes dans la classe proxy lorsque le programme a besoin d'elles :

 
Sélectionnez
sub AUTOLOAD
{
    my ($nom)   = our $AUTOLOAD =~ /::(\w+)$/;
    my $methode = sub { ... };

    no strict 'refs';
    *{ $AUTOLOAD } = $methode;
    return $methode->( @_ );
}

Le corps de la fonction AUTOLOAD() précédente est devenu une fermeture (FermeturesFermetures) liée au nom de la méthode non définie. L'enregistrement de cette fermeture dans la table de symboles appropriée permet à toutes les recherches ultérieures de la méthode de trouver la fermeture créée (et d'éviter AUTOLOAD()). Finalement, ce code appelle directement la méthode et retourne le résultat.

Bien que cette approche soit plus propre et presque toujours plus transparente que la manipulation du comportement directement dans AUTOLOAD(), le code appelé par AUTOLOAD() peut voir AUTOLOAD() dans la liste de ses appelants en invoquant caller(). Si cela survient, non seulement ce serait une violation de l'encapsulation, la fuite des détails sur comment un objet fournit une méthode violerait également l'encapsulation.

Certains programmes utilisent des appels de fonction terminaux (TailcallsAppels de fonctions terminaux et récursion) pour remplacer l'invocation courante de AUTOLOAD() par un appel à la méthode de destination :

 
Sélectionnez
sub AUTOLOAD
{
   my ($nom)   = our $AUTOLOAD =~ /::(\w+)$/;
   my $methode = sub { ... }

   no strict 'refs';
   *{ $AUTOLOAD } = $methode;
   goto &$methode;
}

Cela a le même effet que l'appel direct de $methode, sauf que AUTOLOAD() n'apparaîtra plus dans la liste des appelants retournée par caller(), ce qui donne l'impression que la méthode générée a été appelée directement.

VI-N-3. Inconvénients de AUTOLOAD

AUTOLOAD() peut être utile, mais il est difficile de l'utiliser correctement. L'approche naïve à générer des méthodes à l'exécution signifie que la méthode can() ne donnera pas des informations correctes sur les capacités des objets et des classes. La solution la plus simple est de déclarer préalablement toutes les fonctions que vous prévoyez utiliser dans AUTOLOAD() avec le pragma subs :

 
Sélectionnez
use subs qw( rouge vert bleu ocre cyan );

Vous les voyez maintenant

Les déclarations anticipées ne sont utiles que dans les deux cas rares d'utilisation des attributs et d'utilisation d'AUTOLOAD (AUTOLOADAUTOLOAD).

Cette technique documente bien votre intention, mais vous oblige à maintenir une liste statique de fonctions ou méthodes. Surcharger la méthode can() (Le paquetage UNIVERSALLe paquetage UNIVERSAL) fonctionne mieux parfois :

 
Sélectionnez
sub can
{
    my ($self, $methode) = @_;

    # utiliser les résultats  de la méthode can() parente
    my $meth_ref = $self->SUPER::can( $methode  );
    return $meth_ref if $meth_ref;

    # ajouter un filtre ici
    return unless $self->doit_generer( $methode );

    $meth_ref = sub { ... };
    no strict 'refs';
    return *{ $methode } = $meth_ref;
}

sub AUTOLOAD
{
    my ($self) = @_;
    my ($nom)  = our $AUTOLOAD =~ /::(\w+)$/;>

    return unless my $meth_ref = $self->can( $nom );
    goto &$meth_ref;
}

AUTOLOAD() est un gros marteau manquant de délicatesse et peut attraper des fonctions et méthodes que vous n'aviez pas l'intention de charger automatiquement, comme DESTROY(), le destructeur d'objets. Si vous écrivez une méthode DESTROY() sans implémentation, Perl se fera un plaisir de pointer vers elle plutôt que vers AUTOLOAD() :

 
Sélectionnez
# éviter AUTOLOAD()
sub DESTROY {}

Une méthode très spéciale

Les méthodes spéciales import(), unimport() et VERSION() ne passent jamais par AUTOLOAD().

Si vous mélangez fonctions et méthodes dans un seul espace de noms qui hérite d'un autre paquetage qui fournit son propre AUTOLOAD(), vous risquez de voir cette erreur étrange :

Use of inherited AUTOLOAD for non-method slam_door() is deprecated

Si cela vous arrive, simplifiez votre code ; vous avez appelé une fonction qui n'existe pas dans un paquetage qui hérite d'une classe qui contient sa propre AUTOLOAD(). Le problème arrive de plusieurs manières : mélanger fonctions et méthodes dans un seul espace de noms est souvent un défaut de conception, l'héritage et AUTOLOAD() deviennent très rapidement complexes et raisonner sur le code est difficile lorsque vous ne savez pas quels objets les méthodes fournissent.

AUTOLOAD() est utile pour la programmation rapide et sale, mais un code robuste l'évite. (8)


précédentsommairesuivant
En programmation fonctionnelle, utiliser une application partielle pour réduire le nombre d'arguments passés à une fonction fréquemment invoquée s'appelle souvent « curryfier » la fonction (ce terme provient du nom du mathématicien américain Haskell Curry, dont le prénom a par ailleurs servi à baptiser un langage de programmation fonctionnelle). (NdT)
Mais attention, soyez prévenu, on peut certes lire ce livre gratuitement en ligne, mais il y a un sérieux risque que cela vous coûte en définitive de l'argent : le livre Higher Order Perl de Dominus est tellement intéressant que vous allez peut-être vous sentir obligé d'acquérir une version papier pour mieux l'explorer. (NdT)
Dans son livre De l'art de programmer en Perl (aux éditions O'Reilly, traduction française du livre Perl Best Practices), Damian Conway, l'un des gourous les plus respectés de la communauté Perl, est nettement plus péremptoire à l'encontre d'AUTOLOAD() : « N'utilisez pas AUTOLOAD() ». (NdT)

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Licence Creative Commons
Le contenu de cet article est rédigé par chromatic et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Pas de Modification 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2015 Developpez.com.