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.
Auteur: Louis Marchand
Sauf pour les sections spécifiées autrement, ce travail est sous licence Creative Commons Attribution 4.0 International.