• Aucun résultat trouvé

Un cran de plus dans la simplification : la curryfication

La curryfication est une technique essentielle dans les langages fonctionnels, en particulier dans les langages fonctionnels purs comme Haskell.

Dans un langage fonctionnel réellement pur, non seulement une fonction ne doit pas changer son environnement d'exécution (pas d'effet de bord), mais il faut parfois que la fonction elle- même soit un être mathématique pur qui ne peut prendre qu'un seul argument et ne retourner qu'une seule valeur. Cela présente des avantages certains et des inconvénients indéniables dont nous ne discuterons pas ici. Disons simplement que Perl ne fait pas partie de ce genre de langage et n'est donc nullement concerné par ces limitations, mais il peut néanmoins tirer parti des techniques utilisées par ces langages fonctionnels pour faire face à ces contraintes.

La curryfication tire son nom du mathématicien et logicien américain Haskell B. Curry (1900- 1982), l'un des fondateurs des théories mathématiques logiques (lambda-calcul et autres) à l'origine de la plupart des concepts à la base des langages de programmation fonctionnelle. Le langage Haskell a bien sûr été baptisé d'après son prénom. Une partie au moins des idées de Curry provient d'un article très novateur et apparemment unique de Moses Schönfinkel (alias Moisei Isai'evich Sheinfinkel), logicien et mathématicien juif soviétique (1889-1942), qui semble avoir inventé l'idée de curryfication. Certains auteurs ont même proposé de renommer « schönfikelisation » la curryfication. N'étant nullement spécialiste, nous ne prendrons pas parti et nous contenterons d'utiliser le terme le plus courant de curryfication.

4-1. L'idée de base de la curryfication

La curryfication consiste à remplacer une fonction ayant plusieurs arguments par une fonction n'acceptant qu'un seul argument et renvoyant une autre fonction (généralement une fermeture) chargée de traiter les autres arguments.

L'exemple classique est une fonction d'ajout. Soit une fonction add(x, y) qui prend deux arguments et en renvoie la somme. Une fois curryfiée, on aurait une fonction add(x) qui prend un argument et renvoie une fonction qui prend le deuxième argument. La curryfication permet de réaliser une application partielle : si on appelle la fonction curryfiée avec l'argument 2, on reçoit en retour une fonction unaire qui ajoute 2 à son argument.Voici un exemple en Caml :

let add x y = x + y;; (* définit la fonction add *)

let add_two = add 2;; (* la fonction add_two ajoute 2 à son argument *)

add_two 3;; (* applique add_two à 3 et retourne 5 *)

Le mécanisme de la fermeture permet de faire la même chose très simplement en Perl : add_2

sub add {

my $ajout = shift;

return sub {$ajout + shift} };

my $add_2 = add(2);

print $add_2->(3); # imprime 5

Ici, la coderef $add_2 est une version curryfiée de la fonction add. On peut bien sûr en créer une autre (ou autant que l'on veut) :

my $add_3 = add(3);

print $add_3->(5); # imprime 8

4-2. Rien de bien nouveau sous le soleil ?

À vrai dire, il n'y a pas grand-chose de bien nouveau dans ce que nous venons de faire. Plusieurs des fonctions étudiées aux chapitres 3.3 à 3.5 de la partie 2 de ce tutoriel faisaient déjà à peu près le même chose et étaient pour certaines implicitement curryfiées (partiellement ou totalement). Simplement, nous ne l'avons pas dit à l'époque parce que notre objectif était alors autre : expliquer le mécanisme de fonction retournant des fonctions (ou d' « usine à fonctions »). Cette idée était suffisamment nouvelle pour ne pas l'encombrer de considérations théoriques supplémentaires. Implicitement, dès que l'on crée une fermeture qui stocke une partie de ses paramètres dans son environnement d'exécution, on pratique une forme (primaire et peut-être incomplète) de curryfication, même si ce n'est pas le but premier.

Essayons cependant d'utiliser cette notion de façon plus explicite, pour réellement transformer des fonctions à plusieurs arguments en une chaîne de fonctions à un seul argument.

4-3. Curryfication de la fonction parcours_dir

Vous vous souvenez de la fonction parcours_dir qui nous a permis d'introduire la notion de fonction de rappel au début de la seconde partie de ce tutoriel ? Il s'agissait de parcourir une arborescence de répertoires sur un disque dur et d'appliquer une fonction de rappel passée en paramètre aux objets trouvés dans ce répertoire. Celle-ci s'écrivait à peu près comme suit : parcours_dir 1 #!/usr/bin/perl use strict; use warnings; my $dir_depart = $ARGV[0]; chomp $dir_depart;

my $imprime_fic = sub { print $_[0], "\n"; }; parcours_dir($imprime_fic, $dir_depart); sub parcours_dir {

my ($code_ref, $path) = @_;

my @dir_entries = glob("$path/*"); foreach my $entry (@dir_entries) { $code_ref->($entry) if -f $entry;

parcours_dir($code_ref, $entry) if -d $entry; }

}

Il n'est pas bien difficile de créer une version curryfiée : parcours_dir curryfiée

#!/usr/bin/perl use strict; use warnings;

sub create_func { my $code_ref = shift; my $func; $func = sub { my $path = shift; my @dir_entries = glob("$path/*"); foreach my $entry (@dir_entries) { $code_ref->($entry) if -f $entry; $func->($entry) if -d $entry; }

} }

my $print_dir = create_func(sub { print shift, "\n"; }); $print_dir->($ARGV[0]);

Cela marche, mais l'intérêt de l'opération ne saute pas sans doute aux yeux pour l'instant : le code est sans doute un peu plus complexe à comprendre et un peu plus long. Peut-être est-ce marginalement plus rapide parce que la fonction récursive $func prend un paramètre de moins, mais ce n'est même pas sûr et la différence est de toute façon au mieux minime. Cela nous permet de résoudre en partie le problème de la variable globale que nous utilisions pour mesurer la taille des fichiers présents dans l'arborescence du répertoire :

parcours_dir curryfiée 2 #!/usr/bin/perl use strict; use warnings; sub create_func { my ($code_ref, $tot_size_ref) = @_; my $func; $func = sub { my $path = shift; my @dir_entries = glob("$path/*"); foreach my $entry (@dir_entries) {

$func->($entry) and next if -d $entry;

$code_ref->($tot_size_ref, $entry) if -f $entry; }

} }

my $tot_size = 0;

my $size_dir_func_ref = create_func(

sub {my $size_ref = shift; $$size_ref += (-s shift)}, \$tot_size);

$size_dir_func_ref->($ARGV[0]); print $tot_size, "\n";

Cette version fonctionne, mais est à la vérité un peu maladroite et peu satisfaisante, parce que la fonction create_func n'est plus générique (il faut lui passer un paramètre supplémentaire, la taille), l'appel récursif à $code_ref nécessite également un paramètre supplémentaire et deux des fonctions prennent deux paramètres et ne sont donc pas réellement curryfiées.

En fait, la coderef $func doit être une fermeture sur la coderef de la fonction de rappel, mais n'a pas besoin de se fermer sur la taille, qu'elle n'a pas réellement besoin de connaître. C'est la coderef ($size_dir_func_ref) de la fonction de rappel qui aurait besoin de se fermer sur la taille. Voici donc une nouvelle version répondant à ces besoins et contenant trois fermetures et des fonctions réellement curryfiées :

parcours_dir curryfiée 3 #!/usr/bin/perl use strict; use warnings; sub create_func { my $code_ref = shift; my $func; $func = sub { my $path = shift; my @dir_entries = glob("$path/*"); foreach my $entry (@dir_entries) {

$func->($entry) and next if -d $entry; $code_ref->($entry) if -f $entry; } } } sub parcours_dir { my $tot_size = 0;

my $size_dir_func_ref = create_func( sub { $tot_size += (-s shift)} );

$size_dir_func_ref->($_[0]); return $tot_size, "\n"; }

print parcours_dir($ARGV[0]);

Cette fois, nous avons atteint tous nos objectifs :

 la fonction create_func est redevenue complètement générique et abstraite, elle peut être réutilisée sans changement pour n'importe quel autre parcours de répertoire, c'est la fonction de rappel qui assure entièrement la logique fonctionnelle à employer ;

 il n'y a plus de variable globale contenant la taille du répertoire et, plus généralement, toutes les variables ne sont accessibles que là où elles ont besoin de l'être ;

 toutes les fonctions ne prennent qu'un seul argument et sont bien complètement curryfiées, il n'y a plus de passage répétitif du même paramètre.

4-4. Une version curryfiée de reduce

La fonction reduce étudiée précédemment présente encore un petit défaut : la fonction utilisatrice (max, par exemple) reçoit en paramètre une liste et doit repasser cette liste à reduce. N'y a-t-il pas moyen de simplifier cette syntaxe ? Il ne s'agit pas simplement d'ajouter du sucre syntaxique : accessoirement, n'est-il pas possible d'éviter ce double passage de paramètres, qui peut être gourmand en mémoire et en temps CPU ? Est-ce que la curryfication de cette fonction ne pourra pas améliorer les choses ?

Essayons une première tentative : reduce curryfiée 1

use strict; use warnings; sub reduce (&) {

my $func = sub { my $result = shift; for (@_ ) {

local ($a, $b) = ($result, $_ ); $result = $code_ref->($a, $b ); } return $result; }; return $func; }

my $max = sub { reduce { $a > $b ? $a : $b } }; my $maximum = $max->()->(1..10);

print "Le max est: $maximum \n";

L'écriture de la fonction reduce curryfiée ne présente pas de difficulté particulière. Elle peut être simplifiée comme suit :

reduce curryfiée simplifiée sub reduce (&) {

my $code_ref = shift; return sub { my $result = shift; for (@_ ) {

local ($a, $b) = ($result, $_ ); $result = $code_ref->($a, $b ); }

return $result; };

}

Mais le problème est que l'on ne peut pas vraiment dire que l'appel de la coderef $max : Appel de max

my $maximum = $max->()->(1..10);

soit simplifié, en raison de la double indirection des coderefs. Bref, on est bien loin du sucre syntaxique.

Les syntaxes d'appel alternatives sont à peine plus encourageantes. Par exemple, la syntaxe d'appel suivante :

reduce curryfiée 2

my $max = sub { reduce { $a > $b ? $a : $b } }; my $maximum = &$max->(1..10);

fonctionne également, mais n'est pas beaucoup plus lisible. La syntaxe suivante :

reduce curryfiée 3

sub max { reduce { $a > $b ? $a : $b } }; my $maximum = max->(1..10);

est déjà un peu plus lisible mais reste assez lourde.

Nous pouvons utiliser un peu de magie blanche et écrire directement dans la table des symboles(5) :

Reduce curryfiée use strict; use warnings; sub reduce (&@) {

my $code_ref = shift; return sub { my $result = shift; for (@_ ) {

local ($a, $b) = ($result, $_ ); $result = $code_ref->($a, $b ); } return $result; }; } *max = reduce { $a > $b ? $a : $b }; my $maximum = max(1..10);

print "Le max est: $maximum \n"; # imprime "Le max est: 10"

La syntaxe d'appel de la fonction max devient très simple, le résultat recherché est atteint, mais expliquer l'utilisation des typeglobs(il s'agit d'utiliser le sigil « * » pour écrire directement des entrées dans la table des symboles du programme) ferait appel à des notions avancées de Perl qui sortiraient du cadre de ce tutoriel. Le but pour nous est de chercher à étendre le langage, ce qui justifie ici d'utiliser la magie des typeglobs, il n'est vraiment pas conseillé de les utiliser pour de la programmation courante.

En tout état de cause, il devient possible d'écrire une bibliothèque de fonctions utilisant cette version de reduce et plus simple que celle que nous avions décrite plus haut :

Nouvelle bibliothèque avec reduce

*max reduce { $a > $b ? $a : $b }; *min reduce { $a > $b ? $b : $a }; *product reduce { $a * $b }; # etc.

4-5. La curryfication automatique

Si la syntaxe d'appel des fonctions curryfiées nous a posé quelques problèmes de simplification, la curryfication d'une fonction existante est assez simple. La fonction reduce curryfiée est à peine plus complexe que la version de base de reduce. Il suffit d'ajouter une fonction intermédiaire qui se charge de passer les paramètres de façon satisfaisante. Ne serait-il pas possible de le faire de façon automatique ?

Considérons pour commencer le cas simple, étudié au début de ce chapitre, de la fonction add :

Fontion add de base sub add {

my ($c, $d) = @_; return $c + $d; }

Nous pouvons écrire une fonction curry qui transforme automatiquement cette fonction en fonction curryfiée :

Curryfication automatique use strict;

use warnings; sub curry (\&@) { my $code = shift; my @args = @_; sub { unshift @_, @args; goto &$code; }; } my $curried_add = curry(&add, 6); print $curried_add->(5); # imprime 11 sub add {

my ($c, $d) = @_; return $c + $d; }

S'agissant de curryfier la fonction add, le prototype de la fonction curry pourrait être simplement (\&$), au lieu de (\&@). Mais nous désirons que la fonction curry soit généraliste et puisse curryfier des fonctions ayant plus de paramètres.

Ici encore, nous utilisons un peu de magie (blanche). La ligne suivante :

goto &$code;

est un peu particulière. Nous ne succombons pas ici au plaisir rebelle de transgresser les principes établis par Edsger Dijkstra dans son célèbre article « Go To Statement Considered Harmful » (6) (« L'instruction Go To considérée comme nuisible »), publiée dans les ACM en

1968. Perl possède cette forme de goto critiquée à juste titre par Dijkstra et beaucoup d'autres et permettant de brancher inconditionnellement le programme sur une autre instruction, mais non seulement je ne l'ai jamais utilisée personnellement en Perl, mais je ne l'ai presque jamais vue utilisée ailleurs (du moins en Perl). La forme de goto utilisée ici est très particulière et bien différente. Comme le dit la documentation officielle de Perl : « La forme goto &name est hautement magique et est suffisamment éloignée des formes ordinaires de goto pour exonérer ses utilisateurs de l'opprobre qui leur est habituellement réservé. Cet appel substitue la sous-routine nommée par un appel à la fonction fournie ». Pour dire toute la vérité, nous écrivons à nouveau dans la table des symboles en sorte que, quand le programme appelle la fonction add, c'est en réalité la version curryfiée de cette fonction qui est appelée.

Il est possible d'utiliser la même fonction curry pour curryfier une fonction subtract : subtract curryfiée

sub subtract {

my ($c, $d) = @_; return $c - $d; }

my $curried_subtr = curry(&subtract, 6); print $curried_subtr->(5); # imprime 1

Cela fonctionne, mais peut-être pas vraiment comme on pourrait le souhaiter. Le deuxième paramètre passé à la fonction curry est le nombre (le « diminuende ») dont on retranche la valeur (le « diminuateur ») passée à la fonction curried_subtr, alors que l'on pourrait souhaiter l'inverse : que la fonction curryfiée subtract_1, par exemple, retranche 1 de la valeur qui lui est passée en paramètre. Ce comportement est dû à la non-commutativité de la soustraction. On peut facilement contourner ce problème en redéfinissant l'ordre des paramètres passés à la fonction subtract :

subtract_1 curryfiée sub subtract { my ($c, $d) = @_; return $d - $c; } my $subtract_1 = curry(&subtract, 1); print $subtract_1->(4); # imprime 3

4-6. Prototypes et curryfication automatique : l'exemple de la curryfication de la fonction comb_sort

La fonction curry permet de curryfier automatiquement la fonction comb_sort étudiée au § 2.22 ci-dessus :

comb_sort curryfiée my @v;

my $max = 500;

$v[$_] = int rand(20000) foreach (0..$max);

my $curried_comb_sort = curry (&comb_sort, sub {$a<=>$b}); $curried_comb_sort->(\@v);

print "@v";

Ce qui imprime bien une liste correctement triée : $ perl curried_sort.pl 3 25 111 227 241 250 274 280 297 326 334 463 557 585 597 598 605 701 732 734 739 739 797 830 859 864 1063 1081 1108 1118 1145 1167 1170 1222 1243 1302 1321 1337 1346 1379 1424 1470 1492 1511 1517 1542 1543 1628 1649 1718 1719 1721 1721 1723 1727 1842 1892 1950 2000 2050 2079 2094 2124 2165 2194 2298 2311 ...

Il a cependant fallu faire une petite modification syntaxique pour que cela fonctionne correctement. La fonction comb_sort utilisait les prototypes pour permettre l'appel suivant : appel de comb_sort

Le paramètre @v était transformé en référence au tableau grâce au prototype de la fonction comb_sort. Dans la version curryfiée, comme la fonction intermédiaire curry n'impose pas la transformation en référence au tableau, nous avons dû passer une référence au tableau :

Appel de comb_sort curryfiée $curried_comb_sort->(\@v);

mais, d'un autre côté, nous avons gagné le fait de ne plus devoir passer le bloc de code assurant le tri numérique.

Ceci n'est qu'un exemple, mais, plus généralement, la curryfication automatique s'accommode parfois assez mal des fonctions prototypées parce que l'interposition d'une fonction anonyme entre la fonction de base et la version curryfiée bat en brèche le but des prototypes. D'un autre côté, on pourrait écrire des fonctions curry distinctes adaptées aux différents prototypes, mais on perdrait alors la généralité d'une fonction curry générique. Bref, il y a une forme d'opposition entre le caractère général recherché pour une fonction générique telle que curry, et le caractère très particulier et coercitif d'un prototype ; on peut trouver assez facilement des solutions, comme le simple passage d'une référence au tableau @v ci-dessus, mais il faut savoir que la cohabitation des deux notions entraîne parfois des contradictions ou difficultés et nécessite parfois un peu de réflexion. On n'est plus, dans ce cas particulier, dans du « tout automatique ».(7)

Cette (petite) limitation pourrait constituer une raison supplémentaire de limiter l'utilisation des prototypes aux fonctions pour lesquelles ils amènent une réelle simplification syntaxique. Cela dit, il faut relativiser, tout le monde n'a pas forcément besoin de la curryfication automatique, une curryfication manuelle ou semi-automatique « sur mesure » peut suffire dans bien des cas.

Documents relatifs