Les closures

Lecture 17 min. ‱


Table des matiĂšres

Bonjour à toutes et à tous 😀

Rust est un langage qui possĂšde tout une collection de concepts permettant une grande variĂ©tĂ© d’usages.

Parmis ceux-ci, je voudrai vous parler d’un en particulier: les closures.

Qu’est-ce qu’une closure ?

Une closure ou “fermeture” est un concept gĂ©nĂ©ral en informatique qui dĂ©finit un contexte d’exĂ©cution (gĂ©nĂ©ralement une fonction) fermĂ© qui possĂšde son propre environnement.

C’est pas clair? C’est normal ^^

Nous allons y aller progressivement comme d’habitude pour saisir toutes les subtilitĂ©s.

Pointeur de fonction

Premier contact

Partons de la closure la plus simple possible.

let closure = || {};

Deux choses sont Ă  remarquer:

Si l’on tente de debug la closure, on obtient une erreur et un conseil du compilateur.

let closure = || {};
                 -- consider calling this closure
dbg!(closure);
^^^^^^^^^^^^^ `{closure}` cannot be formatted using `{:?}` because it doesn't implement `Debug`

La closure elle-mĂȘme n’est pas debuggable mais par contre, elle est appelable.

Appellons-la

dbg!(closure())

Cette fois-ci, nous obtenons bien un résulat

closure() = ()

Bon, on progresse, certe lentement mais on progresse.

Quel est le type de closure ?

Ça c’est une qustion intĂ©ressante.

Il en a deux, mais je réserve le deuxiÚme pour une partie qui lui sera dédié.

Le premier type est ce que l’on appelle un pointeur de fonction.

Dans notre cas, nous pouvons l’écrire de deux façons diffĂ©rentes:

let closure : fn() -> () = || {};

ou en passant sous silence le type de retour

let closure : fn() = || {};

On voit que la syntaxe est extrĂȘmement similaire Ă  ce que l’on utilise dĂ©jĂ  lors de la dĂ©claration de fonctions.

fn closure() -> () {
    ()
}

fn closure() {
    ()
}

fn closure() {}

On commence par dĂ©clarer une fonction avec l’opĂ©rateur fn, puis les paramĂštres (ici aucun), entre paranthĂšses, puis une flĂšche -> et finalement le type de retour, ici unit ().

Et c’est parfaitement normal car c’est le mĂȘme mĂ©canisme qui est Ă  l’oeuvre. Le code gĂ©nĂ©rĂ© sera le mĂȘme.

pour les curieux et curieuses

En fait closure est une adresse quelque part dans la mémoire.

On peut demander à Rust de nous l’afficher via

dbg!(&closure as *const _); //  &closure as *const _ = 0x00000010baaff410

Si l’on veut non pas l’addresse de closure mais l’adresse du code de la closure pointĂ© par la variable closure on peut Ă©crire quelque chose comme cela

dbg!(closure as fn()); // closure as fn() = 0x00007ff7ca7c1c80

Temporiser un retour

Ne rien renvoyer n’est pas trùs excitant, essayons de lui faire retourner quelque chose, un 42 par exemple.

let universal_answer : fn() -> u8 = || 42;

Je vous donne son type fn() -> u8, une fonction qui ne prend pas de paramĂštre et qui renvoit un u8 et toujours le mĂȘme 42.

Ce qui signifie que l’on a enfermĂ© dans une variable une valeur que l’on peut rĂ©cupĂ©rer Ă  tout moment.

dbg!(universal_answer()); // universal_answer() = 42

Autrement dit, nous avons temporiser l’appel de la fonction dans une variable.

Cette variable est appelable autant de fois que l’on dĂ©sire.

dbg!(universal_answer()); // universal_answer() = 42
dbg!(universal_answer()); // universal_answer() = 42
dbg!(universal_answer()); // universal_answer() = 42

Paramétrisation simple

Bon c’est pas tout ça mais notre fonction retourne quelque chose mais tout le temps la mĂȘme chose.

C’est pas trĂšs intĂ©ressant.

DĂ©clarons une autre closure, nous allons l’appeler identity, elle renverra ce que l’on lui dĂ©fini Ă  l’appel de la closure.

Pour cela nous allons introduire une syntaxe

let identity = |x: u8| x;

Pour dĂ©finir un paramĂštre il faut crĂ©er ceux-ci entre les barres verticales. Ensuite c’est la syntaxe classique qui s’applique: on dĂ©fini le nom du paramĂštre suivi de son type sĂ©parĂ© par deux :.

Pour l’appeler c’est le mĂȘme fonctionnement qu’une fonction standard

dbg!(identity(42)); // identity(42) = 42

On peut également la typer explicitement:

let identity : fn(u8) -> u8 = |x : u8| x;

Dans cette syntaxe il est alors possible de ne pas redéfinir le type du paramÚtre, car la signature le contient déjà.

let identity : fn(u8) -> u8 = |x| x;

Il est mĂȘme possible de ne pas donner explicitement le type de retour et laisser Rust se dĂ©brouiller.

let identity : fn(u8) -> _ = |x| x;

À l’inverse le type de retour peut ĂȘtre dĂ©fini dans la dĂ©claration du la closure.

let identity = |x| -> u8 { x }; // les accolades sont obligatoires

Toutes ces syntaxes alternatives peuvent sembler superflues, mais dans la pratique, elles auront presque toutes un intĂ©rĂȘt en fonction du contexte de leur utilisation.

Résult attendu, mais toujours pas transcendant.

Essayons de lui donner un comportement plus évolué.

let add_10 = |x| x + 10; // je laisse Rust typer comme un grand

Nous avons maintenant une closure qui ajoute 10 Ă  ce que l’on lui donne entrĂ©.

dbg!(add_10(1)); // 11
dbg!(add_10(1000)); // 1010
dbg!(add_10(100000000)); // 100000010

Attention

Ce n’est pas de la gĂ©nĂ©ricitĂ©!

Le premier appel fixe le type.

Si c’est integer_u8 en premier, le type sera u8, si c’est l’inverse, ça sera u64.

let add_10 = |x| x + 10;

let integer_u64: u64 = add_10(565345345341354154151);
let integer_u8: u8 = add_10(2);

Provoque

let integer_u8: u8 = add_10(2);
                     ^^^^^^^^^ expected `u8`, found `u64`

Ceci est dû car le code effectivement compilé sera

let add_10 : fn(u64) -> u64 = |x| x + 10;

ParamĂštres multiples

Et pourquoi pas deux paramĂštres ?

let add = |x : u8, y: u8| x + y;

Maintenant nous commençons avoir quelque chose d’intĂ©ressant.

dbg!(add(5, 2)) // add(5, 2) = 7

On peut alors créer des comportement différents.

let mul = |x : u8, y: u8| x * y;
dbg!(mul(5, 2)) // mul(5, 2) = 10

Tous les paramĂštres n’ont pas besoin d’ĂȘtre du mĂȘme type et vous pouvez en mettre autant que vous voulez.

|x: u8, y: f32, z: bool| {...}

Comme paramĂštre de fonction

Si une closure a un type, alors elle peut ĂȘtre passĂ© en paramĂštre d’une autre fonction.

fn run_closure(closure: fn(u8, u8) -> u8, x : u8, y: u8) {
    dbg!(closure(x, y));
}

On se contente de reprendre la signature que l’on a dĂ©fini plus haut.

On perd ici le nom de la closure, mais sa signature Ă©tant correcte, Rust n’a pas rĂ©ellement besoin de savoir si c’est une addition ou une multiplication pour laisser le CPU l’éxĂ©cuter. Tout ce qu’il a besoin de savoir c’est quels sont le nombre et le type d’entrĂ©es et quel est le type de retour. Le reste est de la cuisine interne Ă  la closure.

Pour l’appeler c’est aussi simple que de passer un paramùtre quelconque.

let add = |x, y| x + y;
let mul = |x, y| x * y;
run_closure(add, 5, 2); // 7
run_closure(mul, 5, 2); // 10

Vous pouvez mĂȘme vous abstenir de dĂ©finir la variable tout court

run_closure(|x, y| x + y, 5, 2); // 7
run_closure(|x, y| x * y, 5, 2); // 10

Variations

Tout que l’on a dĂ©fini jusqu’à maintenant peut l’ĂȘtre de 3 façons diffĂ©rentes qui amĂ©nent toutes au mĂȘme code gĂ©nĂ©rĂ©.

La forme closure

fn main() {
    let add_closure = |x: u8, y: u8| x + y;
    dbg!(add_closure(5, 2));
}

La forme fonction locale

fn main() {
    fn add_local(x: u8, y: u8) -> u8 {
        x + y
    }
    dbg!(add_local(5, 2));
}

Ou mĂȘme la forme, fonction classique

fn add_global(x: u8, y: u8) -> u8 {
    x + y
}

fn main() {
    dbg!(add_global(5, 2));
}

Pour le moment toutes ces écritures sont équivalentes.

En effet nous pouvons Ă©crire ceci et c’est parfaitement valide.

run_closure(add_global, 5, 2);
run_closure(add_local, 5, 2);
run_closure(add_closure, 5, 2);
run_closure(|x, y| x + y, 5, 2);

La seul chose qui diffĂšre entre toutes Ă©criture est validitĂ© d’existence de leur dĂ©claration.

Comme retour de fonction

Dans l’informatique il y a un terme qui se dit High Order Function. Moins pompeusement appelĂ©, il s’agit d’une fonction qui retourne une fonction.

fn return_universal_awswer() -> fn() -> u8 {
    || 42
}

Le type de retour sera alors fn() -> u8.

Lorsque l’on appelera cette fonction, ce n’est non pas 42 qui sera retournĂ© mais quelque chose qui retournera 42 lorsque l’on l’appelera.

fn main() {
    let future_universal_answer = return_universal_awswer();
    dbg!(future_universal_answer()); // 42
}

Closure

Vous allez voir que depuis tout Ă  l’heure, nous n’avons pas utilisĂ© une seule closure, mais juste un pointeur de fonction.

Il est temps de se plonger dans le vrai fonctionnement des closures. 😝

Faire référence

Bien commençons Ă  donner un peu d’intĂ©rĂȘt Ă  notre notation sous la forme de closure.

let a = 20;
let add_with_ref_value = |x: u8, y: u8| x + y + &a;
dbg!(add_with_ref_value(5, 2)); // 27

Pour les personnes qui se demandent

On remarque ici, que Rust permet d’additionner des u8 et des &u8 grĂące Ă  l’implĂ©mentation par dĂ©faut ci-dessous

impl<'a> Add<i32> for &'a i32 {}
// et
impl Add<&i32> for i32 {}

Si on essaie de faire la mĂȘme chose avec la fonction locale, nous avons dĂ©jĂ  un souci.

let a = 20;
fn add_local(x: u8, y: u8) -> u8 {
    x + y + &a
}

Le compilo n’est pas content et un peu Ă©nigmatique.

can't capture dynamic environment in a fn item

         x + y + &a
                  ^

= help: use the `|| { ... }` closure form instead

can't capture dynamic environment in a fn item, ce mot de capture est trĂšs important pour la suite.

La force d’une closure par rapport à une fonction c’est qu’elle capture l’environnement. Donc clairement la notation closure est la voie royale pour faire ce dont on a besoin.

Muter une référence

Il est également possible de modifier une référence dans une closure.

let mut a = 2;
let double_ref = || *&mut a *= 2; // peut-ĂȘtre Ă©crit aussi comme a *= 2
double_ref();

Ah échec!

cannot borrow `double_ref` as mutable, as it is not declared as mutable
    let double_ref = || *&mut a *= 2;
                              - calling `double_ref` requires mutable binding due to mutable borrow of `a`
    double_ref();
    ^^^^^^^^^^ cannot borrow as mutable
consider changing this to be mutable
    let mut double_ref = || *&mut a *= 2;
        +++

Mais comme d’habitude, le langage nous apprend ce qu’il faut, il suffit de lire. 😊

On remplace le let double_ref par un let mut double_ref.

let mut a = 2;
let mut double_ref = || *&mut a *= 2;
double_ref();
dbg!(a); // 4

Maintenant, cela compile. 🎉

Nous sommes désormais sur les terres du Borrow Checker, nous allons devoir nous plier à ses rÚgles.

Borrow Checker

Pour rappel:

  • il ne peut pas y avoir plusieurs rĂ©fĂ©rences mutables sur un mĂȘme espace mĂ©moire
  • si une rĂ©fĂ©rence mutable est active, alors il ne peut pas y avoir d’autre rĂ©fĂ©rence immutables sur l’espace mĂ©moire rĂ©fĂ©rencĂ©
  • si une rĂ©fĂ©rence est dĂ©fini, elle doit ĂȘtre valide

Bon allons titiller le MaĂźtre de Maison.

Tout d’abord, commençons avec les rĂ©fĂ©rences mutables, multiples.

let mut a = 2;
let mut double_ref = || *&mut a *= 2;
let mut triple_ref = || *&mut a *= 3;
double_ref();

Ça explose comme prĂ©vu đŸ’„

cannot borrow `a` as mutable more than once at a time

let mut double_ref = || *&mut a *= 2;
                     --       - first borrow occurs due to use of `a` in closure
                     |
                     first mutable borrow occurs here
let mut triple_ref = || *&mut a *= 3;
                     ^^       - second borrow occurs due to use of `a` in closure
                     |
                     second mutable borrow occurs here
double_ref();
---------- first borrow later used here

Et comme prĂ©vu c’est le fait de borrow mut plusieurs fois qui l’ennuie.

Si on fait un petit dessin cela donne ceci

A droite des rectangle double_ref et triple_ref, j’ai dĂ©fini les dĂ©but et fin de vie des closures.

A gauche j’ai exprimĂ© les lignes de vie des borrow mut.

On voit ici que double ref impose que sa rĂ©fĂ©rence soit valide de la dĂ©claration de closure jusqu’à la fin de son exĂ©cution.

Autrement dit, nous avons deux rĂ©fĂ©rences mutables pointant sur une mĂȘme zone mĂ©moire pendant un instant, ici reprĂ©sentĂ© en hachurĂ©.

MĂȘme si, triple_ref n’est jamais appelĂ©, Rust Ă  un doute, et quand il a un doute il interdit.

Deux maniÚres de résoudre le problÚme.

Soit on appelle jamais, mais ce n’est pas intĂ©ressant.

let mut a = 2;
let mut double_ref = || *&mut a *= 2;
let mut triple_ref = || *&mut a *= 3;
dbg!(a); // 2

Et alors les lignes de vie ne se croisent jamais.

Soit on place astucieusement nos appels et déclaration pour ne pas tomber hors la loi;

let mut a = 2;
let mut double_ref = || *&mut a *= 2;
double_ref();
let mut triple_ref = || *&mut a *= 3;
triple_ref();
dbg!(a); // 12

LĂ  encore pas de croisement.

Si nous tentons d’appeler add_with_ref_value en ayant dĂ©clarĂ© double_ref aprĂšs add_with_ref_value

let mut a = 2;
let add_with_ref_value = |x: u8, y: u8| x + y + &a;
let mut double_ref = || *&mut a *= 2;
add_with_ref_value(2, 5);

Instantannément cela explose

cannot borrow `a` as mutable because it is also borrowed as immutable

let add_with_ref_value = |x: u8, y: u8| x + y + &a;
                         --------------          -
                         |                       |
                         |                       first borrow occurs due to use of `a` in closure
                         |
                         immutable borrow occurs here
let mut double_ref = || *&mut a *= 2;
                     ^^       - second borrow occurs due to use of `a` in closure
                     |
                     mutable borrow occurs here
add_with_ref_value(2, 5);
------------------ immutable borrow later used here

Clairement, il est pas d’accord, et effectivement, il y a un croisement.

La rĂ©fĂ©rence non mutable Ă©tant toujours active Ă  la dĂ©claration de double_ref, il lui est impossible d’acquĂ©rir une rĂ©fĂ©rence mutable sur a, mĂȘme si double_ref ne sera jamais appelĂ©.

Pour rĂ©soudre la situation, il suffit d’intervertir les dĂ©clarations.

let mut a = 2;
let mut double_ref = || *&mut a *= 2;
let add_with_ref_value = |x: u8, y: u8| x + y + &a;
dbg!(add_with_ref_value(2, 5)); // 9
dbg!(a); // 2

Les lignes de vie ne croisant plus, le borrow checker est heureux. 😇

Maintenant, si nous voulons appeler les deux closures, nous allons avoir un souci.

let mut a = 2;
let mut double_ref = || *&mut a *= 2;
let add_with_ref_value = |x: u8, y: u8| x + y + &a;
add_with_ref_value(2, 5);
double_ref();
cannot borrow `a` as immutable because it is also borrowed as mutable

let mut double_ref = || *&mut a *= 2;
                     --       - first borrow occurs due to use of `a` in closure
                     |
                     mutable borrow occurs here
let add_with_ref_value = |x: u8, y: u8| x + y + &a;
                         ^^^^^^^^^^^^^^          - second borrow occurs due to use of `a` in closure
                         |
                         immutable borrow occurs here
add_with_ref_value(2, 5);
double_ref();
---------- mutable borrow later used here

Le dessin montre bien que la ligne de vie des références de double_ref englobe complÚtement celle des références utilisées par add_with_ref_value.

Intervertir les appels des closures.

Ou les déclarations des closures.

Ne résout pas la situation, il reste toujours une intersection.

Finalement la solution est de déclarer successivement les closures et de les appeler.

Soit dans un sens

let mut a = 2;
let mut double_ref = || *&mut a *= 2;
double_ref();
let add_with_ref_value = |x: u8, y: u8| x + y + &a;
dbg!(add_with_ref_value(2, 5)); // 11
dbg!(a); // 4

Soit dans l’autre

let mut a = 2;
let add_with_ref_value = |x: u8, y: u8| x + y + &a;
dbg!(add_with_ref_value(2, 5)); // 9
let mut double_ref = || *&mut a *= 2;
double_ref();
dbg!(a); // 4

Dans les deux cas, la rĂ©fĂ©rence mutable est la seule active lorsqu’elle est active.

Retourner une closure

Vous souvenez des High Order Function ?

Nous allons les rendre bien plus intéressantes.

Nous allons créer une fonction qui mémorise un paramÚtre et qui utilise ce paramÚtre dans le calcul.

On veut:

let adder_bias_10 = adder_with_bias(10);
let adder_bias_20 = adder_with_bias(20);
adder_bias_10(2, 5); // 17
adder_with_bias(10)(4, 5); // 19
dder_bias_20(2, 5); // 27

Vous voyez un peu l’idĂ©e. 🙂

En voici une implémentation.

fn adder_with_bias(bias: u8) -> fn(u8, u8) -> u8 {
    |x, y| x + y + bias
}

Mais malheureusement, cela ne se passe pas aussi bien que prĂ©vu 🧹

mismatched types

 fn return_adder_with_bias(bias: u8) -> fn(u8, u8) -> u8 {
                                        ---------------- expected `fn(u8, u8) -> u8` because of return type
     |x, y| x + y + bias
     ^^^^^^^^^^^^^^^^^^^ expected fn pointer, found closure

 note: expected fn pointer `fn(u8, u8) -> u8`
             found closure `{closure@src\main.rs:110:5: 110:11}`
 closures can only be coerced to `fn` types if they do not capture any variables

     |x, y| x + y + bias
                    ^^^^ `bias` captured here

Bien des choses sont intéressantes ici:

Bon plus qu’à comprendre comment retourner la closure.

Le compilateur n’est malheureusement pas assez explicite sur la maniĂšre de rĂ©soudre la situation.

Donc dans le cas prĂ©sent le seul moyen est de le savoir Ă  l’avance. 😐

Pour renvoyer une closure d’une qui a pour signature fn(u8, u8) -> u8 , il faut renvoyer une implĂ©mentation du trait Fn(u8, u8) -> u8.

On reviendra en détail sur les implication de ce trait trÚs en détail dans la partie suivante.

fn adder_with_bias(bias: u8) -> impl Fn(u8, u8) -> u8 {
    |x, y| x + y + bias
}

Hum, presque, l’erreur a changĂ© au moins. đŸ€·â€â™‚ïž

error: closure may outlive the current function, but it borrows `bias`, which is owned by the current function

|x, y| x + y + bias
^^^^^^         ---- `bias` is borrowed here
|
may outlive borrowed value `bias`

help: to force the closure to take ownership of `bias`, use the `move` keyword

move |x, y| x + y + bias
++++

Décortiquons ce qui est raconté:

En Rust ce qui est copiable sera copiĂ© lorsqu’elle doit ĂȘtre dĂ©placĂ©.

Notre bias: u8 sera donc copié.

Une closure est dĂ©finitivement bien plus qu’une fonction et semble avoir son propre contexte d’exĂ©cution.

Finalement cela nous donne la fonction suivante:

fn adder_with_bias(bias: u8) -> impl Fn(u8, u8) -> u8 {
    move |x, y| x + y + bias
}

Et ça compile enfin! 🎉

Référence et capture

Maintenant question:

Pourquoi cette implémentation fonctionne sans move?

fn main() {
    let bias_10 = 10;
    let adder = |x, y| x + y + bias_10;
    adder(5, 2) // 17
}

Alors que celle-ci échoue ?

fn main() {
    let adder = adder_with_bias(10);
    adder(5, 7)
}

fn adder_with_bias(bias: u8) -> impl Fn(u8, u8) -> u8 {
    |x, y| x + y + bias
}

Tout d’abord, il faut savoir que le comportement naturelle d’une closure est de rĂ©fĂ©rencĂ© son environnement et non de le capturer.

On va Ă©galement revenir sur ce terme de “capture”

Donc lorsque l’on Ă©crit:

fn main() {
    let bias_10 = 10;
    let adder = |x, y| x + y + bias_10;
    adder(5, 2) // 17
}

C’est plus cela qui est rĂ©ellement dĂ©fini

fn main() {
    let bias_10 = 10;
    let adder = |x, y| x + y + &bias_10;
    adder(5, 2) // 17
}

Ici pas de souci, bias_10 est défini tout au long de la vie de adder.

Revenons Ă  notre retour de closure.

fn adder_with_bias(bias: u8) -> impl Fn(u8, u8) -> u8 {
    |x, y| x + y + bias
}

Est plutÎt cette définition

fn adder_with_bias(bias: u8) -> impl Fn(u8, u8) -> u8 {
    |x, y| x + y + &bias
}

Vous sentez l’arnaque arriver ?

Le fameux problĂšme de “on ne peut pas dĂ©finir un Ă©lĂ©ment et en renvoyer une rĂ©fĂ©rence”

fn fail<'a>() -> &'a u8 { // lifetime obligatoire
    let a = 12;
    &a // erreur de borrow : returns a reference to data owned by the current function
}

bias est défini dans le contexte de la fonction adder_with_bias. La closure vient référencer bias.

Mais, il y a un soucis, la closure, sera appelée dans main, aprÚs le retour de adder_with_bias.

Or, bias n’existe plus à ce moment là.

Lorsque la closure est déplacé dans le main.

La référence &bias est donc désormais invalide et le Borrow Checker interdit ces situations.

Bon trÚs bien, et ça change quoi de rajouter un move devant la closure ?

fn adder_with_bias(bias: u8) -> impl Fn(u8, u8) -> u8 {
    move |x, y| x + y + bias
}

Le move modifie le comportement de la closure, au lieu de rĂ©fĂ©rencer son environnement, il va se l’apporoprier, le capturer.

Cela se manifest par le déplacement de bias dans la closure (ici on copie car u8 est copiable).

Lorsque la closure arrive dans le contexte de main, l’original de bias est dĂ©jĂ  mort, mais ce n’est pas grave, nous avons conservĂ© une copie.

La closure peut “s’auto-rĂ©fĂ©rencer” pour rĂ©cupĂ©rer bias. Le Borrow Checker est content. 😄

Fabriquer nos closures Ă  la main

Je sais pas vous mais moi, ce mĂ©lange de fonctions et de donnĂ©es qui s’auto-rĂ©fĂ©rence, ça me rappelle fortement un objet et donc une classe.

Nous sommes en Rust, donc ça sera une structure et une implĂ©mentation mais le concept sera le mĂȘme Ă  l’arrivĂ©.

struct ClosureAdderWithBias {
    bias: u8
}

impl ClosureAdderWithBias {
    pub fn call(&self, x: u8, y: u8) -> u8 {
        x + y + self.bias
    }
}

On peut alors créer une méthode qui en retourne une instance

fn adder_with_bias(bias: u8) -> ClosureAdderWithBias {
    ClosureAdderWithBias { bias }
}

Que l’on utilise ainsi

fn main() {
    let closure_with_bias_10 = adder_with_bias(10);
    let closure_with_bias_20 = adder_with_bias(20);
    closure_with_bias_10.call(5, 2); // 17
    closure_with_bias_10.call(4, 2); // 16
    closure_with_bias_20.call(4, 2); // 26
}

Sympa non ? đŸ€©

Et on peut également émuler le fonctionnement de la closure sans move.

fn main() {
    let bias_10 = 10;
    let adder = |x, y| x + y + &bias_10;
    adder(5, 2) // 17
}

Au lieu de créer une structure qui prend la propriété, nous allons juste référencer.

struct ClosureAdder<'a> {
    bias: &'a u8,
}

impl ClosureAdder<'_> {
    pub fn call(&self, x: u8, y: u8) -> u8 {
        x + y + self.bias
    }
}

La lifetime 'a commence Ă  nous montrer pourquoi le Borrow Checker n’a pas Ă©tĂ© content tout Ă  l’heure ^^

On peut alors l’utiliser ainsi 👇

fn main() {
    let bias_10 = 10;
    let adder_with_bias_10 = ClosureAdder { bias: &bias_10 };
    adder_with_bias_10.call(5, 2); // 17
}

Et maintenant la mutation pour en finir.

fn main() {
    let mut a = 10;
    let mut twice = || *&mut a *= 2;
    twice();
    dbg!(a); // 20
}

Cela nous donne alors cette implémentation par structure qui prend en paramÚtre une référence mutable.

struct ClosureTwice<'a> {
    value: &'a mut u8,
}

impl ClosureTwice<'_> {
    pub fn call(&mut self) {
        *self.value *= 2
    }
}

À l’utilisation

fn main() {
    let mut a = 10;
    let mut twice = ClosureTwice { value: &mut a };
    twice.call();
    dbg!(a); // 20
}

FnOnce, Fn, FnMut

Implémentation

C’est pas mal mais on sent qu’il manque quelque chose.

adder_with_bias retourne un impl Fn pas une structure custom.

fn adder_with_bias(bias: u8) -> impl Fn(u8, u8) -> u8 {
    move |x, y| x + y + bias
}

Mais du coup, pourquoi ne pas implémenter Fn sur notre ClosureAdderWithBias?

Alors oui mais c’est un peu, obscur ^^’

Vous n’ĂȘtes pas sensĂ© ĂȘtre lĂ  et le langage vous le fait sentir. 👿

DĂ©jĂ  vous avez l’obligation de passer en nightly. 🌜

rustup default nightly

Ensuite, il faut rajouter en haut du fichier cette directive

#![feature(unboxed_closures, fn_traits)]

Puis la signature de l’implĂ©mentation est bizarre.

Dans notre cas cela donne:

impl Fn<(u8, u8)> for ClosureAdderWithBias {
    extern "rust-call" fn call(&self, args: (u8, u8)) -> Self::Output {
        todo!()
    }
}

Sauf que Fn est supertrait de FnMut

trait Fn<Args: Tuple>: FnMut<Args>

Et FnMut est le supertrait de FnOnce

trait FnMut<Args: Tuple>: FnOnce<Args>

Implémentons tout le monde

struct ClosureAdderWithBias {
    bias: u8,
}

impl FnMut<(u8, u8)> for ClosureAdderWithBias {
    extern "rust-call" fn call_mut(&mut self, args: (u8, u8)) -> Self::Output {
        todo!()
    }
}

impl FnOnce<(u8, u8)> for ClosureAdderWithBias {
    type Output = u8; // notre Self::Output est définissable ici :)

    extern "rust-call" fn call_once(self, args: (u8, u8)) -> Self::Output {
        todo!()
    }
}

impl Fn<(u8, u8)> for ClosureAdderWithBias {
    extern "rust-call" fn call(&self, args: (u8, u8)) -> Self::Output {
        todo!()
    }
}

On peut alors implémenter la logique de la closure

impl Fn<(u8, u8)> for ClosureAdderWithBias {
    extern "rust-call" fn call(&self, args: (u8, u8)) -> Self::Output {
        args.0 + args.1 + self.bias
    }
}

Le fait d’utiliser les vrais outils du langage nous ouvre la porte à deux choses:

PremiĂšrement, nous n’avons plus besoin d’appeler explicitement la mĂ©thode ClosureAdderWithBias::call.

fn main() {
    let closure_with_bias_10 : ClosureAdderWithBias = adder_with_bias(10);
    // appel explicite
    dbg!(closure_with_bias_10.call((5, 2))); // 17
    // surcre syntaxique
    dbg!(closure_with_bias_10(5, 2)); // 17
}

DeuxiÚmement, ClosureAdderWithBias implémentant Fn(u8, u8) -> u8, nous pouvons en faire un type opaque.

fn adder_with_bias(bias: u8) -> impl Fn(u8, u8) -> u8 {
    ClosureAdderWithBias { bias }
}

Notre structure est une closure !! đŸŽ‰đŸŽ‰đŸ€©đŸ˜Ž

Les différences

Pourquoi trois traits différents ?

Pour répondre à cette question, analysons les signatures des méthodes implémentées.

fn call(&self, args: Args) -> Self::Output;
fn call_mut(&mut self, args: Args) -> Self::Output;
fn call_once(self, args: Args) -> Self::Output;

Nous avons trois méthodes qui ont des signatures semblables mais qui diffÚrent par un élément:

Voyons les différences

// lifetime '_ obligatoire
// elle indique que la valeur modifiée doit exister
// au moins aussi longtemps que la closure
fn twice(value: &mut u8) -> impl FnMut() + '_ {
    // le move est optionnel car &mut est forcément unique
    // donc le compilateur ne se pose pas de question et deplace
    // le &mut dans la closure
    || *value *= 2
}

fn display(data: u8) -> impl FnOnce() {
    move || {
        dbg!(data); // data est consommé ici
    }
}

// lifetime '_ obligatoire
// la référence doit exister aussi longtemps que la closure
// car la closure possÚde une copie de cette référence
// pas la valeur elle-mĂȘme
fn adder_with_bias(bias: &u8) -> impl Fn(u8, u8) -> u8 + '_ {
    // le move force la copie de la référence
    move |x: u8, y: u8| x + y + bias
}

Commençons avec la mutabilité.

fn main() {
    let mut value = 10;
    // on remarque l'obligation du "let mut"
    let mut twice_closure = twice(&mut value);
    twice_closure();
    twice_closure();
    twice_closure();
    // on se débarrasse de la closure pour pouvoir borrow
    drop(twice_closure);
    dbg!(&value); // 80
}

Continuons avec le call_once

fn main() {
    let data = 42;
    let display_closure = display(data);
    display_closure(); // 42
    display_closure(); // erreur : value used here after move
}

Et finalement par oĂč tout Ă  commencĂ©

fn main() {
    let bias = 10;
    let adder_with_10 = adder_with_bias2(&bias);
    dbg!(adder_with_10(5,2)); // 17
    dbg!(adder_with_10(4,2)); // 16
}

Petit dessin pour rĂ©sumer l’oignon de super-traits et les diffĂ©rents contextes capturable.

Async

Dernier truc avant de nous quitter.

Vous avez surement vu des closures qui ressemble Ă 

let future1 = || async { 42 };

Et bien c’est simplement un double sucre syntaxique

let future2 = || -> Ready<u8> { future::ready(42) };

Qui devient

struct ClosureFuture;

impl FnMut<()> for ClosureFuture {
    extern "rust-call" fn call_mut(&mut self, args: ()) -> Self::Output {
        todo!()
    }
}

impl FnOnce<()> for ClosureFuture {
    type Output = future::Ready<u8>;

    extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
        todo!()
    }
}

impl Fn<()> for ClosureFuture {
    extern "rust-call" fn call(&self, args: ()) -> Self::Output {
        future::ready(42_u8)
    }
}

Attention

Les closures asynchrones existent mais ne sont pas stables!

async || { 42 }

Leurs implémentation resemblerait à cela

struct AsyncClosure;

impl AsyncClosure {
    async fn call(&self) -> u8 {
        42
    }
}

Or les traits async ne sont pas encore stabilisĂ©s ce qui rend l’existence des closures async pour le moment impossible.

Mais si vous voulez faire de la black magic, vous pouvez avec cette crate.

Conclusion

On a vu que le pointeur de fonction est une fonction tout Ă  fait classique, tous les paramĂštres doivent ĂȘtre passĂ©s explicitement par copie ou par rĂ©fĂ©rence.

Un pointeur de fonction ne capture pas d’environnement, car c’est une routine de code quelque part en mĂ©moire.

Une closure n’est rien d’autre qu’une structure qui implĂ©mente Fn, FnOnce ou FnMut en fonction des besoins.

La sensation de capture de l’environnement, n’est du qu’à un sucre syntaxique du langage qui vient nous faciliter la vie.

Par dĂ©faut, une closure ne fait que rĂ©fĂ©rencer son environnement, il est possible de lui donner le droit de propriĂ©tĂ© en spĂ©cifiant explicetement que l’on dĂ©sire le faire avec l’opĂ©rateur move.

Merci de votre lecture, n’hĂ©sitez pas Ă  le partager s’il vous a plu ❀