Les fonctions lambda

Introduction

En programmation, l’utilisation de variable est un élément extrêmement important, autant pour stocker des données que pour les partager entre les différentes instances de votre système. En effet, s’il n’y avait pas de variables, nous ne pourrions pas transférer des valeurs d’un objet à un autre en utilisant les assignateurs (« setters »), les constructeurs ou même des variables globales. Ces variables peuvent contenir toute sorte de valeurs: des entiers, des conteneurs, des pointeurs, des chaînes de caractères, des objets haut niveau (clients, produits, formes, etc.)

Les fonctions lambda est un principe permettant de stocker une routine (ou une méthode dans un contexte objet) dans une variable. De la même manière qu’une variable contenant toute sorte de valeurs permet de partager ces valeurs d’une instance du système à une autre, une fonction lambda permet également de partager une routine d’une instance à une autre. Cette mécanique est donc très intéressante pour créer des structures d’événement, tout en gardant un faible couplage entre les différents éléments du système (par exemple, entre les classes en langage-objet par classes).

Cette mécanique, basée sur la théorie mathématique « Lambda Calcul », a été créée initialement par les langages fonctionnels. Il s’agit en fait d’un élément primordial des langages comme Lisp, Haskell, OCaml, Scheme ou même JavaScript. Si vous avez déjà utilisé JQuery avec Javascript, vous avez certainement déjà fait du code similaire à ceci:

mon_bouton.click(function () {
  alert("Bonjour tout le monde!");
});

On voit ici qu’on lance la méthode .click(...) de la variable mon_bouton (méthode qui indique à Javascript quel code il devra lancer lorsque l’utilisateur clique sur le bouton). Le bout de code qui nous intéresse ici, c’est l’argument qui est envoyé à cette méthode click:

function () {
  alert("Bonjour tout le monde!");
}

On voit ici que ce qui est envoyé comme valeur d’argument à la méthode .click(...) est… une fonction en tant que telle. La méthode .click(...) pourra stocker la fonction dans une variable accessible à l’intérieur du bouton et ce dernier pourra lancer la fonction lorsque l’utilisateur appuiera sur ce bouton. On voit qu’on peut donc créer une structure d’événement (lancer une certaine routine lorsque l’utilisateur clique sur un bouton), sans avoir à utiliser de mécanique de « Listener » comme utilisé en Java.

Ce qui est intéressant avec cette mécanique, c’est qu’elle permet à la classe bouton de lancer une méthode d’une autre classe sans avoir de dépendance. En d’autres mots, si on a le diagramme suivant:

On a que le contrôleur connait le bouton (il a une dépendance), mais le bouton ne connait pas le contrôleur (il est indépendant). Par contre, malgré cette indépendance, le bouton peut tout de même lancer une méthode du contrôleur. C’est le contrôleur qui doit ajouter sa méthode dans le bouton en utilisant la méthode « click ». Le bouton contient, dans une variable, la méthode à lancer, mais sans savoir dans quelle classe cette méthode se trouve.

Cette mécanique nous permet d’avoir les mêmes avantages que le patron conceptuel Observeur ou que les Listener, sans avoir les inconvénients. En effet, aucune interface (ou classe abstraite) n’est nécessaire et on peut utiliser n’importe quelle méthode, peu importe le nom qu’on lui donne (contrairement aux Listeners ou aux Observeurs qui imposent le nom des méthodes lancés).

Les Agents en Eiffel

Dans le langage Eiffel, le lambda calcul est implémenté en utilisant le mot clé « agent ». Un « agent » permet de prendre une méthode et de la transformer en objet, permettant ainsi de l’utiliser comme valeur dans une variable (ou attribut, argument, etc.) L’objet créée est d’un type descendant de ROUTINE. Une ROUTINE peut être autant de types FUNCTION ou PROCEDURE (ou PREDICATE, mais nous ne toucherons pas à cette classe dans ce cours). Je rappelle qu’une fonction est une routine qui retourne une valeur et une procédure est une routine qui ne retourne pas de valeur.

Les fonctions

Initialement, nous créons une méthode de manière conventionnelle. Par exemple, voici une méthode qui fait une addition de deux nombres et qui retourne la somme:

addition(a, b:INTEGER):INTEGER
	do
		Result := a + b
	end

Il est important de comprendre que ce n’est qu’un exemple pour vous faire comprendre la mécanique des « agents ». Voici maintenant la création d’une variable de type FUNCTION en utilisant un « agent »:

make
	local
		l_addition:FUNCTION[TUPLE[a, b:INTEGER], INTEGER]
	do
		l_addition := agent addition
		print("Le resultat: " + l_addition.item(3, 5).out + "%N")
	end

Ce code affichera le résultat suivant:

Le resultat: 8

Sachez également que dans les dernières versions d’EiffelStudio, il est possible d’utiliser la variable, comme s’il s’agissait d’une méthode en soi. C’est-à-dire que nous pouvons éviter d’utiliser le « item ». Par exemple:

make
	local
		l_addition:FUNCTION[TUPLE[a, b:INTEGER], INTEGER]
	do
		l_addition := agent addition
		print("Le resultat: " + l_addition(3, 5).out + "%N")
	end

donnera également le résultat:

Le resultat: 8

Il est également possible de « pré-assigner » des arguments lors de la création de l’agent. Pour ce faire, nous ajoutons directement les arguments « pré-assignés » dans la création de l’agent et nous laissons les autres arguments en utilisant le caractère « ? ». Par exemple:

make
	local
		l_incrementation:FUNCTION[TUPLE[valeur:INTEGER], INTEGER]
	do
		l_incrementation := agent addition(?, 1)
		print("Le resultat: " + l_incrementation(8).out + "%N")
	end

Le code donnera le résultat suivant:

Le resultat: 9

Dans le cas où il n’y a pas d’argument à la FUNCTION, on doit utiliser .item([]) (ou sans le « .item ») pour lancer l’exécuter. Par exemple:

make
	local
		l_5:FUNCTION[TUPLE, INTEGER]
	do
		l_5 := agent addition(3, 2)
		print("Le resultat: " + l_5.item([]).out + "%N")
	end

ou

make
	local
		l_5:FUNCTION[TUPLE, INTEGER]
	do
		l_5 := agent addition(3, 2)
		print("Le resultat: " + l_5([]).out + "%N")
	end

Dans les deux cas, nous obtenons le résultat:

Le resultat: 5

Les procédures

L’utilisation d’une procédure se fait de la même manière, mais en utilisant « .call » au lieu de « .item » et sans valeur de retour.

Par exemple, voici une méthode standard que nous utiliserons dans notre exemple:

afficher_addition(a, b:INTEGER)
	do
		print("La somme est: " + (a + b).out + "%N")
	end

Voici comment on utilise la procédure sous la forme d’agent:

make
	local
		l_afficher:PROCEDURE[TUPLE[a, b:INTEGER]]
	do
		l_afficher := agent afficher_addition
		l_afficher.call(3, 5)
	end

Le résultat du code est:

La somme est: 8

On peut également lancer l’agent sans utiliser le « .call ». Par exemple:

make
	local
		l_afficher:PROCEDURE[TUPLE[a, b:INTEGER]]
	do
		l_afficher := agent afficher_addition
		l_afficher(3, 5)
	end

De la même manière que pour les fonctions, il y a la possibilité de « pré-assigner » des arguments en utilisant le caractère « ? » lors de la création de l’agent:

make
	local
		l_afficher:PROCEDURE[TUPLE[valeur:INTEGER]]
	do
		l_afficher := agent afficher_addition(1, ?)
		l_afficher(8)
	end

Le résultat de ce code est:

La somme est: 9

Finalement, toujours de manière similaire au fonction, lorsque nous lançons une procédure sous la forme d’agent sans argument, il faut utiliser « .call([]) » (avec ou sans le « .call »). Par exemple:

make
	local
		l_afficher:PROCEDURE[TUPLE]
	do
		l_afficher := agent afficher_addition(2, 3)
		l_afficher.call([])
	end

ou:

make
	local
		l_afficher:PROCEDURE[TUPLE]
	do
		l_afficher := agent afficher_addition(2, 3)
		l_afficher([])
	end

Les agents en ligne

Il est possible de spécifier directement le code qu’exécutera un agent dans la définition de l’agent. Voici un exemple avec une fonction:

make
	local
		l_addition:FUNCTION[TUPLE[a, b:INTEGER], INTEGER]
	do
		l_addition := agent (a, b:INTEGER):INTEGER
						do
							Result := a + b
						end
		print("Le resultat: " + l_addition(3, 5).out + "%N")
	end

Le résultat donne:

Le resultat: 8

Et un autre exemple avec une procédure:

make
	local
		l_addition:PROCEDURE[TUPLE[a, b:INTEGER]]
	do
		l_addition := agent (a, b:INTEGER)
						do
							print("La somme est: " + (a + b).out + "%N")
						end
		l_addition(3, 5)
	end

Qui donne le résultat:

La somme est: 8

Les agents de classes

Il est possible de créer un « agent » à partir d’une méthode de classe. Le premier argument de cette méthode est un argument du type de la classe qui contenait la méthode à l’origine.

Par exemple, avec une procédure:

make
		-- Exécute l'application
	local
		l_to_upper:PROCEDURE[TUPLE[STRING]]
		l_texte:STRING
	do
		l_to_upper := agent {STRING}.to_upper
		l_texte := "Un Texte"
		l_to_upper(l_texte)
		print(l_texte + "%N")
	end

Ce code donne le résultat:

UN TEXTE

Ensuite, avec une fonction:

make
		-- Exécute l'application
	local
		l_head:FUNCTION[TUPLE[STRING, INTEGER], STRING]
	do
		l_head := agent {STRING}.head
		print("Les 4 premiers caracteres: " +
				l_head("Bonjour tout le monde", 4) + "%N")
	end

Ce code donne le résultat:

Les 4 premiers caracteres: Bonj

Les méthodes d’action de boucle

Il est très intéressant d’utiliser les agents dans un contexte de boucle, car ça permet d’encapsuler la boucle à l’intérieur de la classe de liste. En d’autres mots, au lieu de faire la boucle nous-mêmes, dans notre programme, on indique à la liste l’action qu’on veut exécuter et la liste s’occupe d’exécuter la boucle.

Il existe 3 méthodes d’action dans la classe LIST:

    • do_all: Permets d’exécuter un agent pour chaque élément de la liste;
    • for_all: Test avec une fonction booléenne (sous forme d’agent) que tous les éléments retournent Vrai;
    • do_if: Pour tous les éléments de la liste, exécute un agent sur un élément de la liste, seulement si un test avec une fonction booléenne (sous forme d’agent) retourne Vrai.

Voici des exemples d’utilisation des méthodes d’action de liste:

make
	local
		l_liste:ARRAYED_LIST[STRING]
	do
		create l_liste.make (5)
		l_liste.extend ("Ligne 1")
		l_liste.extend ("Ligne 2")
		l_liste.extend ("Ligne 3")
		l_liste.extend ("Ligne 4")
		l_liste.extend ("Ligne 5")
		l_liste.do_all (agent (l_string:STRING)
						do
							print(l_string + "%N")
						end)
	end

Ce code donne le résultat suivant:

Ligne 1
Ligne 2
Ligne 3
Ligne 4
Ligne 5

Un autre exemple qui permet rapidement de valider les entrées des utilisateurs:

make
		-- Exécute l'application
	local
		l_liste:ARRAYED_LIST[STRING]
	do
		create l_liste.make (5)
		l_liste.extend ("1")	-- Simule l'entrée de données
		l_liste.extend ("Allo")	-- par l'utilisateur
		l_liste.extend ("2")
		l_liste.extend ("3")
		l_liste.extend ("Bonjour")
		executer_programme(l_liste)
	end

executer_programme(a_liste:LIST[STRING])
		-- Lance le programme seulement si tous les éléments sont entiers
	do
		if a_liste.for_all (agent {STRING}.is_integer) then
			-- Tous les éléments sont des entiers valides. Procédons
		else
			a_liste.do_if (agent (a_string:STRING)
								do
									print("La valeur: " + a_string + " n'est pas un entier valide%N")
								end
							, agent (a_string:STRING):BOOLEAN
								do
									Result := not a_string.is_integer
								end
						)
		end
	end

Les lambdas en Java

Le langage Java a également sa version des fonctions lambda. Malheureusement, très peu de mécanismes inclue dans les librairies de base de java utilisent ou permette l’utilisation des fonctions lambda. La raison pour cette absence est que les fonctions lambda ont fait leur apparition dans le langage Java à partir de la version 8 et que les librairies de bases ont été faites bien avant cette version.

De plus, les fonctions lambda en Java sont beaucoup moins structurées que ce que nous avons vu en Eiffel avec les « agents ». En effet, l’implémentation des fonctions lambda en Java est basée sur le principe des langages fonctionnels qui ne sont généralement pas typés. Donc, le système de fonctions lambda en Java ne respecte pas le paradigme de programmation orientée objet par classe, mais bien celui de programmation orientée objet par prototype. En d’autres mots, il n’y a pas de classe telle que Methode, Procedure ou Function généré à partir de la syntaxe des lambdas en Java. Un lambda génère d’une certaine façon un objet qui n’a pas de type. Inversement, afin de faire en sorte que ces fonctions lambda soient utilisables avec le reste du langage Java, les concepteurs ont fait en sorte que les fonctions lambda peuvent générer des objets qui se conforment à certaines interfaces.

Pour créer une fonction lambda, il faut utiliser la syntaxe:

(TypeArgument1 argument1, TypeArgument2 argument2, ...) -> code

Voici un exemple. Premièrement, prenons la classe Personne suivante qui nous servira dans notre exemple:

public class Personne {
    private int age;

    private String nom;

    public void setAge(int aAge) {
        age = aAge;
    }

    public int getAge() {
        return age;
    }

    public void setNom(String aNom) {
        nom = aNom;
    }

    public String getNom() {
        return nom;
    }

    public Personne(int aAge, String aNom) {
        setAge(aAge);
        setNom(aNom);
    }
}

Ensuite, prenons le programme suivant:

public class Programme {

    private interface CheckPersonne {
       public boolean test(Personne aPersonne);
    } 

    public Programme() {
        CheckPersonne ma_lambda = (Personne p) -> p.getAge() > 18;
        Personne personne1 = new Personne(7, "Alice");
        Personne personne2 = new Personne(900, "Yoda");
        if(ma_lambda.test(personne1)){
            System.out.println(personne1.getNom() + " a plus de 18 ans");
        }
        if(ma_lambda.test(personne2)){
            System.out.println(personne2.getNom() + " a plus de 18 ans");
        }
    }

    public static void main(String[] aArguments) {
        new Programme();
    }
}

On voit ici que la fonction lambda est placée dans une variable de Type CheckPersonne qui contient la fonction test(). Pour utiliser la fonction lambda, nous devons utiliser cette fonction test(). Le résultat de ce code donnera:

Yoda a plus de 18 ans.

On peut varier le type de retour de la fonction lambda en modifiant l’interface et en faisant en sorte que la lambda retourne le bon type. On peut également créer une fonction lambda qui ne retourne aucune valeur. Par exemple:

public class Programme {

    private interface PrintPersonne {
       public void run(Personne aPersonne);
    } 

    public Programme() {
        PrintPersonne ma_lambda = (Personne p) -> System.out.println(
                "L'age de " + p.getNom() + " est de " + p.getAge() + " ans.");
        Personne personne1 = new Personne(7, "Alice");
        ma_lambda.run(personne1);
    }

    public static void main(String[] aArguments) {
        new Programme();
    }
}

Le résultat de ce programme code est:

L'age de Alice est de 7 ans.

Lambda sur des listes

Un peu comme dans le cas des actions de boucle dans Eiffel, il est possible d’utiliser les lambdas dans le contexte d’une liste afin d’indiquer à la liste une action à effectuer sur chaque élément de la liste. Pour ce faire, on utilise la méthode forEach contenue dans toutes les List. Voici un exemple:

public class Programme {

    private static void printPersonne(Personne aPersonne) {
        System.out.println("L'age de " + aPersonne.getNom() + " est de " + 
                aPersonne.getAge() + " ans.");
    }

    public Programme() {
        List<Personne> lPersonnes = new LinkedList<Personne>();
        lPersonnes.add(new Personne(7, "Alice"));
        lPersonnes.add(new Personne(900, "Yoda"));
        lPersonnes.forEach((Personne p) -> printPersonne(p));
    }


    public static void main(String[] aArguments) {
        new Programme();
    }
}

Le résultat de cet exemple est:

L'age de Alice est de 7 ans.
L'age de Yoda est de 900 ans.

Retour


Auteur: Louis Marchand
Creative Commons License
Sauf pour les sections spécifiées autrement, ce travail est sous licence Creative Commons Attribution 4.0 International.