• Aucun résultat trouvé

5-1. Utiliser deux listes : la fonction combine

Il est assez fréquent de devoir utiliser membre à membre deux listes ou deux tableaux différents, par exemple pour les comparer ou les combiner. Il est assez courant d'appeler combine une fonction générique abstraite réalisant ce genre d'opération.

Contrairement à ce que nous avons fait précédemment, nous n'allons plus, cette fois, proposer une première approche naïve, constater qu'elle aboutit à une duplication de code ou à des erreurs syntaxiques et arriver progressivement à la solution générique permettant d'éviter cette duplication. Le lecteur est maintenant habitué à la méthode et aux techniques qu'elle emploie, nous pouvons nous lancer directement dans le grand bain et essayer de créer directement une fonction générique abstraite combine (ce qui n'empêche cependant pas d'améliorer le résultat progressivement).

5-1-1. La fonction combine

De quoi avons-nous besoin ? D'une fonction recevant en paramètres une fonction et deux listes. Ou, plus exactement, des références à une fonction (une coderef, donc) et à deux listes. La fonction doit ensuite prendre un à un chaque élément de chaque liste, appliquer la coderef à ces deux éléments et stocker le résultat dans une nouvelle liste qui sera renvoyée à la fonction appelante lorsque l'une au moins des deux listes sera épuisée. Ce qui peut nous donner ceci :

combine

my ($code_ref, $l1_ref, $l2_ref) = @_; my @result;

while (1) {

local ($a, $b);

($a, $b) = (shift @$l1_ref, shift @$l2_ref); return @result unless defined $a and defined $b; push @result, $code_ref->($a, $b);

} }

Nous pouvons maintenant écrire des fonctions calculant par exemple les sommes membre à membre et les moyennes membre à membre de deux listes :

Somme et moyenne membre à membre sub add2 (\@\@) {

my ($l1, $l2) = @_;

return combine {$a + $b} @$l1, @$l2; }

sub avg2(\@\@) {

my ($l1, $l2) = @_;

return combine {($a + $b) /2} @$l1, @$l2; }

À l'utilisation, cela semble fonctionner plutôt bien : Ajout membre à membre

use strict; use warnings;

use Combine qw/combine add2/; my @list1 = qw /1 5 7 98 32/;

my @list2 = grep $_%2, 1..10; # nombres impairs entre 1 et 10 print join " ", add2(@list1, @list2), "\n";

Ce qui donne :

Exécution de la fonction add2 $ perl add2.pl

2 8 12 105 41

De même, pour calculer la moyenne membre à membre des listes avec la fonction avg2 : Exécution de la fonction avg2

$ perl avg2.pl 1 4 6 52.5 20.5

Cette fonction souffre cependant d'un grave défaut de conception : les listes passées en paramètres sont vidées de leur contenu. Si nous voulons éviter cet effet de bord somme toute assez fâcheux dans certains cas, nous devons soit modifier nos fonctions add2 et avg2 pour qu'elles fassent une copie temporaire des tableaux reçus en paramètres, soit plus probablement modifier la fonction combine pour qu'elle lise les tableaux sans les modifier.

Modifions donc la fonction combine pour qu'elle ne modifie pas les tableaux reçus en argument, mais se contente de les lire :

combine (2)

sub combine (&\@\@) {

my ($code_ref, $l1_ref, $l2_ref) = @_; my @result;

my $i = 0; while (1) {

local ($a, $b) = ($l1_ref->[$i], $l2_ref->[$i++]); return @result unless defined $a and defined $b; push @result, $code_ref->($a, $b);

} }

Cela résout facilement le problème.

5-2. Une fonction intermédiaire de création de fonctions

Nous voudrions cependant aller plus loin dans la simplification et trouver une syntaxe encore plus simple. La solution peut être de créer une fonction intermédiaire de création de fonctions. Celle-ci peut avoir la forme suivante :

Fonction génératrice de fonctions sub generate_function2 { my $code_ref = shift; return sub {

my ($l1_ref, $l2_ref) = @_;

return combine {&$code_ref} @$l1_ref, @$l2_ref; }

}

On peut également l'écrire un peu plus brièvement comme suit :

sub generate_function2(&) { my $code_ref = shift; return sub {

return combine {&$code_ref} @{$_[0]}, @{$_[1]}; } } Ou même : sub generate_function2 { my $code_ref = shift;

sub { combine {&$code_ref} @{$_[0]}, @{$_[1]}; }

}

Cela marche parfaitement :

my $add2 = generate_function2(sub {$a + $b}); my @list1 = qw /1 5 7 98 32/;

my @list2 = grep $_%2, 1..10;

print join " ", $add2->(\@list1, \@list2), "\n"; sub generate_function2 {

my $code_ref = shift;

sub { combine {&$code_ref} @{$_[0]}, @{$_[1]}; } } Ce qui imprime : $ perl generate.pl 2 8 12 105 41

5-2-1. Créer une bibliothèque de fonctions

Il est maintenant possible d'utiliser ce modèle pour générer très simplement toute une bibliothèque de fonctions de listes du même type :

my $add2 = generate_function2( sub { $a + $b } );

my $avg2 = generate_function2( sub { ( $a + $b ) / 2 } ); my $substr2 = generate_function2( sub { $a - $b } );

my $diff2 = generate_function2( sub { abs( $a - $b ) } ); my $mult2 = generate_function2( sub { $a * $b } );

my $equal_nr = generate_function2( sub { $a if $a == $b } ); my $equal_str = generate_function2( sub { $a if $a eq $b } ); my $each_array = generate_function2( sub { $a, $b } );

# ...

Avec ce bout de programme utilisateur :

my @list1 = qw /1 32 5 7 98/; my @list2 = grep $_%2, 1..10;

print join " ", $add2->(\@list1, \@list2), "\n"; print join " ", $avg2->(\@list1, \@list2), "\n"; print join " ", $substr2->(\@list1, \@list2), "\n"; print join " ", $diff2->(\@list1, \@list2), "\n"; print join " ", $mult2->(\@list1, \@list2), "\n"; print join " ", $equal_nr->(\@list1, \@list2), "\n"; print join " ", $equal_str->(\@list1, \@list2), "\n"; print join " ", $each_array->(\@list1, \@list2), "\n"; On obtient le résultat suivant :

$ perl generate.pl 2 35 10 14 107 1 17.5 5 7 53.5 0 29 0 0 89 0 29 0 0 89 1 96 25 49 882 1 5 7 1 5 7 1 1 32 3 5 5 7 7 98 9

Ce qui est bien ce qui était attendu.

5-2-2. Simplification syntaxique

Il subsiste un petit inconvénient, cependant : nous obligeons notre utilisateur à utiliser des coderefs, dont la syntaxe peut être jugée un peu plus rébarbative (pour le débutant) que celle de simples fonctions :

my @list1 = qw /1 5 7 98 32/; my @list2 = grep $_ % 2, 1 .. 10; my @sum = $add2->( @list1, @list2 );

L'inconvénient est assez mineur, mais si l'on veut vraiment l'éliminer, un peu de sucre syntaxique supplémentaire permet d'y remédier :

sub add2 (\@\@) {

my ($list1, $list2) = @_;

return $add2->(\@list1, \@list2); }

Nous pouvons maintenant faire la somme membre à membre comme suit :

my @list1 = qw /1 32 5 7 98/; my @list2 = grep $_%2, 1..10;

print join " ", add2(@list1, @list2), "\n"; Ce qui donne le résultat attendu :

$ perl generate.pl 2 35 10 14 107

Nous avons créé un nouveau niveau d'indirection permettant de simplifier au maximum la syntaxe d'appel de la fonction. Encore une fois, on complique d'un côté (côté codeur du module) et simplifie de l'autre (côté utilisateur du module). À chacun de déterminer jusqu'où aller dans la simplification et le sucre syntaxique.

L'exemple fourni ici montre comment « améliorer » (c'est-à-dire, en réalité, compliquer) petit à petit la façon de faire les choses pour réussir de l'autre côté à simplifier au maximum

l'interface de la fonction fournie à l'utilisateur. D'autres façons de faire la même chose sont envisageables, mais la technique proposée rend le service attendu.

On pourra aussi considérer que nous avons un peu « tourné en rond » : il y avait sans doute un moyen moins compliqué de définir la fonction add2. Mais si l'on considère l'utilisation d'un module prédéfinissant les fonctions dont nous avons besoin, le gain est réel, car nous avons réellement évité beaucoup de duplication de code.

Documents relatifs