Comparatif d'implémentations, fonctions

Ce qui suit montre une implémentation simple de code impliquant diverses manipulations de fonctions, et ce dans quatre langages distincts soit Java, JavaScript, C# et C++. Nous débuterons par un comparatif côte à côte des quatre implémentations, suite à quoi nous reviendrons sur les détails de chacune (détail de la version C#, détail de la version C++, détail de la version Java, détail de la version JavaScript.

Ce comparatif est particulier en ce sens où les quatre langages sont plutôt différents les uns des autres dans leur traitement des fonctions. Pour cette raison, nous donnerons un aperçu des mécanismes avec lesquels chacun manipule les fonctions, sans prétendre à l'exhaustivité, et sans prétendre non plus proposer des programmes dont les résultats seront les mêmes peu importe le langage.

Comparatif côte à côte

Placées côte à côte, les implémentations que nous examinerons dans ces quatre langages vont comme suit.

Implémentation C# Implémentation C++ Implémentation Java Implémentation JavaScript
using System;

namespace Fonctions
{
    public delegate int delg(double x);
    class X
    {
        private int Val
        {
            set; get;
        }
        public X(int val)
        {
            Val = val;
        }
        public int F(double x) =>
            (int)(x + Val);
        public static int G(double x) =>
            (int) -x;
    }
    class Program
    {
        static void Tester(Func<double, int> f, double arg)
        {
            Console.WriteLine(
               $"Tester sur f({arg}) == {f(arg)}"
            );
        }
        static void TesterGénérique<T,U>(Func<T, U> f, T arg)
        {
            Console.WriteLine(
               "TesterGénérique sur f({arg}) == {f(arg)}"
            );
        }
        static Func<double, int> Créer() =>
            x => (int)(x < 0 ? x - 0.5 : x + 0.5);
        static Func<int> Générateur(int n) =>
            return () => n++;

        static void Main(string[] args)
        {
            X x = new X(3);
            delg fct = x.F;
            Console.WriteLine(
               $"x.F(3.5) == {x.F(3.5)}; fct(3.5) == {fct(3.5)}"
            );
            Tester(x.F, 3.5);
            TesterGénérique(x.F, 3.5);
            fct = X.G;
            Console.WriteLine(
               $"X.G(3.5) == {X.G(3.5)}; fct(3.5) == {fct(3.5)}"
            );
            fct = (double n) => (int) (n + 1);
            Tester(X.g, 3.5);
            TesterGénérique(X.g, 3.5);
            Console.WriteLine(
               $"fct(3.5) == {fct(3.5)}"
            );
            Tester(
               (double n) => (int)(n + 1), 3.5
            );
            TesterGénérique(
               (double n) => (int)(n + 1), 3.5
            );
            var f = Créer();
            Console.WriteLine(
               $"f(3.5) == {f(3.5)}"
            );
            var gen = Générateur(0);
            Console.WriteLine(
               $"{gen()}, {gen()}, {gen()}"
            );
        }
    }
}
#include <iostream>
#include <functional>
using namespace std;
int tite_somme(int x, int y, int z) {
   return x + y + z;
}
void tester(function<int(double)> f, double arg) {
   cout << "tester sur f(" << arg << ") == "
        << f(arg) << endl;
}
template <class F, class A>
   void tester_generique(F f, A arg)    {
      cout << "tester_generique sur f(" << arg << ") == "
           << f(arg) << endl;
   }
template <class T>
   void afficher_params(T && arg) {
      cout << arg;
   }
template <class T, class ... Args>
   void afficher_params(T && arg, Args && ... args) {
      afficher_params(forward<T>(arg));
      cout << ", ";
      afficher_params(forward<Args>(args)...);
   }
template <class F, class ... Args>
   void tester_variadique(F f, Args && ... args) {
      cout << "tester_variadique sur f(";
      afficher_params(args...);
      cout << ") == " << f(forward<Args>(args)...) << endl;
   }
function<int(double)> creer() {
   return [](double x) {
      return static_cast<int>(x < 0 ? x - 0.5 : x + 0.5);
   };
}
function<int()> generateur(int n) {
   return[=]() mutable { return n++; };
}
int fglob(double x) {
   return static_cast<int>(2.0 * x);
}
class X {
   int val_;
public:
   X(int val) : val_{ val } {
   }
   int f(double x) const {
      return static_cast<int>(x + val_);
   }
   static int g(double x) {
      return static_cast<int>(-x);
   }
};

//
// Pointeur de fonction (type)
//
typedef int(*pfct)(double);

//
// Pointeur de fonction (C++11)
//
using pfct11 = int(*)(double);

int main() {
   //
   // Note: on aurait aussi pu écrire (sans typedef) ceci:
   //
   // int (*pointeur_fonction)(double) = fglob;
   //
   pfct ptr_fct = fglob;
   pfct11 ptr_fct11 = fglob;
   cout << "fglob(3.5) == " << fglob(3.5) << endl;
   cout << "ptr_fct(3.5) == " << ptr_fct(3.5) << endl;
   cout << "ptr_fct11(3.5) == " << ptr_fct11(3.5) << endl;
   //
   // Une méthode de classe est une fonction globale déguisée
   //
   ptr_fct = X::g;
   ptr_fct11 = X::g;
   cout << "X::g(3.5) == " << X::g(3.5) << endl;
   cout << "ptr_fct(3.5) == " << ptr_fct(3.5) << endl;
   cout << "ptr_fct11(3.5) == " << ptr_fct11(3.5) << endl;
   //
   // Pour une méthode d'instance, c'est plus subtil
   //
   X x(3);
   int (X::*pmeth)(double) const = &X::f;
   cout << "x.f(3.5) == " << x.f(3.5) << endl;
   cout << "(x.*pmeth)(3.5) == " << (x.*pmeth)(3.5) << endl;
   X *p = &x;
   cout << "(p->*pmeth)(3.5) == " << (p->*pmeth)(3.5) << endl;
   //
   // Pour généraliser, couvrir il y a function
   //
   function<int(double)> func = fglob;
   cout << "func(3.5) == " << func(3.5) << endl;
   func = X::g;
   cout << "func(3.5) == " << func(3.5) << endl;
   func = [](double x) { return static_cast<int>(x * x); };
   cout << "func(5.5) == " << func(5.5) << endl;
   function<int(const X, double)> meth(&X::f);
   cout << "meth(x, 3.5) == " << meth(x, 3.5) << endl;
   //
   // Pour une syntaxe de fonction unaire...
   //
   using namespace std::placeholders;
   auto fm = bind(meth, x, _1);
   cout << "fm(3.5) == " << fm(3.5) << endl;
   //
   // Évidemment, il y a d'autres mécanismes
   //
   tester(fglob, 3.5);
   tester_generique(fglob, 3.5);
   tester_variadique(tite_somme, 1, 2, 3);
}
public class Fonctions {
   interface Appelable {
      int f(double x);
   }
   interface AppelableGénérique<T,U> {
      U f(T x);
   }
   static class X {
      private int val;
      public X(int val) {
         this.val = val;
      }
      public int f(double x) {
         return (int)(x + val);
      }
      public static int g(double x) {
         return (int) -x;
      }
   }
   static void tester(Appelable app, double arg) {
      System.out.println(
         "tester sur app(" + arg + ") == " + app.f(arg)
      );
   }
   static <T,U> void testerGénérique(AppelableGénérique<T, U> app, T arg) {
      System.out.println(
         "testerGénérique sur app(" + arg + ") == " + app.f(arg)
      );
   }
   static Appelable créer() {
      return new Appelable() {
         public int f(double arg) {
            return (int) (arg < 0? arg - 0.5 : arg + 0.5);
         }
      };
   }
   interface Générateur {
      int prochain();
   }
   static Générateur générer(int n) {
      class Gen implements Générateur {
         private int n;
         public Gen(int n) {
            this.n = n;
         }
         public int prochain() {
            return n++;
         }
      }
      return new Gen(n);
   }
   public static void main(String[] args) {
      final X x = new X(3); // final est requis pour permettre une fermeture
      Appelable app0 = new Appelable() {
         public int f(double arg) {
            return x.f(arg);
         }
      };
      System.out.println(
         "x.f(3.5) == " + x.f(3.5) + "; app0.f(3.5) == " + app0.f(3.5)
      );
      tester(app0, 3.5);
      testerGénérique(
         new AppelableGénérique<Double,Integer>() {
            public Integer f(Double arg) {
               return x.f(arg.doubleValue()); // boxing!
            }
         }, 3.5
      );
      app0 = new Appelable() {
         public int f(double arg) {
            return X.g(arg);
         }
      };
      System.out.println(
         "X.g(3.5) == " + X.g(3.5) + "; app0.f(3.5) == " + app0.f(3.5)
      );
      tester(app0, 3.5);
      testerGénérique(
         new AppelableGénérique<Double,Integer>() {
            public Integer f(Double arg) {
               return X.g(arg.doubleValue()); // boxing!
            }
         }, new Double(3.5)
      );
      tester(créer(), 3.5);
      Générateur gen = générer(0);
      System.out.println(
         gen.prochain() + ", " + gen.prochain() + ", " + gen.prochain()
      );
   }
}
function appliquer(f, arg) {
   return f(arg);
}
function testA() {
   var f = function(x) {
      return x + 0.5;
   };
   alert("appliquer(f, 3.5) == " + appliquer(f,3.5));
}
function creer() {
   return function(arg) {
      return arg < 0? arg - 0.5 : arg + 0.5;
   };
}
function testB() {
   var f = creer();
   alert("f(3.5) == " + f(3.5));
}
function generer(n) {
   return function() {
      return n++;
   };
}
function testC() {
   var gen = generer(0);
   alert("gen(), gen(), gen() -> " + gen() + ", " + gen() + ", " + gen());
}
Exécution, implémentation C# Exécution, implémentation C++ Exécution, implémentation Java Exécution, implémentation JavaScript
x.F(3.5) == 6; fct(3.5) == 6
Tester sur f(3,5) == 6
TesterGénérique sur f(3,5) == 6
X.G(3.5) == -3; fct(3.5) == -3
Tester sur f(3,5) == -3
TesterGénérique sur f(3,5) == -3
fct(3.5) == 4
Tester sur f(3,5) == 4
TesterGénérique sur f(3,5) == 4
f(3.5) == 4
0, 1, 2
fglob(3.5) == 7
ptr_fct(3.5) == 7
ptr_fct11(3.5) == 7
X::g(3.5) == -3
ptr_fct(3.5) == -3
ptr_fct11(3.5) == -3
x.f(3.5) == 6
(x.*pmeth)(3.5) == 6
(p->*pmeth)(3.5) == 6
func(3.5) == 7
func(3.5) == -3
func(5.5) == 30
meth(x, 3.5) == 6
fm(3.5) == 6
tester sur f(3.5) == 7
tester_generique sur f(3.5) == 7
tester_variadique sur f(1, 2, 3) == 6
x.f(3.5) == 6; app0.f(3.5) == 6
tester sur app(3.5) == 6
testerGénérique sur app(3.5) == 6
X.g(3.5) == -3; app0.f(3.5) == -3
tester sur app(3.5) == -3
testerGénérique sur app(3.5) == -3
tester sur app(3.5) == 4
0, 1, 2

Pour testA() :

appliquer(f, 3.5) == 4

Pour testB() :

f(3.5) == 4

Pour testC() :

gen(), gen(), gen() -> 0, 1, 2

Cette diversité est intéressante étant donné les similitudes syntaxiques générales entre ces quatre langages : noms des types primitifs, structures de contrôle de base (if, while, for, try, etc.) et recours à des accolades pour délimiter les blocs, entre autres choses.

Quelques notes d'ordre général :

Clairement, on parle ici d'un créneau pour lequel ces quelques langages ont pris des chemins bien différents les uns des autres.

Discussion de l'implémentation C#

Le détail de notre implémentation à l'aide du langage C# suit.

En C#, depuis les tout débuts, la métephore privilégiée pour manipuler des méthodes en tant que méthodes est le délégué.

Représenté par le type delegate dans le langage, un délégué est une entité polymorphique sur la base de la signature. En C#, ceci couvre à la fois les méthodes d'instance et les méthodes de classe.

Dans cet exemple, un delg est un délégué qui peut pointer sur n'importe quelle méthode prenant en paramètre un double et retournant un int.

using System;

namespace Fonctions
{
    public delegate int delg(double x);

La classe X contient une propriété privée, un constructeur paramétrique, et surtout (pour notre exemple) une méthode d'instance F et une méthode de classe G, toutes deux ayant la même signature (qui correspond à celle supportée par le type delg, plus haut).

    class X
    {
        private int Val
        {
            set; get;
        }
        public X(int val)
        {
            Val = val;
        }
        public int F(double x) =>
            (int)(x + Val);
        public static int G(double x) =>
            (int) -x;
    }

Dans le code de test, j'utilise une méthode de classe Tester() prenant en paramètre un Func<double,int> et un double.

Un Func<T0,T1,...,R> est une sorte de délégué générique prenant en paramètre un T0, un T1, ... et retournant un R. Dans notre cas, le Func représente une fonction unaire (à un paramètre), et Tester(f,arg) appelle f(arg) pour en afficher le résultat.

    class Program
    {
        static void Tester(Func<double, int> f, double arg)
        {
            Console.WriteLine(
               $"Tester sur f({arg}) == {f(arg)}"
            );
        }

La méthode de classe TesterGénérique() se comporte comme le fait Tester(), mais ne se limite pas à un Func<double,int>, acceptant plutôt un Func<T,U> pour deux types T et U quelconques.

        static void TesterGénérique<T,U>(Func<T, U> f, T arg)
        {
            Console.WriteLine(
               $"TesterGénérique sur f({arg}) == {f(arg)}"
            );
        }

La méthode de classe Créer() retourne un objet (une λ, concrètement) pouvant s'utiliser comme un délégué unaire prenant un double et retournant un int. Puisque le type d'une λ est inaccessible au programme, nous entreposons cet objet dans un Func de signature adéquate.

        static Func<double, int> Créer()
            (x) => (int)(x < 0 ? x - 0.5 : x + 0.5);

La méthode de classe Générateur(n) retourne un objet (une λ, concrètement) se comportant comme une opération nullaire (sans paramètres). L'objet en question capture une copie de n dans une fermeture; à chaque utilisation de cet objet comme une fonction, il retournera n++, modifiant chaque fois sa copie interne de n.

        static Func<int> Générateur(int n) =>
            () => n++;

Le programme de test procède aux opérations suivantes :

  • Une instance de X, nommée x, est créée. Ceci nous permettra de tester un délégué sur une méthode d'instance comme sur une méthode de classe
  • Un délégué nommé fct est ensuite placé sur x.f, ce qui permettra de l'utiliser comme la méthode d'instance f de l'objet x. Cette syntaxe concise peut surprendre un programmeur C++, langage où le passage de this aux méthodes d'instance est moins transparent
  • Des tests sont faits en traitant fct comme un Func<double,int> et comme un Func<T,U> pris au sens large du terme
  • Par la suite, fct est utilisé pour mener sur une λ de signature appropriée
  • Enfin, un test est fait sur Créer(), méthode qui retourne une sorte de fonction, et sur gen, méthode qui retourne un objet muni d'état et pouvant être utilisé pour générer une séquence d'entiers

Remarquez que pour les fonctions Créer() et Générateur(), j'ai utilisé var pour indiquer le type de l'objet retourné. En utilisant var, le type de la variable est déduit du type de l'expression utilisée pour l'initialiser.

        static void Main(string[] args)
        {
            X x = new X(3);
            delg fct = x.f;
            Console.WriteLine(
               $"x.F(3.5) == {x.F(3.5)}; fct(3.5) == {f(3.5)}"
            );
            Tester(x.F, 3.5);
            TesterGénérique(x.F, 3.5);
            fct = X.G; // méthode de classe
            Console.WriteLine(
               $"X.G(3.5) == {X.G(3.5)}; fct(3.5) == {fct(3.5)}"
            );
            fct = (double n) => (int) (n + 1); // lambda
            Tester(X.G, 3.5);
            TesterGénérique(X.G, 3.5);
            Console.WriteLine(
               $"fct(3.5) == {fct(3.5)}"
            );
            Tester(
               (double n) => (int)(n + 1), 3.5
            );
            TesterGénérique(
               (double n) => (int)(n + 1), 3.5
            );
            var f = Créer();
            Console.WriteLine(
               $"f(3.5) == {f(3.5)}"
            );
            var gen = Générateur(0);
            Console.WriteLine(
               $"{gen()}, {gen()}, {gen()}"
            );
        }
    }
}

Discussion de l'implémentation C++

Le détail de notre implémentation à l'aide du langage C++ suit.

Pour les besoins de nos exemples, nous définirons plusieurs fonctions. L'une d'elles sera tite_somme(), qui calculera la somme de trois entiers et retournera cette somme.

On aurait bien sûr pu y aller avec une version plus générale, par exemple :

template <class T>
   T tite_somme(T && arg) {
      return arg;
   }
template <class T, class ... Args>
   auto tite_somme(T && arg, Args && ... args) {
      return arg + tite_somme(tite_somme(std::forward<Args>(args)...));
   }

... ou encore, avec une Fold Expression :

template <class ... Args>
   auto tite_somme(Args && ... args) {
      return (args + ...);
   }

... mais pour nos besoins, cela n'aurait rien ajouté au propos.

#include <iostream>
#include <functional>
using namespace std;
int tite_somme(int x, int y, int z) {
   return x + y + z;
}

La fonction tester() prend en paramètre une opération f de signature int(double), donc prenant en paramètre un double et retournant un int, de même qu'un double nommé arg, et appelle f(arg).

Un std::function est un proche cousin d'un délégué en C#, en ce sens qu'un objet de ce type encapsule une entité appelable sur la base d'une signature précise. Les deux sont distincts l'un de l'autre sur le plan de l'utilisation avec une méthode d'instance, cela dit : en C#, le lien entre this et sa méthode est encapsulé dans le délégué, alors qu'en C++, this apparaît comme premier paramètre (habituellement silencieux) dans la signature de la méthode. Nous montrerons un exemple plus bas.

void tester(function<int(double)> f, double arg) {
   cout << "tester sur f(" << arg << ") == " << f(arg) << endl;
}

La fonction tester_generique() prend en paramètre une opération f et un paramètre arg, et appelle f(arg). Elle ne connaît pas de prime abord les types de l'un et de l'autre, mais il importe pour elle qu'utiliser f comme une fonction et lui passer arg en paramètre soit légal.

Ceci permet entre autres un cas où f est de signature std::string(int) et où arg est un short, car il est légal de passer un short en paramètre à une fonction s'attendant à recevoir un int.

template <class F, class A>
   void tester_generique(F f, A arg) {
      cout << "tester_generique sur f(" << arg << ") == " << f(arg) << endl;
   }

Les fonctions tester_variadique(f, args...) et afficher_params(args...) permettent respectivement d'appeler f(args...) et d'afficher les valeurs de chaque paramètre dans args...

On peut dire de tester_variadique() qu'il s'agit en quelque sorte d'une généralisation de tester_generique().

template <class T>
   void afficher_params(T && arg) {
      cout << arg;
   }
template <class T, class ... Args>
   void afficher_params(T && arg, Args && ... args) {
      afficher_params(forward<T>(arg));
      cout << ", ";
      afficher_params(forward<Args>(args)...);
   }
template <class F, class ... Args>
   void tester_variadique(F f, Args && ... args) {
      cout << "tester_variadique sur f(";
      afficher_params(args...);
      cout << ") == " << f(forward<Args>(args)...) << endl;
   }

La fonction creer() retourne une λ encapsulée dans un function<int(double)> et pouvant s'utiliser comme telle. Puisque le type d'une λ est inaccessible au programme, nous entreposons cet objet dans un function de signature adéquate.

function<int(double)> creer() {
   return [](double x) {
      return static_cast<int>(x < 0 ? x - 0.5 : x + 0.5);
   };
}

La fonction generateur(n) retourne une λ encapsulée dans un function<int()> et pouvant s'utiliser comme telle (sans paramètres). L'objet en question capture une copie de n dans une fermeture; à chaque utilisation de cet objet comme une fonction, il retournera n++, modifiant chaque fois sa copie interne de n.

Notez qu'en C++, par défaut, l'opérateur () d'une λ est une méthode const. Pour éviter cette situation, dans un cas comme celui-ci où nous souhaitons que les attributs capturés par copie à la construction de la λ soient modifiables, il convient de qualifier la λ de mutable.

function<int()> generateur(int n) {
   return[=]() mutable { return n++; };
}

Nous utiliserons plusieurs formats de fonctions pour les fins de notre démonstration. La fonction fglob() est une fonction globale de signature int(double).

int fglob(double x) {
   return static_cast<int>(2.0 * x);
}

Dans la classe X, on trouve deux méthodes de signature int(double), soit la méthode d'instance f et la méthode de classe g.

Notez que dans le cas de X::f(), la signature est trompeuse car en réalité, le premier paramètre passé à l'appel de cette fonction est this, ici de type const X*. Ceci se reflétera dans notre utilisation de std::function pour l'appeler, un peu plus bas.

class X {
   int val_;
public:
   X(int val) : val_{ val } {
   }
   int f(double x) const {
      return static_cast<int>(x + val_);
   }
   static int g(double x) {
      return static_cast<int>(-x);
   }
};

Il est souvent utile de définir un type (avec typedef) pour un pointeur de fonction; une raison pour cette habitude répandue est que la syntaxe des pointeurs de fonctions, héritée de C, est quelque peu lourde (le nom du type apparaît en plein centre de l'expression!). Consolez-vous cependant : la syntaxe d'un pointeur de méthode d'instance est pire encore!.

Ici, le type pfct est un pointeur sur une fonction prenant un double en paramètre et retournant un int.

typedef int(*pfct)(double);

Depuis C++ 11, une nouvelle écriture, plus générale, est possible avec using. Ici, le type pfct11 est un pointeur sur une fonction prenant un double en paramètre et retournant un int.

Bien que les écritures pour pfct et pfct11 soient équivalentes sur le plan opératoire, il est probable que celle utilisée pour pfct11 vous semble plus lisible.

using pfct11 = int(*)(double);

Le programme de test commence en utilisant un pfct et un pfct11 pour mener à fglob et montre qu'appeler fglob de manière directe ou indirecte, que ce soit par l'un ou l'autre de ces types, donne exactement le même résultat en pratique.

Si nous avions voulu déclarer un pointeur de fonction globale sans définir au préalable un type spécial pour ce faire, une écriture correcte aurait été :

int (*pointeur_fonction)(double) = fglob;

Vous conviendrez que ce n'est pas une syntaxe immédiatement accessible à tous et chacun.

int main() {
   pfct ptr_fct = fglob;
   pfct11 ptr_fct11 = fglob;
   cout << "fglob(3.5) == " << fglob(3.5) << endl;
   cout << "ptr_fct(3.5) == " << ptr_fct(3.5) << endl;
   cout << "ptr_fct11(3.5) == " << ptr_fct11(3.5) << endl;

Une méthode de classe n'a pas de paramètre this silencieux; en ce sens, il s'agit d'une fonction globale, bien que son nom soit qualifié par celui de la classe à laquelle elle appartient. Pour cette raison, nos types pfct et pfct11 permettent tous deux de prendre l'adresse d'une méthode de classe ayant la signature qu'ils décrivent.

   ptr_fct = X::g;
   ptr_fct11 = X::g;
   cout << "X::g(3.5) == " << X::g(3.5) << endl;
   cout << "ptr_fct(3.5) == " << ptr_fct(3.5) << endl;
   cout << "ptr_fct11(3.5) == " << ptr_fct11(3.5) << endl;

Une méthode d'instance a par contre une syntaxe plus complexe, du fait qu'il faut tenir compte du passage de this (en tant que premier paramètre, habituellement implicite, à la fonction). Ainsi, à droite, pmeth est une variable du type « pointeur sur une méthode d'un X qualifié const, prenant en paramètre un double et retournant un int », alors que l'adresse de la méthode f dans un X s'écrit &X::f.

Notez qu'en exprimant &X::f, nous savons où la méthode f d'un X se trouve peu importe ce X, mais pas à quelle instance de X elle se rattache (pas quel est le this à lui passer).

À l'utilisation, on peut appeler la méthode f d'un X spécifique avec son opérateur .* s'il s'agit d'un objet ou d'une référence, ou à travers son opérateur ->* s'il s'agit d'un pointeur. Oui, je sais, ces syntaxes sont épouvantables, mais on parle ici de code pour spécialistes, pas d'opérateurs « de tous les jours ». Notez l'utilisation de parenthèses, nécessaire dans ce cas-ci pour réguler la priorité des opérateurs.

   X x(3);
   int (X::*pmeth)(double) const = &X::f;
   cout << "x.f(3.5) == " << x.f(3.5) << endl;
   cout << "(x.*pmeth)(3.5) == " << (x.*pmeth)(3.5) << endl;
   X *p = &x;
   cout << "(p->*pmeth)(3.5) == " << (p->*pmeth)(3.5) << endl;

Depuis C++ 11, il est plus « normal » d'avoir recours à un std::function applicable à la signature souhaitée pour manipuler fonctions, méthodes et λ. Quelques exemples sont présentés ici. Portez en particulier attention à meth, qui déclare un std::function applicable à une méthode d'instance d'un const X : la signature du function prend deux paramètres plutôt qu'un, le premier étant le this pour cette méthode (de type const X), et à l'utilisation de meth, l'instance sur laquelle le pointeur de méthode doit être utilisé est explicitement passée en tant que paramètre.

   function<int(double)> func = fglob;
   cout << "func(3.5) == " << func(3.5) << endl;
   func = X::g;
   cout << "func(3.5) == " << func(3.5) << endl;
   func = [](double x) { return static_cast<int>(x * x); };
   cout << "func(5.5) == " << func(5.5) << endl;
   function<int(const X, double)> meth(&X::f);
   cout << "meth(x, 3.5) == " << meth(x, 3.5) << endl;

Il est possible de ramener la syntaxe d'utilisation d'un pointeur de méthode unaire à celle d'une fonction unaire en liant le premier paramètre lors de l'appel à une instance spécifique. Ici, nous y arrivons en utilisant std::bind(), et en indiquant que x doit être utilisé en tant que premier paramètre à l'appel de meth (le std::placeholders::_1 représente conceptuellement ce premier paramètre).

   using namespace std::placeholders;
   auto fm = bind(meth, x, _1);
   cout << "fm(3.5) == " << fm(3.5) << endl;

Enfin, quelques exemples d'utilisation de fonctions en tant que paramètres à des fonctions sont proposés, pour compléter le portrait.

   tester(fglob, 3.5);
   tester_generique(fglob, 3.5);
   tester_variadique(tite_somme, 1, 2, 3);
}

Discussion de l'implémentation Java

Le détail de notre implémentation à l'aide du langage Java suit.

Contrairement à C# ou à C++, Java ne supporte ni les délégués, ni les λ – les λ sont l'un des ajouts les plus attendus pour la version 8 du langage.

Ce qui ressemble le plus à un délégué en Java est une classe anonyme, implémentant une interface donnée. Nous en utiliserons deux ici, soit Appelable, dont la méthode f() prend un double et retourne un int, et AppelableGénérique<T,U>, dont la méthode f() prend un T et retourne un U.

public class Fonctions {
   interface Appelable {
      int f(double x);
   }
   interface AppelableGénérique<T,U> {
      U f(T x);
   }

Pour les besoins de la démonstration, nous utilisons une classe X (qualifiée static car elle sera utilisée dans la méthode de classe main()), qui a un attribut privé, un constructeur paramétrique public et (surtout, pour nos fins) une méthode d'instance f et une méthode de classe g, toutes deux de même signature.

   static class X {
      private int val;
      public X(int val) {
         this.val = val;
      }
      public int f(double x) {
         return (int)(x + val);
      }
      public static int g(double x) {
         return (int) -x;
      }
   }

Nous utiliserons deux méthodes de test pour nos interfaces concrète et générique, soit tester(app,arg) qui appellera app.f(arg) et testerGénérique<T,U>(app,arg) qui fera de même, mais sur un app générique.

   static void tester(Appelable app, double arg) {
      System.out.println("tester sur app(" + arg + ") == " + app.f(arg));
   }
   static <T,U> void testerGénérique(AppelableGénérique<T, U> app, T arg) {
      System.out.println("testerGénérique sur app(" + arg + ") == " + app.f(arg));
   }

La méthode de classe créer() montre un idiome de Java, soit l'instanciation « juste à temps » d'une classe anonyme. Par classe anonyme, on entend une classe qui n'a pas de nom explicite dans le programme.

Ici, l'expression new Appelable() { ... } crée un dérivé d'une classe anonyme implémentant Appelable, ce qui est légal dans la mesure où toutes les méthodes déclarées dans Appelable y sont bel et bien implémentées. Ici, on parle de la méthode f prenant en paramètre un double et retournant un int.

Notez qu'une classe anonyme ne peut avoir de constructeur paramétrique. Ceci aura des conséquences dans la méthode générer(n), plus bas.

   static Appelable créer() {
      return new Appelable() {
         public int f(double arg) {
            return (int) (arg < 0? arg - 0.5 : arg + 0.5);
         }
      };
   }

L'interface Générateur expose un service prochain() retournant un entier. Elle pourrait servir de porte d'entrée pour les services d'un générateur d'entiers séquentiels ou aléatoires, par exemple.

   interface Générateur {
      int prochain();
   }

La méthode de classe générer(n) retourne une instance d'une classe interne à la méthode mais implémentant Générateur. La raison pour laquelle nous devons utiliser une classe nommée ici est que nous ne pouvons utiliser de constructeur paramétrique pour une classe anonyme si celle-ci instancie une implémentation d'une interface.

   static Générateur générer(int n) {
      class Gen implements Générateur {
         private int n;
         public Gen(int n) {
            this.n = n;
         }
         public int prochain() {
            return n++;
         }
      }
      return new Gen(n);
   }

Le programme de test procède comme suit :

  • Il instancie d'abord X pour obtenir l'instance x. Notez le recours à une référence qualifiée final ici : que la référence soit inamovible est nécessaire pour la capturer dans iune fermeture, ce que nous ferons incessamment
  • En effet, l'instruction suivante instancie app0, instance d'une classe anonyme implémentant Appelable de manière telle que sa méthode f(arg) délègue le traitement à x.f(arg). Pour qu'une telle capture soit légale en Java, il faut que x soit une référence qualifiée final
  • Des appels à tester() et à testerGénérique() apparaissent à divers endroits, pour montrer comment ces objets créés sur demande peuvent être traités comme des implémentations de l'interface dont ils sont issus
  • Notez que dans le cas des implémentations de l'interface AppelableGénérique, le fait que la généricité en Java passe nécessairement par des classes et implique du Boxing réduit la généralité de la solution :
    • en effet, le double (un primitif) passé en paramètre à l'origine est réifié en Double (une classe) pour assurer la généricité de la méthode f()
    • tristement, le fait que nous appelions à l'interne une méthode prenant en paramètre un double force la conversion du Double en sa valeur de type double
    • pour obtenir le double dans un Double, la méthode à appeler porte un nom qui indique le type de destination (ici : doubleValue()) plutôt qu'être général (quelque chose comme value() serait idéal)
  • Le reste va un peu de soi
   public static void main(String[] args) {
      final X x = new X(3);
      Appelable app0 = new Appelable() {
         public int f(double arg) {
            return x.f(arg);
         }
      };
      System.out.println(
         "x.f(3.5) == " + x.f(3.5) + "; app0.f(3.5) == " + app0.f(3.5)
      );
      tester(app0, 3.5);
      testerGénérique(
         new AppelableGénérique<Double,Integer>() {
            public Integer f(Double arg) {
               return x.f(arg.doubleValue());
            }
         }, 3.5
      );
      app0 = new Appelable() {
         public int f(double arg) {
            return X.g(arg);
         }
      };
      System.out.println(
         "X.g(3.5) == " + X.g(3.5) + "; app0.f(3.5) == " + app0.f(3.5)
      );
      tester(app0, 3.5);
      testerGénérique(
         new AppelableGénérique<Double,Integer>() {
            public Integer f(Double arg) {
               return X.g(arg.doubleValue());
            }
         }, new Double(3.5)
      );
      tester(créer(), 3.5);
      Générateur gen = générer(0);
      System.out.println(
         gen.prochain() + ", " + gen.prochain() + ", " + gen.prochain()
      );
   }
}

Discussion de l'implémentation JavaScript

Le détail de notre implémentation à l'aide du langage JavaScript suit. Puisque JavaScript est de prime abord un langage fonctionnel, les fonctions y sont une entité normale et naturelle à manipuler : il y est simple et idiomatique d'écrire des fonctions qui prennent des fonctions en paramètre et qui retournent des fonctions.

L'implémentation proposée ici se décline en trois temps. Le premier est fait :

  • D'une fonction appliquer(f,arg), où f est une fonction unaire et arg est le paramètre à y suppléer. Une exécution de appliquer(f,arg) a pour effet d'appeler f(arg), et
  • D'une fonction testA(), qui réalise un appel à appliquer() en lui passant une fonction f et un paramètre (un nombre à virgule flottante)

Dans testA(), la fonction passée à appliquer() est une variable locale nommée f, associée à une fonction unaire. Cet exemple montre qu'il est simple de mettre une fonction dans une variable et de passer celle-ci en paramètre à une autre fonction qui, enfin, l'appellera.

function appliquer(f, arg) {
   return f(arg);
}
function testA() {
   var f = function(x) {
      return x + 0.5;
   };
   alert("appliquer(f, 3.5) == " + appliquer(f,3.5));
}

Le deuxième  est fait :

  • D'une fonction creer() qui retourne une fonction unaire (à un paramètre) qui retourne le résultat d'un calcul réalisé sur son paramètre, et
  • D'une fonction testB(), qui appelle creer() et place le résultat (la fonction ainsi créée) dans une variable, puis l'utilise en appelant la fonction en question.
function creer() {
   return function(arg) {
      return arg < 0? arg - 0.5 : arg + 0.5;
   };
}
function testB() {
   var f = creer();
   alert("f(3.5) == " + f(3.5));
}

Enfin, le troisième est fait :

  • D'une fonction generer(n), qui retourne une fonction nullaire (qui ne prend aucun paramètre) capturant n dans une fermeture et retournant n++ à chaque appel, et
  • D'une fonction testC() qui crée, avec generer(0) une fonction associée à la variable gen l'appelle trois fois

Ici, il faut comprendre que generer(n) n'incrémente pas n; en pratique, generer(n) crée et retourne une fonction qui incrémentera une copie de n à chaque appel. La fonction retournée a donc un état (la copie de n), un peu comme un objet.

function generer(n) {
   return function() {
      return n++;
   };
}
function testC() {
   var gen = generer(0);
   alert("gen(), gen(), gen() -> " + gen() + ", " + gen() + ", " + gen());
}

Conclusion

En espérant que ce tour d'horizon succinct vous ait intéressé, malgré son caractère non-exhaustif.


Valid XHTML 1.0 Transitional

CSS Valide !