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:
PremiĂšremet, il y a apparition dans la syntaxe dâune double-barre verticale
||, câest ce symbole qui dit Ă Rust que nous dĂ©marrons la dĂ©claration dâune closure. Ensuite nous avons un bloc classique de Rust.DeuxiĂšmement, la closure est affectable Ă une variable comme nâimporte quelle valeur.
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
closureest une adresse quelque part dans la mĂ©moire.On peut demander Ă Rust de nous lâafficher via
dbg!(&closure as *const _); // &closure as *const _ = 0x00000010baaff410Si lâon veut non pas lâaddresse de
closuremais lâadresse du code de la closure pointĂ© par la variableclosureon peut Ă©crire quelque chose comme celadbg!(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() = 42Autrement 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() = 42Paramé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) = 42On 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 obligatoiresToutes 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 grandNous 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)); // 100000010Attention
Ce nâest pas de la gĂ©nĂ©ricitĂ©!
Le premier appel fixe le type.
Si câest
integer_u8en premier, le type serau8, si câest lâinverse, ça serau64.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) = 7On peut alors créer des comportement différents.
let mul = |x : u8, y: u8| x * y;
dbg!(mul(5, 2)) // mul(5, 2) = 10Tous 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); // 10Vous 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.
- La fonction est accessible de nâimporte oĂč dans le code, elle est dĂ©fini en mĂȘme temps que le
mainet peut mĂȘme lui survivre, (dans les faits tout sera nettoyĂ© Ă la fin demain). - La fonction local nâa dâexistence que dans le
mainet ne peut donc pas ĂȘtre appelĂ© de lâextĂ©rieur demain, elle peut par contre ĂȘtre passĂ© en paramĂštre et appelĂ©e autant de fois que nĂ©cessaire. - La closure affectĂ© Ă une variable a le mĂȘme comportement que la fonction local, on verra dans la suite pourquoi.
- La closure anonyme par contre nâa de sens que dans le
run_closure
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)); // 27Pour les personnes qui se demandent
On remarque ici, que Rust permet dâadditionner des
u8et des&u8grĂące Ă lâimplĂ©mentation par dĂ©faut ci-dessousimpl<'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 insteadcan'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); // 4Maintenant, 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 hereEt 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.
- les références utilisées par
double_refdoivent vivre entre le carré et le triangle - les références utilisées par
triple_refdoivent vivre entre le cercle et la croix
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); // 2Et 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); // 12LĂ 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 hereClairement, 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); // 2Les 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 hereLe 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); // 27Vous 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 hereBien des choses sont intéressantes ici:
expected fn pointer, found closureclairement on retrourne une closure mais la fonction attend un pointeur de fonction.closures can only be coerced to "fn" types if they do not capture any variables, le compilateur nous dis que câest la capture de la variable qui provoque le passage dâun pointeur de fonction vers une closure.Et nous avons mĂȘme quâest ce qui a Ă©tĂ© capturĂ©.
"bias" captured here.
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é:
closure may outlive the current function: oui on la retourne donc normalbut it borrows "bias", which is owned by the current function: donc borrow checkerto force the closure to take ownership of "bias", use the "move" keyword: doncmovemodifie le comportement de la closure par rapport Ă la rĂ©fĂ©rence Ăbiaset en prend lâownership.
En Rust ce qui est copiable sera copiĂ© lorsquâelle doit ĂȘtre dĂ©placĂ©.
Notre
bias: u8sera 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
movedevant 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
Fnsur notreClosureAdderWithBias?
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 nightlyEnsuite, 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!()
}
}On voit le
Fn<(u8, u8)>qui correspond Ă la signature des paramĂštres dâentrĂ©es de la closure.De la magie noire
extern "rust-call".Un paramĂštre
args: (u8, u8)qui aussi a la mĂȘme signature.Et finalement
Self::Outputque lâon ne peut pas dĂ©finir
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:
callutilise une rĂ©fĂ©rence immutable&self, elle peut ĂȘtre rejouĂ©e autant de fois que lâon veut sans se soucier de lâĂ©tat, elle est purecall_mutpossĂšde une rĂ©fĂ©rence mutable&mut self, elle peut ĂȘtre rappelĂ©e autant de fois que lâon veut mais peut muter son Ă©tat internecall_oncea un passage par valeur deself, Ă la fin de lâappel, la structure sera dĂ©truite et donc la mĂ©thode ne peut pas ĂȘtre rappellĂ©e
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.

fn: est un pointeur de fonction dĂ©fini Ă lâavance, il ne prend pas de contexteFn: peut lire des rĂ©fĂ©rence de contexteFnMut: peut modifier des rĂ©fĂ©rence de contexteFnOnce: prend les droits de propriĂ©tĂ© du contexte
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 â€ïž
