Des fonctions lambda en C ?

(18 octobre 2019)

BTW: Je n'ai pas étudié le code assembler généré par GCC, une partie de mes conclusions sont donc des spéculations sur l'architecture assembler du code. Il est possible que je fasse des erreurs (en plus de l'orthographe). Et je vous rappelle que, toutes les contributions, au code ou au contenu, sont les bienvenues. Bonne lecture.

Je viens d'apprendre au détour de recherche sur le web (et en marge d'une conversation avec @amj@mastodon.xyz sur Mastodon) qu'il est possible de créer des fonctions anonymes (ou fonction lambda (λ)) en C. Alors, pour moi qui aime le C sans doute autant que la programmation fonctionnelle, je me devais de tester ça !

Comment ça marche ?

Déjà, il est bon de noter que ce qui suit marche avec GCC, mais pas forcément avec d'autre compilateur C. De plus l'ajout de la macro ci-dessous est nécessaire.

#define lambda(c_) ({ c_ _; })

Pour plus de détail sur le fonctionnement cette macros, je vous renvoie vers le blog de Guillaume Chereau (dans les sources). Je vais ici me concentrer sur les possibilités et les limites des fonctions lambda en C.

Les possibilités !

Commençons pas un cas basique, additionner deux variables.

  /* voir source "add.c" */
  int x = 5, y = 6;

  /* Création d'une fonction λ */
  int (*fun) (int, int) = lambda (int _(int a, int b)
                                  { return a+b; });

  /* Appel à la fonction et affichage du resultat */
  printf ("a+b = %d\n", fun (x, y));

Ce code affichera à l'exécution la somme de a et de b. Il est aussi possible de ne pas enregistrer la lambda dans une variable, et de l'utiliser directement.

  /* voir source "add2.c" */
  int x = 5, y = 6;

  printf ("a+b = %d\n",
          lambda (int _(int a, int b)
                  { return a+b; }) (x, y));

Il est donc aussi possible de passer la nouvelle fonction lambda comme paramètre d'une fonction.

Bon c'est bien beau tous ça, mais jusque-là rien de bien utile. C'est ici que nous arrivons aux choses intéressantes ! Il est aussi possible de capturer des variables de l'environnement courant dans la fonction lambda.

Reprenons notre exemple de a+b mais parton du principe que notre fonction lambda connait déjà le a. Nous n'avons alors plus qu'un paramètre int b.

  /* voir source "add3.c" */
  int a = 5, b = 6;

  printf ("a+b = %d\n",
          lambda (int _(int b)
                  { return a+b; }) (b));

Le programme revoir toujours bien la valeur 11. Il a donc bien la bonne valeur de a. On peut donc se poser la question, "au moment de la création de la fonction, la valeur de a est elle copié ou c'est le pointer vers a qui est utilisé ?"

Pour y répondre, rien de mieux qu'un petit programme de test.

  /* voir source "add4.c" */
  int a = 5, b = 6;

  /* On enregistre la lambda */
  int (*fun) (int) = lambda (int _(int b)
                             { return a+b; });

  /* On fait un premier appel */
  printf ("a+b = %d\n", fun (b));

  /* On modifie la valuer de a */
  a += 1;

  /* On fait un second appel */
  printf ("a+b = %d\n", fun (b));

Et voici le résultat :

a+b = 11
a+b = 12

On peut en conclure que c'est bien le pointer sur a qui est donnée a la lambda, car dans le cas contraire nous aurions eu le même résultat pour les 2 appels a fun.

Limitation(s)

Il y a quand même une limitation de taille : les fonctions lambda ne sont disponibles que durant la fonction courante. C'est à dire qu'une fois sorti de la fonction qui créer la lambda il n'est plus possible de l'utiliser (ou presque). Pour comprendre, je vous propose un exemple.

/* voir error.c */
#include <stdio.h>
#include <stdlib.h>

#define lambda(c_) ({ c_ _; })

void *
get_lambda_add ()
{
  return lambda (int _(int a, int b) { return a+b; });
}

int
main ()
{
  int a = 5, b = 6;

  int (*fun) (int,int) = (int (*) (int a, int b))
                          get_lambda_add ();

  printf ("a+b = %d\n", fun (a, b));

  a += 1;
  printf ("a+b = %d\n", fun (a, b));

  return 0;
}

Jusque-là, pas d'erreur à la compilation. Mais à l'exécution :

a+b = 11
Segmentation fault (core dumped)

Pourquoi ? Et bien, c'est parce que, les lambdas sont au moins en partie initialisé sur la pile (stack). Elle ne sont donc disponibles que pendant l'exécution de la fonction en cours. Ici la lambda est créé dans la fonction get_lambda_add puis exécuter dans le main. Comme il n'y a pas d'appel a une fonction entre get_lambda_add () et fun (a,b) la pile d'exécution n'a pas été réécrite, la fonction et donc toujours dans la pile. Raison pour là qu'elle le première appel marche.

Mais pour le second, fun et printf ont été appelé, il y a donc de nouvelles données sur la pile, le pointer sur la lambda, n'est donc plus valide. Voilà à mon avis la raison du Seg fault.

Il est donc important de bien faire attention à l'utilisation des fonctions lambda, et je conseille de ne pas les utiliser comme fonctions de callback ou comme handler de signal.

Conclusion

Bien que limité, ce mécanisme de lambda est plutôt sympa, il permet de voir le C sous un nouvel angle, avec de nouvelle façon de programmer et avec du code un peu plus compact.

Sources