• Aucun résultat trouvé

Outre la présentation des solutions classiques, nous discutons de leurs avantages et de leurs inconvénients. Dans toute la partie nous prendrons l’exemple de la composition de deux fonctions pour illustrer la composition d’algorithmes. Plus précisément, pour la clarté de l’exposé, seules les fonctions unaires seront considérées, et ce, sans perte de généralité ; en effet, nous pouvons toujours concaténer plusieurs variables en une seule. Nous verrons que ce cas simple permet d’exhiber des cas délicats. Nous notons F et G les deux fonctions unaires considérées. Le code source de cet exemple est donné en C++ avec la convention qu’un texte entre guillemet "..." indique qu’une partie du code source a été omise.

Dans la suite, nous supposons que nous disposons d’un système de "traits" originalement proposé par (134). Un "trait" fait référence à une caractéristique associée à un type. C’est un mécanisme qui peut être utilisé pour faire de la déduction de type. Pour notre exemple, nous supposons que nous mettons en place un tel mécanisme qui associe le type de sortie d’un algorithme en fonction du type de ses entrées. Dans la suite, nous notons le mécanisme de déduction Output_Algo_Trait. L’obtention du type de retour d’un algorithme en fonction de son entrée Input est notée Output_Algo_Trait<Input>::Ret.

Nous présentons maintenant les différentes solutions existantes pour le problème de la composition.

D.5.1 Les fonctions ”templates"

Rappelons qu’il est naturel de représenter un algorithme par une fonction (129). Dans un souci de généricité, une telle fonction est paramétrée par le type des entrées de l’algorithme. Ce qui nous conduit à écrire le code suivant :

template <class Input>

typename Output_Twice_Trait<Input>::RET twice(const Input & x) { return 2*x }

Étant donné qu’il n’est nul besoin de préciser le type des paramètres pour utiliser une telle fonction, cette solution est particulièrement pratique car générique. En outre, le compilateur infère automatiquement le type des entrées et génère le type de sortie grâce au "trait" associé à l’algorithme.

Cependant, cette solution présente un inconvénient majeur puisqu’il est impossible de passer une fonction paramétrée à une autre fonction paramétrée par un type de donnée. En d’autres termes, cela signifie que le polymorphisme de fonction d’ordre supérieur ne peut pas être exprimé par ce procédé.

L’opération de composition s’écrit donc ainsi : F((G(input))) et le type de retour est donné par la ligne suivante :

typename Output_F_Trait< typename Output_G_Trait<Input>::RET>::RET

En d’autre termes, le type de retour de cette composition est le type de retour de F dont le type en entrée est celui donné par le type de retour de G ; et où le type d’entrée de G est Input. Un utilisateur doit écrire cette dernière ligne à chaque fois qu’il désire effectuer une composition. Remarquons que le type de retour de plusieurs compositions imbriquées devient assez vite

très pénible à écrire.

Nous présentons maintenant une solution qui repose sur la notion d’objets fonctions.

D.5.2 Le codage à la “STL”

Le langage C++ possède la capacité de créer des objets qui se comportent comme des fonctions. Cela est rendu possible en surchargeant l’opérateur "()". Ces objets particuliers sont appelés objets fonctions. La bibliothèque standard du C++, largement inspiré par la bibliothèque "Standard Template Library" (STL) (170), a popularisé ce type d’approche. La mise en œuvre est immédiate :

template <class Input, class Output>

struct Twice : public std::unary_function<Input, Output>{ Typedef Output Result;

Result operator()(const Input & x) { return 2*x; } };

Dans ce cas, les types d’entrée et de sortie doivent être fournis explicitement par le program- meur avant l’appel de la fonction. La STL fournit un opérateur de composition compose1 pour les fonctions unaires. La composition s’écrit ainsi :

typedef G<Input, Output_G_Trait<Input>::RET > G_T ;

compose1(F< G_T::Result, Output_F_Trait< G_T::Result> >(), G_T())

Encore une fois, la déduction du type de retour doit être précisée par l’utilisateur. Au final remarquons que cette méthode proposée par la STL requiert que le programmeur donne ex- plicitement la signature de la fonction. Le fait de donner le type en entrée pour un algorithme est contraignant mais toujours possible. En revanche, le fait de devoir donner la signature de la fonction est une contrainte rédhibitoire dans le cadre qui nous intéresse, puisque le compilateur pourrait le faire automatiquement. Enfin, nous remarquons également que le type de retour de cette composition n’est pas une fonction paramétrée. Ceci empêche tout espoir de réutiliser le résultat de cette composition (qui est une fonction) dans une nouvelle composition.

Nous présentons maintenant une solution à notre problème beaucoup plus élégante et adéquate.

D.5.3 Les foncteurs polymorphes directs

Rappelons que nous sommes intéressés par la composition de fonctions en C++ afin que le résultat soit polymorphique, comme dans un langage fonctionnel. McNamara and Smaragdakis présentent dans (122; 163) une bibliothèque nommée FC++ qui fournit une solution au problème de la composition et qui offre la propriété de polymorphisme. Le but de cette bibliothèque est d’émuler un comportement fonctionnel en C++. La solution repose l’utilisation de classe paramétrée imbriquée. Afin de mettre évidence la sémantique de la fonction, la méthode déclare explicitement la signature d’une fonction. Ceci est atteint en paramétrant l’opérateur parenthèse, operator(), et en incluant un système de trait dans la classe. Le code pour la fonction twice est le suivant :

struct Twice {

template <class Input> struct Sig

{

typedef Input FirstArgType;

typedef typename OutputTwiceTrait<Input>::RET ResultType; };

template <class Input>

typename Sig<Input>::ResultType operator()(const Input & x) { return 2*x; }

} twice;

L’encodage de la signature est réalisé par l’utilisation de la classe imbriquée Sig. Cette dernière joue le rôle du "trait", et définit donc le type d’entrée et de retour. Ces informations permettent de déduire les types comme un langage fonctionnel le fait. Remarquons que l’opérateur "()", operator() , est défini comme pour les fonctions paramétrées.

Puisque l’objet fonction contient également sa signature, il est envisageable de passer un objet fonction à un objet fonction d’ordre supérieur. En effet, une fonction d’ordre supérieur doit être capable de déduire sa signature à partir des objets fonctions en entrée. Puisque qu’une fonction d’ordre supérieur doit être considérée comme une fonction classique, la mêmeforme de construction doit être utilisée. Nous insistons sur le fait que le type de retour doit être une fonction. Voici le code pour effectuer une composition de deux fonctions : struct Compose

{

... // Definition de la signature Sig template <class F, class G>

Composer<F,G> operator()(const F &f, const G & g) { return Composer<F,G>(f,g); }

} mycompose;

Le type de retour de la composition est une instance d’un objet qui est construit à partir des fonctions f et g, données en entrée. L’opérateur parenthèse operator() de cet objet effectue le calcul de la composition g( f (.)). Le code pour cet objet est le suivant :

template <class F, class G> struct Composer

{

Composer( const F& ff, const G& gg ) : f(ff), g(gg){} const F &f;

const G &g;

... // Definition of Sig

template <class X> typename Sig<X>::ResultType

operator()( const X& x ) const { return f( g(x) ); } };

Remarquons que cette manière d’effectuer la composition implique nécessairement l’écriture de deux classes disjointes. En effet, la classe Compose est incapable de tenir les fonctions f et gpuisqu’elle ne les connaît pas (les fonctions ne sont connues qu’au moment de l’appel de la fonction avec l’opérateur parenthèse).

Nous voyons maintenant comment utiliser cette approche. Pour effectuer l’appel de la fonction twice sur l’entier 3, l’utilisateur écrit simplement twice(3). Pour effectuer la com- position double de twice sur l’entier 3, il écrit compose(twice, twice)(3). Cette approche a été conçue pour faciliter l’écriture des compositions pour les utilisateurs. Ce type d’écriture est celui prôné par l’approche fonctionnelle (language OCaml par exemple). En revanche, si l’utilisateur désire stocker le résultat dans une variable alors il doit écrire explicitement la de- mande de déduction de type, en respectant les étapes intermédiaires. Par exemple, pour une simple composition de deux fonctions, il doit écrire le code suivant pour stocker le résultat dans une variable i :

MyCompose::Sig<Twice, Twice>::ResultType::Sig<int>::ResultType i= mycompose(twice, twice)(3);

Il faut remarquer que le code à écrire pour récupérer le type de sortie après plusieurs com- position est long. Plusieurs lignes sont nécessaires pour déterminer le type de retour de la composition compose(compose(twice, twice),compose(twice, twice)).

Nous allons présenter notre solution dans la partie suivante.

D.6 Notre solution à base de déduction de type et de classe imbri-