88 votes

Pointeurs de fonction, fermetures et lambda

Je suis en train de me familiariser avec les pointeurs de fonction et, alors que je préparais le chapitre de K&R sur le sujet, la première chose qui m'a frappé a été : "Hé, c'est un peu comme une fermeture". Je savais que cette hypothèse est fondamentalement fausse d'une manière ou d'une autre et après une recherche en ligne n'était pas vraiment de trouver une analyse de cette comparaison.

Alors pourquoi les pointeurs de fonction de style C sont-ils fondamentalement différents des fermetures ou des lambdas ? Pour autant que je puisse dire, cela a à voir avec le fait que le pointeur de fonction pointe toujours vers une fonction définie (nommée), par opposition à la possibilité de définir anonymement la fonction.

Pourquoi le passage d'une fonction à une fonction est-il considéré comme plus puissant dans le second cas, où la fonction n'est pas nommée, que dans le premier, où c'est une fonction normale de tous les jours qui est passée ?

Dites-moi s'il vous plaît comment et pourquoi j'ai tort de comparer les deux si étroitement.

Merci.

109voto

Mark Brackett Points 46824

Un lambda (ou fermeture ) encapsule à la fois le pointeur de fonction et les variables. C'est pourquoi, en C#, vous pouvez faire :

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

J'ai utilisé un délégué anonyme comme fermeture (sa syntaxe est un peu plus claire et plus proche du C que l'équivalent lambda), qui a capturé lessThan (une variable de pile) dans la fermeture. Lorsque la fermeture est évaluée, lessThan (dont le cadre de pile peut avoir été détruit) continuera à être référencé. Si je change lessThan, alors je change la comparaison :

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

lessThanTest(99); // returns true
lessThan = 10;
lessThanTest(99); // returns false

En C, ce serait illégal :

BOOL (*lessThanTest)(int);
int lessThan = 100;

lessThanTest = &LessThan;

BOOL LessThan(int i) {
   return i < lessThan; // compile error - lessThan is not in scope
}

mais je pourrais définir une fonction pointeur qui prendrait 2 arguments :

int lessThan = 100;
BOOL (*lessThanTest)(int, int);

lessThanTest = &LessThan;
lessThanTest(99, lessThan); // returns true
lessThan = 10;
lessThanTest(100, lessThan); // returns false

BOOL LessThan(int i, int lessThan) {
   return i < lessThan;
}

Mais, maintenant, je dois passer les 2 arguments lorsque je l'évalue. Si je souhaitais passer ce pointeur de fonction à une autre fonction où lessThan n'était pas dans la portée, je devrais soit le maintenir manuellement en vie en le passant à chaque fonction de la chaîne, soit le promouvoir à un global.

Bien que la plupart des langages courants qui prennent en charge les fermetures utilisent des fonctions anonymes, il n'y a aucune obligation à cet égard. Vous pouvez avoir des fermetures sans fonctions anonymes, et des fonctions anonymes sans fermetures.

Résumé : une fermeture est une combinaison de pointeur de fonction + variables capturées.

45voto

Norman Ramsey Points 115730

En tant que personne ayant écrit des compilateurs pour des langages avec et sans fermetures "réelles", je suis respectueusement en désaccord avec certaines des réponses ci-dessus. Une fermeture Lisp, Scheme, ML ou Haskell ne crée pas dynamiquement une nouvelle fonction . Au lieu de cela, il réutilise une fonction existante mais le fait avec nouvelles variables libres . La collection de variables libres est souvent appelée environnement du moins par les théoriciens du langage de programmation.

Une fermeture est juste un agrégat contenant une fonction et un environnement. Dans le compilateur Standard ML of New Jersey, nous en avons représenté une comme un enregistrement ; un champ contenait un pointeur vers le code, et les autres champs contenaient les valeurs des variables libres. Le compilateur création dynamique d'une nouvelle fermeture (pas de fonction) en allouant un nouvel enregistrement contenant un pointeur sur le fichier même mais avec différents pour les variables libres.

Vous pouvez simuler tout cela en C, mais c'est une véritable corvée. Deux techniques sont populaires :

  1. Passez un pointeur vers la fonction (le code) et un pointeur séparé vers les variables libres, de sorte que la fermeture soit répartie sur deux variables C.

  2. Passe un pointeur vers un struct, où le struct contient les valeurs des variables libres et aussi un pointeur vers le code.

La technique n°1 est idéale lorsque vous essayez de simuler une sorte de polymorphisme en C et vous ne voulez pas révéler le type de l'environnement - vous utilisez un pointeur void* pour représenter l'environnement. Pour des exemples, regardez l'article de Dave Hanson intitulé C Interfaces et implémentations . La technique n° 2, qui ressemble davantage à ce qui se passe dans les compilateurs de code natif pour les langages fonctionnels, ressemble également à une autre technique familière... Des objets C++ avec des fonctions membres virtuelles. Les implémentations sont presque identiques.

Cette observation a donné lieu à une remarque judicieuse de la part d'Henry Baker :

Les gens du monde Algol/Fortran se sont plaints pendant des années qu'ils ne comprenaient pas quelle utilisation possible les fermetures de fonctions auraient dans la programmation efficace du futur. Puis la révolution de la "programmation orientée objet" a eu lieu, et maintenant tout le monde programme en utilisant des fermetures de fonctions, sauf qu'ils refusent toujours de les appeler ainsi.

8voto

Herms Points 13069

En C, vous ne pouvez pas définir la fonction en ligne, donc vous ne pouvez pas vraiment créer une fermeture. Tout ce que vous faites, c'est de faire circuler une référence à une méthode prédéfinie. Dans les langages qui supportent les méthodes/fermetures anonymes, la définition des méthodes est beaucoup plus flexible.

En termes simples, les pointeurs de fonction n'ont pas de portée associée (sauf si vous comptez la portée globale), alors que les fermetures incluent la portée de la méthode qui les définit. Avec les lambdas, vous pouvez écrire une méthode qui écrit une méthode. Les fermetures vous permettent de lier "certains arguments à une fonction et d'obtenir une fonction de plus faible polarité comme résultat". (extrait du commentaire de Thomas). Vous ne pouvez pas faire cela en C.

EDIT : Ajout d'un exemple (je vais utiliser une syntaxe de type Actionscript car c'est ce que j'ai en tête en ce moment) :

Imaginons que vous ayez une méthode qui prend une autre méthode comme argument, mais qui ne permet pas de passer des paramètres à cette méthode lorsqu'elle est appelée ? Par exemple, une méthode qui provoque un délai avant d'exécuter la méthode que vous lui avez passée (exemple stupide, mais je veux rester simple).

function runLater(f:Function):Void {
  sleep(100);
  f();
}

Imaginons maintenant que vous voulez utiliser runLater() pour retarder le traitement d'un objet :

function objectProcessor(o:Object):Void {
  /* Do something cool with the object! */
}

function process(o:Object):Void {
  runLater(function() { objectProcessor(o); });
}

La fonction que vous passez à process() n'est plus une fonction statiquement définie. Elle est générée dynamiquement, et est capable d'inclure des références à des variables qui étaient dans la portée lorsque la méthode a été définie. Ainsi, elle peut accéder à 'o' et 'objectProcessor', même si ceux-ci ne sont pas dans la portée globale.

J'espère que ça a du sens.

6voto

Jon Skeet Points 692016

Fermeture = logique + environnement.

Par exemple, considérez cette méthode C# 3 :

public Person FindPerson(IEnumerable<Person> people, string name)
{
    return people.Where(person => person.Name == name);
}

L'expression lambda encapsule non seulement la logique ("comparer le nom") mais aussi l'environnement, y compris le paramètre (c'est-à-dire la variable locale) "nom".

Pour en savoir plus à ce sujet, jetez un coup d'oeil à mon article sur les fermetures qui vous fait découvrir C# 1, 2 et 3, en montrant comment les fermetures facilitent les choses.

3voto

dsm Points 7429

Un lambda est un anonyme, définis dynamiquement fonction. Vous ne pouvez tout simplement pas faire cela en C... Quant aux fermetures (ou la combinaison des deux), l'exemple lisp typique ressemblerait à quelque chose du genre :

(defun get-counter (n-start +-number)
     "Returns a function that returns a number incremented
      by +-number every time it is called"
    (lambda () (setf n-start (+ +-number n-start))))

En termes de C, on pourrait dire que l'environnement lexical (la pile) de get-counter est capturée par la fonction anonyme, et modifiée en interne comme le montre l'exemple suivant :

[1]> (defun get-counter (n-start +-number)
         "Returns a function that returns a number incremented
          by +-number every time it is called"
        (lambda () (setf n-start (+ +-number n-start))))
GET-COUNTER
[2]> (defvar x (get-counter 2 3))
X
[3]> (funcall x)
5
[4]> (funcall x)
8
[5]> (funcall x)
11
[6]> (funcall x)
14
[7]> (funcall x)
17
[8]> (funcall x)
20
[9]>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X