Les GATs (generic associated types) ont été initialement proposés dans la RFC 1598. Ils permettent de définir des génériques de type, de durée de vie ou de constantes sur des types associés. Si vous êtes familier avec les langages qui ont des « types supérieurs », vous pourriez appeler les GATs des constructeurs de type sur les traits.
Que sont les types génériques associés ?
À la base, les types associés génériques permettent d'avoir des génériques (type, lifetime, ou const) sur les types associés. Notons que cela ne fait que compléter les endroits où il est possible de placer des génériques : par exemple, il est déjà possible d'avoir des génériques sur les alias de type indépendants et sur les fonctions dans les traits. Maintenant, il est simplement possible d'avoir des génériques sur les alias de type dans les traits (qui sont simplement appelés les types associés). Voici un exemple de ce à quoi ressemblerait un trait avec un GAT :
Code : | Sélectionner tout |
1 2 3 4 5 | trait LendingIterator { type Item<'a> where Self: 'a; fn next<'a>(&'a mut self) -> Self::Item<'a>; } |
Quelques bugs et limites actuels
Comme nous l'avons déjà mentionné, cette stabilisation n'est pas exempte de bogues et de limites. Ce n'est pas atypique par rapport à des fonctionnalités antérieures de grands langages. L'équipe Rust prevoit de corriger ces bogues et de supprimer ces limitations dans le cadre des efforts continus menés par l'équipe des types nouvellement formée. Ici, nous allons passer en revue quelques-unes des limitations que nous avons identifiées et que les utilisateurs pourraient rencontrer.
Considérons le code suivant :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | trait LendingIterator { type Item<'a> where Self: 'a; } pub struct WindowsMut<'x, T> { slice: &'x [T], } impl<'x, T> LendingIterator for WindowsMut<'x, T> { type Item<'a> = &'a mut [T] where Self: 'a; } fn print_items<I>(iter: I) where I: LendingIterator, for<'a> I::Item<'a>: Debug, { ... } fn main() { let mut array = [0; 16]; let slice = &mut array; let windows = WindowsMut { slice }; print_items::<WindowsMut<'_, usize>>(windows); } |
Imaginons ici que nous voulions avoir un LendingIterator dont les éléments sont des tranches superposées d'un tableau. Nous avons également une fonction print_items qui imprime tous les éléments d'un LendingIterator, pour autant qu'ils implémentent Debug. La plupart de ces éléments devraient vous sembler familiers ; ce trait est très similaire au trait Iterator de la bibliothèque standard. Fondamentalement, cette version du trait permet à la fonction suivante de retourner un élément qui emprunte à self.
Dans l'ensemble, même si vous n'avez pas besoin d'utiliser les GATs directement, il est très possible que les bibliothèques que vous utilisez utilisent les GATs en interne ou en public pour des raisons d'ergonomie, de performance ou simplement parce que c'est la seule façon dont l'implémentation fonctionne. Sans entrer dans les détails ici, le for<'a> I::Item<'a> : Debug implique actuellement que I::Item<'a> doit survivre 'static.
« Ce n'est pas vraiment un bug sympathique. Et de tous ceux que nous mentionnerons aujourd'hui, ce sera probablement celui qui sera le plus contraignant, le plus ennuyeux et le plus difficile à résoudre. Il apparaît beaucoup plus souvent avec les GATs, mais peut être trouvé dans du code qui n'utilise pas du tout les GATs. Malheureusement, la correction de ce problème nécessite quelques remaniements du compilateur, ce qui n'est pas un projet à court terme. C'est pourtant un projet à l'horizon. La bonne nouvelle est que, dans l'intervalle, nous travaillons à l'amélioration du message d'erreur que vous obtenez avec ce code. Voici à quoi il ressemblera lors de la prochaine stabilisation :
error[E0597]: `array` does not live long enough
|
| let slice = &mut array;
| ^^^^^^^^^^ borrowed value does not live long enough
| let windows = WindowsMut { slice };
| print_items::<WindowsMut<'_, usize>>(windows);
| -------------------------------------------- argument requires that `array` is borrowed for `'static`
| }
| - `array` dropped here while still borrowed
|
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
| for<'a> I::Item<'a>: Debug,
| ^^^^
Ce n'est pas parfait, mais c'est quelque chose. Cela ne couvre peut-être pas tous les cas, mais si vous avez un for<'a> I::Item<'a> : Trait lié quelque part et que vous obtenez une erreur indiquant que quelque chose ne vit pas assez longtemps, il se peut que vous rencontriez ce bogue. Nous travaillons activement à sa correction. Cependant, cette erreur n'apparaît pas aussi souvent que vous pourriez le penser en lisant ceci (d'après notre expérience), donc nous pensons que la fonctionnalité est toujours immensément utile même avec ce problème.
Les traits avec GATs ne sont pas sûrs pour les objets
Cette fonctionnalité est simple. Rendre les traits avec GATs sûrs pour les objets va demander un peu de travail de conception pour sa mise en œuvre. Pour avoir une idée du travail qu'il reste à faire, commençons par un bout ce code :
Code : | Sélectionner tout |
fn takes_iter(_: &dyn Iterator) {}
Nous pouvons écrire ça, mais ça ne compile pas :
error[E0191]: the value of the associated type `Item` (from trait `Iterator`) must be specified
--> src/lib.rs:1:23
|
1 | fn takes_iter(_: &dyn Iterator) {}
| ^^^^^^^^ help: specify the associated type: `Iterator<Item = Type>`
Pour qu'un objet trait soit bien formé, il doit spécifier une valeur pour tous les types associés. Pour la même raison, ce qui suit sera difficilement acceptable :
Code : | Sélectionner tout |
fn no_associated_type(_: &dyn LendingIterator) {}
Cependant, les TAM introduisent un peu plus de complexité. Prenez ce code :
Code : | Sélectionner tout |
fn not_fully_generic(_: &dyn LendingIterator<Item<'static> = &'static str>) {}
Ainsi, nous avons spécifié la valeur du type associé pour une valeur de la durée de vie de l'élément ('static), mais pas pour une autre valeur, comme ceci :
Code : | Sélectionner tout |
fn fully_generic(_: &dyn for<'a> LendingIterator<Item<'a> = &'a str>) {}
Bien que nous ayons une idée solide de la manière d'implémenter l'exigence dans certaines itérations futures du solveur de traits (qui utilise des formulations plus logiques), l'implémenter dans le solveur de traits actuel est plus difficile. C'est pourquoi nous avons choisi de ne pas y toucher pour le moment.
Le vérificateur d'emprunt n'est pas parfait et cela se voit. Pour rester dans l'exemple du LendingIterator, commençons par examiner deux méthodes de l'Iterator : for_each et filter :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | trait Iterator { type Item; fn for_each<F>(self, f: F) where Self: Sized, F: FnMut(Self::Item); fn filter<P>(self, predicate: P) -> Filter<Self, P> where Self: Sized, P: FnMut(&Self::Item) -> bool; } |
Les deux prennent une fonction comme argument. Les fermetures sont souvent utilisées dans ce cas. Maintenant, regardons les définitions de LendingIterator :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | trait LendingIterator { type Item<'a> where Self: 'a; fn for_each<F>(mut self, mut f: F) where Self: Sized, F: FnMut(Self::Item<'_>); fn filter<P>(self, predicate: P) -> Filter<Self, P> where Self: Sized, P: FnMut(&Self::Item<'_>) -> bool; } |
Cela semble assez simple, mais si c'était vraiment le cas, serait-ce le cas ici ? Commençons par regarder ce qui se passe lorsque nous essayons d'utiliser for_each :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 | fn iterate<T, I: for<'a> LendingIterator<Item<'a> = &'a T>>(iter: I) { iter.for_each(|_: &T| {}) } fn iterate<T, I: for<'a> LendingIterator<Item<'a> = &'a T>>(iter: I) { iter.for_each(|_: &T| {}) } |
Il s'avère que cela est assez étroitement lié à la première limitation dont nous avons parlé plus tôt, même si le vérificateur d'emprunt joue un rôle ici. D'un autre côté, regardons quelque chose qui est très clairement un problème de contrôleur d'emprunt, en regardant une implémentation de la structure Filter retournée par la méthode Filter:
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | impl<I: LendingIterator, P> LendingIterator for Filter<I, P> where P: FnMut(&I::Item<'_>) -> bool, // <- the bound from above, a function { type Item<'a> = I::Item<'a> where Self: 'a; // <- Use the underlying type fn next(&mut self) -> Option<I::Item<'_>> { // Loop through each item in the underlying `LendingIterator`... while let Some(item) = self.iter.next() { // ...check if the predicate holds for the item... if (self.predicate)(&item) { // ...and return it if it does return Some(item); } } // Return `None` when we're out of items return None; } } |
Exigences non locales pour les clauses where sur les GTAs
La dernière limitation prensenté par l'équipe Rust est un peu différente des autres ; ce n'est pas un bogue et elle ne devrait empêcher aucun programme de compiler. Mais tout revient à la clause where Self : 'a déjà prensenté. Comme mentionné précédemment, si vous êtes intéressé à creuser un peu dans la raison pour laquelle cette clause est requise, voir le post sur la poussée de stabilisation.
Il y a une condition pas si idéale à propos de cette clause. Comme pour les clauses where sur les fonctions, vous ne pouvez pas ajouter de clauses aux types associés dans les impls qui ne sont pas présentes dans le trait. Toutefois, si vous n'ajoutez pas cette clause, un grand nombre d'impls potentiels du trait seront interdits.
Pour aider les utilisateurs à ne pas tomber dans le piège d'un oubli accidentel d'ajouter cette clause (ou des clauses similaires qui ont le même effet pour un ensemble différent de génériques), l'éauipe Rust a mis en place un ensemble de règles qui doivent être suivies pour qu'un trait avec des GTAs puisse être compilé. Examinons d'abord l'erreur sans écrire la clause. Il s'agit d'une limitation connue du vérificateur d'emprunts actuel, qui devrait être résolue dans une prochaine itération.
Code : | Sélectionner tout |
1 2 3 4 5 | trait LendingIterator { type Item<'a>; fn next<'a>(&'a mut self) -> Self::Item<'a>; } |
error: missing required bound on `Item`
--> src/lib.rs:2:5
|
2 | type Item<'a>;
| ^^^^^^^^^^^^^-
| |
| help: add the required where clause: `where Self: 'a`
|
Cette erreur devrait être utile. Pour les méthodes qui utilisent le GTAs, toutes les limites qui peuvent être prouvées doivent également être présentes sur le TAGTAs M lui-même. D'accord, alors comment l'équipe Rust a-elle obtenu le Self: 'a requis. Eh bien, jetons un coup d'oeil à la méthode next. Elle retourne Self::Item<'a>, et nous avons un argument &'a mut self.
« Nous exigeons ces limites maintenant pour laisser de la place à l'avenir pour potentiellement les impliquer automatiquement (et bien sûr parce que cela devrait aider les utilisateurs à écrire des traits avec des GATs) », déclare l’équipe Rust.
Source : Rust
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
Rust peut-il sauver la planète ? Un composant JavaScript a été réécrit en Rust et aurait une amélioration de 50 % de la latence, une réduction de 75 % de l'utilisation du CPU et 95 % de la mémoire
Les applications Rust sont-elles plus rapides que leurs équivalents en C ? C'est ce que suggèrent plusieurs benchmarks qui comparent les deux langages de programmation de la filière système