Git --projet-boite-à-outils

Pointeurs de fonction

Motivation

Le projet demande de traiter un corpus de fichier de plusieurs moyens différents (expressions régulières, parseur XML, etc.) pour lequel il y en a en commun de nombreuses opérations : parcours récursif du répertoire à analyser, ouverture de fichiers, nettoyage des chaînes extraites et génération des fichiers de résultats.

Il n'est donc pas utile de répéter ces opérations plusieurs fois, par exemple en faisant une copie d'un fichier et de le modifier pour y écrire un nouveau traitement. Pire, procéder comme cela devient vite un cauchemar à maintenir, car une modification apportée à ces opérations dans un fichier ne sera pas répercutée automatiquement dans les autres.

L'idée est donc d'écrire ce code commun dans une fonction, et d'écrire le code de traitement spécifique dans une autre en lui passant en paramètre la première fonction évoquée (fonction callback).
Pour pouvoir faire cela en Perl, il faut passer une référence vers la fonction à appeler puis utiliser une syntaxe spécifique pour l'appel depuis une variable.

En Perl

Création d'une référence à une fonction.
my $fun_ref = \&function_name;

L'appel à une fonction depuis une référence stockée dans une variable se fait de la manière suivante :

$fun_ref->(arguments);

Démonstration

Voici un script d'illustration. Il prend en argument le mot "lol" ou le mot "pwet". À chacun de ces mots est associée une procédure qui affiche un message. La fonction appelée est cherchée dans une table de hashage qui associe une chaîne de caractère à une référence sur une fonction.

Si un argument non reconnu est passé en paramètre, le script se plaint et aucune des deux fonctions d'affichage n'est appelée. Notez qu'il y a ici deux niveaux d'indirections : la référence elle-même est obtenue depuis la table de hashage.

test_fun_ref.pl (téléchargement)
usage : perl test-fun-ref.pl lol|pwet
#/usr/bin/perl
use warnings;

sub print_lol
{
    print "CALLED function lol\n";
}

sub print_pwet
{
    print "CALLED function pwet\n";
}

sub main
{   
    my $arg = shift @_;
    
    my %functions = (
        'lol' => \&print_lol,
        'pwet' => \&print_pwet
    );
    
    if (exists($functions{$arg}))
    {
        $functions{$arg}->();
    }
    else
    {
        print "La fonction correspondante n'est pas enregistrée.\n";
    }
}

main(@ARGV);

Commentaires

Il n'y a à aucun moment besoin de préciser au langage les arguments d'une fonction, ni d'une référence vers une fonction. Si cela permet d'écrire très facilement pointeurs de fonctions, cela peut poser problème, car même à l'exécution l'interpréteur ne se plaint pas si une fonction est invoquée avec le mauvais nombre d'argument, ou avec des arguments de type erroné.

Cela m'est arrivé une fois quand j'ai modifié la fonction parcours_arborescence_fichiers (sur laquelle nous reviendrons) pour prendre en argument la fonction d'extraction à appliquer. J'avais oublié de repasser cet argument lors de l'appel récursif, ce qui ne menait pas à l'exécution escomptée du programme.

En C#

En C# il est possible d'utiliser des pointeurs de fonctions, appelés delegate (doc. msdn) qui sont des références typées vers une méthode.

La déclaration d'un delegate ressemble à la signature d'une méthode, accompagnée du mot clef delegate.

Le nom du delegate devient un type qui nous permet de créer des variables pour stocker des références vers une méthode qui se conforme au prototype renseigné pour le delegate.
Elle peut ensuite être affectée via le nom d'une méthode, ou une fonction lambda.

delegate int AddOne(int a); // déclaration d'un type delegate

AddOne fun_ptr = x => x + 1; // on instancie une variable de type AddOne avec une fonction lambda

On peut ensuite utiliser le nom du delegate avec la même syntaxe qu'un appel de méthode classique. L'avantage est qu'on utilise ici un typage statique fort, ce qui limite les problèmes d'appels avec de mauvais arguments.