• Aucun résultat trouvé

Implanter en Perl 5 les fonctions gather et take de Perl

Perl 6, une nouvelle version du langage encore en cours de développement qui sera peut- être un jour la nouvelle version de Perl, s'inspire en partie des langages fonctionnels assez récents (comme Haskell) pour définir deux fonctions intimement liées, gather et take, permettant d'effectuer des opérations particulièrement riches sur des listes. Pour simplifier, la fonction gather parcourt la liste et la fonction take décrit ce qui doit être fait sur les éléments retenus de la liste. La fonction take n'a un sens que si elle est appelée dans le cadre d'un gather. Comme nous le verrons, ces deux fonctions associées définissent en quelque sorte un sur-ensemble conceptuel de map et de grep.

La fonction gather offre un moyen de construire une liste de façon procédurale, sans avoir besoin d'utiliser explicitement de variables temporaires. Dans le bloc ou la fermeture contrôlée par un gather, tout appel à take « pousse » (ajoute) la liste d'arguments sur un tableau défini implicitement. À la fin du processus, gather retourne la liste ainsi créée. Ce n'est pas très clair ? Pas très étonnant, c'est vraiment assez difficile à expliquer. Un exemple sera sans doute plus parlant.

6-1. Mise en œuvre

Voici une mise en œuvre possible de gather et take : gather et take use strict; use warnings; { my @result; sub gather(&) { my $code_ref = shift; @result = (); $code_ref->(); return @result; } sub take(@) { my @caller = caller 2;

die "Appel à take pas à l'intérieur d'un bloc gather" unless $caller[3] =~ /gather/;

push @result, @_; return scalar @result; }

}

Qu'avons-nous ? Nous avons d'abord un tableau lexical @result global aux deux fonctions gather et take (mais local au bloc de code englobant gather et take). La fonction gather reçoit en paramètre une coderef, initialise le tableau @result à vide,

appelle la coderef reçue en paramètre qui se charge de remplir le tableau (en appelant pour ce faire la fonction take) et renvoie le tableau.

La fonction take reçoit en paramètre un tableau, vérifie qu'elle a bien été appelée dans le contexte d'un gather (8) (on pourrait s'en passer dans une version minimaliste, mais mieux

vaut vérifier que l'utilisateur ne fait pas n'importe quoi), ajoute le tableau reçu en paramètre à @result, et renvoie accessoirement le nombre d'éléments du tableau @result. Pour notre explication du fonctionnement de gather et take, la fonction take pourrait se réduire à la définition minimaliste suivante :

take, version minimaliste sub take(@) {

push @result, @_; }

6-2. Exemples d'utilisation simple

Soit, direz-vous, mais la fonction take n'est même pas appelée par gather. Quel est le lien entre les deux ? En fait, si, la fonction take est (éventuellement) appelée dans la coderef reçue en paramètre et exécutée par gather.

Par exemple, nous pouvons appeler ces deux fonctions avec la simple ligne de code suivante pour simuler la fonction map et renvoyer le double de chacun des nombres d'une liste en entrée :

Simulation de map

print join " ", gather {take $_ * 2 for (1..10)}; Ce qui imprime bien :

$ perl gather_simple.pl 2 4 6 8 10 12 14 16 18 20

Nous pouvons de même simuler un grep et retourner par exemple les nombres impairs d'une liste en entrée:

Simulation de grep

print join " ", gather {$_ % 2 and take $_ for (1..10)}; # imprime 1 3 5 7 9

On le voit, notre paire de fonctions gather et take simule facilement aussi bien map que grep.

Nous pouvons même combiner les fonctionnalités de map et de grep, filtrer et transformer la liste en entrée. Si par exemple nous désirons obtenir le cube des nombres impairs de la liste en entrée :

simulation de map et grep

print join " ", gather {$_ % 2 and take $_ ** 3 for (1..10)}; # imprime 1 27 125 343 729

Nous pouvons maintenant utiliser gather et take pour construire des fonctions plus complexes.

6-3. Gather et take pour construire une fonction my_map

À titre d'exemple, considérons cette nouvelle implémentation de my_map utilisant gather et take :

my_map utilisant gather et take sub my_map (&@) {

my $coderef = shift; my @list = @_;

return gather {

take $coderef->($_) for @list; };

}

print join " ", my_map {$_ * 2} 1..10; print "\n";

Ce qui imprime bien le résultat attendu :

$ perl gather_map.pl 2 4 6 8 10 12 14 16 18 20

Comme dans la version précédente, la fonction my_map reçoit en paramètres une coderef et un tableau. Mais elle appelle cette fois la fonction gather et lui passe en argument une coderef appelant la fonction take sur la valeur de retour de la coderef reçue en paramètre appliquée à chaque élément du tableau. Reconnaissons-le franchement : nous n'avons pas vraiment simplifié les choses par rapport à notre précédente version de my_map, mais nous avons créé un nouvel outil plus flexible et plus riche fonctionnellement.

6-4. … Ou my_grep

Nous pouvons de même proposer une nouvelle version de my_grep utilisant gather et take :

my_grep utilisant gather et take sub my_grep (&@) {

my $coderef = shift; my @list = @_;

return gather {

$coderef->($_) and take $_ for @list; };

}

print join " ", my_grep {$_ % 2} 1..20; Ce qui imprime le résultat attendu :

$ perl gather_grep.pl 1 3 5 7 9 11 13 15 17 19

6-5. Combiner les fonctions map et grep ?

On souhaiterait parfois combiner les comportements de map et de grep, c'est-à-dire à la fois filtrer et transformer les données en une seule opération, comme nous l'avons fait avec le dernier exemple des utilisations simples de gather et take (§ 6.2). Voyons, à titre d'exercice de style, si nos fonctions génériques gather et take nous permettent de le faire en une seule opération. Essayons par exemple d'écrire un petit script qui affiche les carrés des nombres impairs d'une liste. Nous pouvons créer une fonction grep_and_map et faire ceci : grep_and_map sub grep_and_map { my $coderef_grep = shift; my $coderef_map = shift; my @list = @_; return gather {

$coderef_grep->($_) and take $coderef_map->($_) for @list; };

}

print join " ", grep_and_map ( sub {$_ % 2}, sub {$_ ** 2}, 0..20); print "\n";

# affiche: 1 9 25 49 81 121 169 225 289 361

Cela fonctionne et nous pourrions de même écrire facilement une fonction map_and_grep qui commence par appliquer une transformation aux données en entrée puis filtre le résultat. Mais ceci n'est pas très utile, car nous sommes conduits à créer des fonctions trop spécialisées qui nous font perdre la généralité et l'expressivité que fournit l'abstraction des fonctions map et grep. Il est surtout tellement facile de remplacer notre fonction grep_and_map ci-dessus par un simple enchaînement des deux opérateurs dans un pipe-line comme nous l'avons fait dans la première partie de ce tutoriel :

grep et map simples

print join " ", map {$_ ** 2} grep {$_ % 2} 0..20; print "\n";

# affiche: 1 9 25 49 81 121 169 225 289 361 ou même par un simple map sans le grep :

map simple

print join " ", map {$_ % 2 ? $_ ** 2 : ()} 0..20; print "\n";

# affiche: 1 9 25 49 81 121 169 225 289 361

que nos fonctions grep_and_map et map_and_grep ne seront donc guère utiles. Nous n'irons pas plus loin dans cette voie. Comme nous l'avons dit, il s'agissait uniquement d'un exercice de style destiné à montrer comment les fonctions gather et take permettent de créer un nouveau comportement sur une liste et l'on voit que ça marche, le seul problème est juste que créer ce genre de fonctions n'amène pas grand-chose, nous étions dans une fausse voie.

6-6. Perspectives avec gather et take

Si nous pouvons créer facilement les fonctions map et grep grâce à gather/take, nous devrions pouvoir créer d'autres fonctions de listes utiles, telles que certaines de celles que nous avons vues précédemment. La seule limite est notre imagination : tant que nous

n'avons pas pris un peu l'habitude de les utiliser, tant que nous ne nous les sommes pas vraiment appropriées, nous ne sommes pas trop sûrs de ce que l'on pourrait en faire. Si, comme nous l'espérons, Perl 6 voit le jour prochainement, nous ne doutons pas que, d'ici quelques années, les développeurs Perl 6 auront certainement trouvé des tas d'usages utiles à ces fonctions, mais nous sommes aujourd'hui un peu dans la situation d'une poule ayant trouvé une paire de ciseaux et se demandant à quoi cela peut servir. Peu importe de ne pas avoir un usage immédiat, après tout, on peut aussi très bien mettre cet instrument dans sa boîte à outils et se dire qu'il ne tardera sans doute pas à servir si l'on sait rester vigilant et y penser au bon moment.

Le point important est que la fonction map prend une liste en entrée et applique des transformations à cette liste pour générer une autre liste. La fonction grep prend une liste en entrée et filtre les éléments de cette liste. Avec gather et take, nous pouvons maintenant faire des choses plus variées ou plus complexes sur notre liste en entrée. Les fonctions map et grep ne sont que des cas particuliers limités de gather et take.

Documents relatifs