/// Creates an iterator which uses a closure to determine if an element should be removed.
///
/// If the closure returns true, then the element is removed and yielded.
/// If the closure returns false, it will try again, and call the closure
/// on the next element, seeing if it passes the test.
///
/// Using this method is equivalent to the following code:
///
/// ```
/// # let mut some_predicate = |x: &mut i32| { *x == 2 };
/// # let mut vec = vec![1, 2, 3, 4, 5];
/// let mut i = 0;
/// while i != vec.len() {
/// if some_predicate(&mut vec[i]) {
/// let val = vec.remove(i);
/// // your code here
/// }
/// i += 1;
/// }
/// ```
///
/// But `drain_filter` is easier to use. `drain_filter` is also more efficient,
/// because it can backshift the elements of the array in bulk.
///
/// Note that `drain_filter` also lets you mutate ever element in the filter closure,
/// regardless of whether you choose to keep or remove it.
///
///
/// # Examples
///
/// Splitting an array into evens and odds, reusing the original allocation:
///
/// ```
/// let mut numbers = vec![1, 2, 3, 4, 5, 6, 8, 9, 11, 13, 14, 15];
///
/// let evens = numbers.drain_filter(|x| *x % 2 == 0).collect::<Vec<_>>();
/// let odds = numbers;
///
/// assert_eq!(evens, vec![2, 4, 6, 8, 14]);
/// assert_eq!(odds, vec![1, 3, 5, 9, 11, 13, 15]);
/// ```
fn drain_filter<F>(&mut self, filter: F) -> DrainFilter<T, F>
where F: FnMut(&mut T) -> bool,
{ ... }
Je suis sûr qu'il y a un problème quelque part, mais je ne le trouve pas. Quelqu'un nerd m'a poussé à l'implémenter. PR entrant.
Peut-être que cela n'a pas besoin d'inclure l'évier de la cuisine, mais il _pourrait_ avoir un paramètre de plage, de sorte que c'est comme un sur-ensemble de drain. Des inconvénients à cela ? Je suppose que l'ajout de bornes vérifiant la plage est un inconvénient, c'est une autre chose qui peut paniquer. Mais drain_filter(.., f) ne peut pas.
Y a-t-il une chance que cela se stabilise sous une forme ou une autre dans un avenir pas trop lointain ?
Si le compilateur est assez intelligent pour éliminer les contrôles de limites
dans le cas drain_filter(.., f)
, j'opterais pour cela.
(Et je suis presque sûr que vous pouvez l'implémenter d'une manière
ce qui rend le compilateur assez intelligent, au pire
cas où vous pourriez avoir une "spécialisation en fonction",
en gros quelque chose comme if Type::of::<R>() == Type::of::<RangeFull>() { dont;do;type;checks; return }
)
Je sais que c'est du bikeshedding dans une certaine mesure, mais quel était le raisonnement derrière le nom de ce drain_filter
plutôt que drain_where
? Pour moi, le premier implique que l'ensemble des Vec
sera drainé, mais que nous exécutons également un filter
sur les résultats (quand je l'ai vu pour la première fois, j'ai pensé : "comment est-ce non seulement .drain(..).filter()
?"). Le premier, d'autre part, indique que nous ne drainons que les éléments où certaines conditions sont remplies.
Aucune idée, mais drain_where
sonne beaucoup mieux et est beaucoup plus intuitif.
Y a-t-il encore une chance de le changer ?
.remove_if
a également été une suggestion précédente
Je pense que drain_where
l'explique le mieux. Comme drain, il renvoie des valeurs, mais il ne draine/supprime pas toutes les valeurs, mais uniquement lorsqu'une condition donnée est vraie.
remove_if
ressemble beaucoup à une version conditionnelle de remove
qui supprime simplement un seul élément par index si une condition est vraie, par exemple letters.remove_if(3, |n| n < 10);
supprime la lettre à l'index 3 si elle est < 10 .
drain_filter
d'autre part est légèrement ambigu, le fait-il drain
puis filter
d'une manière plus optimisée (comme filter_map) ou fait si drainer pour qu'un itérateur soit renvoyé comparable à l'itérateur filter
renverrait,
et si c'est le cas, ne devrait-il pas s'appeler filtered_drain
car le filtre est logiquement utilisé avant...
Il n'existe aucun précédent pour l'utilisation _where
ou _if
n'importe où dans la bibliothèque standard.
Le code "ledit équivalent" dans le commentaire n'est pas correct ... vous devez moins un de i sur le site "votre code ici", sinon de mauvaises choses se produisent.
IMO ce n'est pas filter
qui est le problème. Ayant juste cherché cela (et étant un débutant), drain
semble être assez non standard par rapport aux autres langues.
Encore une fois, juste du point de vue du débutant, les choses que je rechercherais si j'essayais de trouver quelque chose à faire ce que ce numéro propose seraient delete
(comme dans delete_if
), remove
, filter
ou reject
.
En fait, j'ai _recherché_ pour filter
, j'ai vu drain_filter
et j'ai continué à chercher_ sans lire parce que drain
ne semblait pas représenter la chose simple que je voulais faire.
Il semble qu'une simple fonction nommée filter
ou reject
serait beaucoup plus intuitive.
Sur une note séparée, je ne pense pas que cela doive muter le vecteur sur lequel il est appelé. Il évite l'enchaînement. Dans un scénario idéal, on voudrait pouvoir faire quelque chose comme :
vec![
"",
"something",
a_variable,
function_call(),
"etc",
]
.reject(|i| { i.is_empty() })
.join("/")
Avec la mise en œuvre actuelle, ce à quoi il se joindrait serait les valeurs rejetées.
J'aimerais voir à la fois un accept
et un reject
. Ni l'un ni l'autre ne modifie la valeur d'origine.
Vous pouvez déjà faire le chaînage avec filter
seul. Tout l'intérêt de drain_filter
est de faire muter le vecteur.
@rpjohnst donc j'ai cherché ici , est-ce qu'il me manque filter
quelque part ?
Oui, c'est un membre de Iterator
, pas Vec
.
Drain est une nouvelle terminologie car elle représentait un quatrième type de propriété dans Rust qui ne s'applique qu'aux conteneurs, tout en étant généralement une distinction dénuée de sens dans presque toutes les autres langues (en l'absence de sémantique de déplacement, il n'est pas nécessaire de combiner l'itération et la suppression dans une seule opération ""atomique"").
Bien que drain_filter déplace la terminologie de drain dans un espace dont d'autres langues se soucieraient (car éviter les retours en arrière est pertinent dans toutes les langues).
Je suis tombé sur drain_filter
dans la documentation en tant que résultat Google pour rust consume vec
. Je sais qu'en raison de l'immuabilité par défaut dans la rouille, le filtre ne consomme pas les données, je ne pouvais tout simplement pas me rappeler comment l'aborder pour mieux gérer la mémoire.
drain_where
est bien, mais tant que l'utilisateur est conscient de ce que font drain
et filter
, je pense qu'il est clair que la méthode draine les données en fonction d'un filtre de prédicat.
J'ai toujours l'impression que drain_filter
implique qu'il draine (c'est-à-dire se vide) puis filtre. drain_where
, d'autre part, semble drainer les éléments où la condition donnée est remplie (ce que fait la fonction proposée).
linked_list::DrainFilter
ne devrait-il pas également implémenter Drop
pour supprimer tous les éléments restants qui correspondent au prédicat ?
Oui
Pourquoi exactement la suppression de l'itérateur le fait-il s'exécuter jusqu'à la fin ? Je pense que c'est un comportement surprenant pour un itérateur et cela pourrait aussi être, si on le souhaite, fait explicitement. L'inverse de ne retirer que le nombre d'éléments dont vous avez besoin est impossible d'autre part parce que mem::forget
ing l'itérateur se heurte à une amplification de fuite.
J'utilise beaucoup cette fonction et je dois toujours me rappeler de retourner true
pour les entrées que je veux vider, ce qui semble contre-intuitif par rapport à retain()
/ retain_mut()
.
D'un point de vue logique intuitif, il serait plus logique de renvoyer true
pour les entrées que je souhaite conserver, est-ce que quelqu'un d'autre ressent cela ? (Surtout si l'on considère que retain()
fonctionne déjà de cette façon)
Pourquoi ne pas faire cela et renommer drain_filter()
en retain_iter()
ou retain_drain()
(ou drain_retain()
) ?
Ensuite, cela refléterait également retain()
plus près !
C'est pourquoi j'ai proposé de le renommer en
drain_where
car il est alors clair que :
C'est une forme de drain donc nous utilisons drain dans le nom
En utilisant where
en combinaison avec drain
, il est clair que le
les éléments _où_ le prédicat est vrai sont drainés, c'est-à-dire supprimés et renvoyés
Il est plus en phase avec d'autres noms dans std, normalement si vous avez un
fonction composée de deux prédicats, vous pouvez l'émuler (grossièrement) en utilisant
fonctions représentant chacun des prédicats de manière "et alors", par exemple
filter_map
peut être émulé (en gros) comme filter an then map
. Le courant
le nom indique qu'il s'agit drain and then filter
, mais il n'en est même pas proche
car il ne fait pas du tout une vidange complète.
Le dimanche 25 février 2018, 17h04, Boscop [email protected] a écrit :
J'utilise beaucoup cette fonction et je dois toujours me souvenir de
renvoie vrai pour les entrées que je veux vider, ce qui semble
contre-intuitif par rapport à retention_mut().
Au niveau primaire, il serait plus logique de renvoyer true pour les entrées I
voulez garder, est-ce que quelqu'un d'autre ressent cela?
Pourquoi ne pas le faire et renommer drain_filter() en retention_filter() ?—
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368320990 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AHR0kfwaNvz8YBwCE4BxDkeHgGxLvcWxks5tYYRxgaJpZM4OY1me
.
Mais avec drain_where()
la fermeture devrait toujours retourner vrai pour les éléments qui devraient être supprimés, ce qui est l'opposé de retain()
qui le rend incohérent.
Peut-être retain_where
?
Mais je pense que vous avez raison de dire qu'il est logique d'avoir "drain" dans le nom, donc je pense que drain_retain()
est le plus logique : c'est comme drain()
mais en conservant les éléments où la fermeture revient true
.
Bien que cela ne change pas, que vous deviez retourner true, il est clair que
vous devez le faire.
Personnellement, j'utiliserais soit:
une. drain_where
b. retain_where
c. retain
Je n'utiliserais pas drain_retain
.
Vider et retenir parlent du même genre de processus mais à l'opposé
perspectives, drain parle de ce que vous supprimez (et r restituez) conservez
parle de ce que vous gardez (dans le sens où il est déjà utilisé dans std).
Actuellement, retaim
est déjà implémenté dans std avec la différence majeure
qu'il supprime des éléments pendant que drain
les renvoie, ce qui rend
retain
(malheureusement) inadapté à être utilisé dans le nom (sauf si vous voulez appeler
il retain_and_return
ou similaire).
Un autre point qui parle de drain est la facilité de migration, c'est-à-dire la migration
à drain_where
est aussi simple que d'exécuter une recherche et un remplacement de mots sur
le code, tandis que le changer pour conserver nécessiterait une négation supplémentaire de
toutes les fonctions de prédicats/filtres utilisées.
Le dimanche 25 février 2018, 18h01, Boscop [email protected] a écrit :
Mais avec drain_where() la fermeture devrait toujours retourner true pour
éléments qui doivent être supprimés, ce qui est l'opposé de retention() qui
rend incohérent..
Peut-être conserver_où ?
Mais je pense que vous avez raison de dire qu'il est logique d'avoir "drain" dans le nom,
donc je pense que drain_retain() a le plus de sens : c'est comme drain() mais
en conservant les éléments où la fermeture renvoie vrai.—
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368325374 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AHR0kfG4oZHxGfpOSK8DjXW3_2O1Eo3Rks5tYZHxgaJpZM4OY1me
.
Mais à quelle fréquence migreriez-vous de drain()
à drain_filter()
?
Dans tous les cas jusqu'à présent, j'ai migré de retain()
vers drain_filter()
car il n'y a pas retain_mut()
dans std et j'ai besoin de muter l'élément ! Alors j'ai dû inverser la valeur de retour de fermeture ..
Je pense que drain_retain()
a du sens car la méthode drain()
draine inconditionnellement tous les éléments de la plage, alors que drain_retain()
conserve les éléments où la fermeture renvoie true
, elle combine les effets des méthodes drain()
et retain()
.
Désolé, je veux dire migrer de drain_filter
vers drain_where
.
Que vous avez une solution utilisant retain
et que vous devez ensuite utiliser
drain_filter
est un aspect auquel je n'ai pas encore pensé.
Le dimanche 25 février 2018, 19h12, Boscop [email protected] a écrit :
Mais pourquoi migreriez-vous de drain() vers drain_filter() ?
Dans tous les cas jusqu'à présent, j'ai migré de retention() vers drain_filter()
car il n'y a pas de retention_mut() dans std et j'ai besoin de muter l'élément !
Je pense que drain_retain() a du sens car la méthode drain() draine
inconditionnellement tous les éléments de la plage, alors que drain_retain() conserve
les éléments où la fermeture renvoie vrai.—
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368330896 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AHR0kSayIk_fbp5M0RsZW5pYs3hDICQIks5tYaJ0gaJpZM4OY1me
.
Ah oui, mais je pense que le "prix" d'inverser les fermetures dans le code actuel qui utilise drain_filter()
en vaut la peine, pour obtenir une API cohérente et intuitive en std puis en stable.
Ce n'est qu'un petit coût fixe (et facilité par le fait qu'il s'accompagnerait d'un changement de nom de la fonction, de sorte que l'erreur du compilateur pourrait indiquer à l'utilisateur que la fermeture doit être inversée, afin qu'elle n'introduise pas silencieusement un bogue) , par rapport au coût de la standardisation de drain_filter()
et ensuite les gens doivent toujours inverser la fermeture lors de la migration de retain()
à drain_filter()
.. (en plus du coût mental de se souvenir de faire cela, et les coûts pour le rendre plus difficile à trouver dans les docs, venant de retain()
et recherchant "quelque chose comme retain()
mais passant &mut
à sa fermeture, qui c'est pourquoi je pense qu'il est logique que le nouveau nom de cette fonction ait "retain" dans le nom, afin que les gens le trouvent lors de la recherche dans les docs).
Quelques données anecdotiques : dans mon code, je n'ai toujours eu besoin que de l'aspect retain_mut()
de drain_filter()
(souvent ils utilisaient retain()
auparavant), je n'ai jamais eu de cas d'utilisation où j'avais besoin de traiter les valeurs drainées. Je pense que ce sera également le cas d'utilisation le plus courant pour les autres à l'avenir (puisque retain()
ne passe pas &mut
à sa fermeture, de sorte que drain_filter()
doit couvrir ce cas d'utilisation , aussi, et c'est un cas d'utilisation plus courant que de devoir traiter les valeurs drainées).
La raison pour laquelle je suis drain_retain
est à cause de la façon dont les noms sont actuellement utilisés dans std wrt. collectes :
drain
, collect
, fold
, all
, take
, ...*_where
, *_while
map
, filter
, skip
, ...)map
vs. filter
/ skip
)filter_map
apply modifier_1 and then apply modifier_2
, juste qu'il est plus rapide ou plus flexible de le faire en une seule étapeVous pouvez parfois avoir :
drain_filter
)drain_where
)Normalement tu n'as pas :
take_collect
car cela prête facilement à confusiondrain_retain
a un peu de sens mais tombe dans la dernière catégorie, alors que vous pouvez probablement deviner ce qu'il fait, il dit essentiellement remove and return all elements "somehow specified" and then keep all elements "somehow specified" discarding other elements
.
D'un autre côté, je ne sais pas pourquoi il ne devrait pas y avoir de retain_mut
ouvrant peut-être un RFC rapide introduisant retain_mut
comme un moyen efficace de combiner modify
+ retain
J'ai l'intuition que ça pourrait être plus rapide
stabilisé alors cette fonction. Jusque-là, vous pourriez envisager d'écrire un trait d'extension fournissant
vous possédez retain_mut
en utilisant iter_mut
+ un bool-array (ou bitarray, ou ...) pour garder une trace de quels éléments
doivent être supprimés. Ou fournir votre propre drain_retain
qui utilise en interne drain_filer
/ drain_where
mais enveloppe le prédicat dans un not |ele| !predicate(ele)
.
@dathinab
*_where
?retain()
, drain()
. Il n'y a pas de confusion avec les méthodes Iterator qui transforment un itérateur en un autre itérateur.retain_mut()
ne serait pas ajouté à std car drain_filter()
sera déjà ajouté et les gens ont été invités à l'utiliser. Ce qui nous ramène au cas d'utilisation de la migration de retain()
vers drain_filter()
étant très courant, il devrait donc avoir un nom et une API similaires (la fermeture renvoyant true
signifie conserver l'entrée )..Je ne savais pas que retain_mut
avait déjà été discuté.
Nous parlons de schémas de dénomination généraux dans std principalement wrt. à
collections, qui incluent les itérateurs car ils sont l'un des principaux
méthodes d'accès aux collections dans rust, surtout quand il s'agit
modifier plus d'une entrée.
_where
n'est qu'un exemple de suffixes pour exprimer un mot légèrement modifié_until
, _while
, _then
, _else
, _mut
et _back
.La raison pour laquelle drain_retain
prête à confusion est qu'il n'est pas clair s'il s'agit
Drain ou Retain based, s'il est basé sur le drain, retourner true supprimerait
la valeur, si elle est basée sur la conservation, elle la conserverait. L'utilisation _where
rend à
dernier clair ce qui est attendu de la fonction transmise.
Le lundi 26 février 2018, 00h25, Boscop [email protected] a écrit :
@dathinab https://github.com/dathinab
- Nous parlons ici d'une méthode sur les collections, pas sur Iterator.
map, filter, filter_map, skip, take_while etc sont toutes des méthodes sur Iterator.
Btw, quelles méthodes voulez-vous dire qui utilisent * _where ?
Nous devons donc comparer le schéma de nommage aux méthodes qui existent déjà
sur les collections, par exemple keep(), drain(). Il n'y a pas de confusion avec
Méthodes d'itérateur qui transforment l'itérateur en un autre itérateur.- AFAIK, le consensus était que retention_mut() ne serait pas ajouté à std
car drain_filter() sera déjà ajouté et les gens ont été avisés
pour utiliser ça. Ce qui nous ramène au cas d'utilisation de la migration depuis
conserver () à drain_filter () étant très commun, il devrait donc avoir un
nom et API similaires (la fermeture retournant vrai signifie conserver l'entrée)..—
Vous recevez ceci parce que vous avez été mentionné.Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368355110 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AHR0kfkRAZ5OtLFZ-SciAmjHDEXdgp-0ks5tYevegaJpZM4OY1me
.
J'utilise beaucoup cette fonction et je dois toujours me rappeler de retourner true pour les entrées que je veux vider, ce qui semble contre-intuitif par rapport à retention()/retain_mut().
FWIW, je pense que retain
est le nom contre-intuitif ici. Je me retrouve généralement à vouloir supprimer certains éléments d'un vecteur, et avec retain
je dois toujours inverser cette logique.
Mais retain()
est déjà stable, donc nous devons vivre avec. Et donc mon intuition s'est habituée à ça..
@Boscop : et donc drain
qui est l'inverse de retain
mais renvoie également les éléments supprimés et l'utilisation de suffixes comme _until
, _while
pour faire fonctions disponibles qui ne sont qu'une version légèrement modifiée d'une fonctionnalité existante.
Je veux dire si je décrirais le drain, ce serait quelque chose comme:
_supprimer et renvoyer tous les éléments spécifiés "d'une certaine manière", conserver tous les autres éléments_
où _"d'une certaine manière"_ est _"par découpage"_ pour tous les types de collection découpables et _"tous"_ pour le reste.
La description de la fonction discutée ici est _la même_ sauf que
_"d'une certaine manière"_ est _" où un prédicat donné renvoie vrai"_.
D'autre part la description que je donnerais à retenir est :
_retenir (c'est-à-dire garder) uniquement les éléments où un prédicat donné renvoie vrai, jeter le reste_
(oui, conserver aurait pu être utilisé d'une manière où il ne supprime pas le reste, malheureusement ce n'était pas le cas)
Je pense que cela aurait été vraiment bien si retain
avait
a passé &mut T
au prédicat et a peut-être renvoyé les valeurs supprimées.
Parce que je pense que retain
est une base de nom plus intuitive.
Mais indépendamment de cela, je pense aussi que les deux drain_filter
/ drain_retain
sont sous-optimaux
car ils ne précisent pas si le prédicat doit renvoyer vrai/faux pour conserver/vider une entrée.
(le drain indique vrai le supprime car il parle de supprimer des éléments pendant le filtrage
et le recyclage parle des éléments à conserver, enfin dans la rouille)
En fin de compte, ce n'est pas _si_ important de savoir lequel des noms est utilisé, ce serait juste bien s'il se stabilisait.
Faire un sondage et/ou laisser quelqu'un de l'équipe linguistique décider pourrait être le meilleur moyen de faire avancer la réflexion ?
Je pense que quelque chose comme drain_where
, drain_if
ou drain_when
est beaucoup plus clair que drain_filter
.
@tmccombs Sur ces 3, je pense que drain_where
est le plus logique. (Parce que if
implique either do the whole thing (in this case draining) or not
et when
est temporel.)
Par rapport à drain_filter
la valeur de retour de fermeture est la même avec drain_where
( true
pour supprimer un élément) mais ce fait est rendu plus clair/explicite par le nom, donc il élimine le risque d'interpréter accidentellement la signification de la valeur de retour de fermeture de manière erronée.
Je pense qu'il est plus que temps de se stabiliser. Résumé de ce fil :
R: RangeArgument
?true
du rappel entraîne l'inclusion de cet élément dans l'itérateur.)drain_where
.)@Gankro , qu'en pensez-vous ?
L'équipe libs en a discuté et le consensus était de ne pas stabiliser plus de méthodes de type drain
pour le moment. (La méthode drain_filter
existante peut rester instable dans Nightly.) https://github.com/rust-lang/rfcs/pull/2369 propose d'ajouter un autre itérateur de type drain qui ne fait rien lorsqu'il est abandonné (au lieu de consommer l'itérateur jusqu'à la fin).
On aimerait voir des expérimentations pour tenter de généraliser sur une surface API plus petite différentes combinaisons de vidange :
RangeArgument
alias RangeBounds
) par rapport à la collection entière (bien que cette dernière puisse être obtenue en passant ..
, une valeur de type RangeFull
).Les possibilités peuvent inclure la "surcharge" d'une méthode en la rendant générique, ou un modèle de générateur.
Une contrainte est que la méthode drain
est stable. Il peut éventuellement être généralisé, mais uniquement de manière rétrocompatible.
On aimerait voir des expérimentations pour tenter de généraliser sur une surface API plus petite différentes combinaisons de vidange :
Comment et où l'équipe prévoit-elle que ce type d'expérimentation se produise ?
Comment : concevoir et proposer une conception d'API concrète, éventuellement avec une implémentation de preuve de concept (qui peut être effectuée hors de l'arborescence via au moins Vec::as_mut_ptr
et Vec::set_len
). Où n'a pas trop d'importance. Il peut s'agir d'un nouveau RFC ou d'un fil de discussion dans https://internals.rust-lang.org/c/libs , et liez-le à partir d'ici.
Je joue un peu avec ça. J'ouvrirai un fil sur les internes dans les prochains jours.
Je pense qu'une API générale qui fonctionne comme ceci a du sens :
v.drain(a..b).where(pred)
Il s'agit donc d'une API de type constructeur : si .where(pred)
n'est pas ajouté, il videra toute la plage de manière inconditionnelle.
Cela couvre les capacités de la méthode actuelle .drain(a..b)
ainsi que .drain_filter(pred)
.
Si le nom drain
ne peut pas être utilisé car il est déjà utilisé, il doit s'agir d'un nom similaire à drain_iter
.
La méthode where
ne doit pas être nommée *_filter
pour éviter toute confusion avec le filtrage de l'itérateur résultant, en particulier lorsque where
et filter
sont utilisés en combinaison comme ceci :
v.drain(..).where(pred1).filter(pred2)
Ici, il utilisera pred1
pour décider ce qui sera drainé (et transmis dans l'itérateur) et pred2
est utilisé pour filtrer l'itérateur résultant.
Tous les éléments pour lesquels pred1
renvoie true
mais pour lesquels $ pred2
renvoient false
seront toujours drainés de v
mais ne seront pas cédés par cet itérateur combiné.
Que pensez-vous de ce type d'approche d'API de style constructeur ?
Pendant une seconde, j'ai oublié que where
ne peut pas être utilisé comme nom de fonction car c'est déjà un mot-clé :/
Et drain
est déjà stabilisé donc le nom ne peut pas être utilisé non plus..
Ensuite, je pense que la deuxième meilleure option globale est de conserver le drain
actuel et de renommer drain_filter
en drain_where
, pour éviter la confusion avec .drain(..).filter()
.
(Comme jonhoo l'a dit plus haut : )
quel était le raisonnement derrière le nom de ce drain_filter plutôt que drain_where ? Pour moi, le premier implique que tout le Vec sera drainé, mais que nous exécutons également un filtre sur les résultats (quand je l'ai vu pour la première fois, j'ai pensé : "comment n'est-ce pas juste .drain(..).filter() ?"). Le premier, d'autre part, indique que nous ne drainons que les éléments où certaines conditions sont remplies.
J'ai ouvert un sujet sur les internes .
Le TLDR est que je pense que le non-auto-épuisement est une plus grande boîte de vers que prévu dans le cas général et que nous devrions stabiliser drain_filter
plus tôt possible avec un paramètre RangeBounds
. À moins que quelqu'un ait une bonne idée pour résoudre les problèmes qui y sont décrits.
Edit : J'ai téléchargé mon code expérimental : drain experiences
Il y a aussi des bancs de vidange et de nettoyage et quelques tests mais ne vous attendez pas à un code propre.
Totalement raté sur ce fil. J'ai eu un vieil impl que j'ai corrigé un peu et copié collé pour refléter quelques-unes des options décrites dans ce fil. La seule bonne chose à propos de l'impl qui, je pense, ne sera pas controversée, c'est qu'elle implémente DoubleEndedIterator
. Regardez-le ici .
@Emerentius mais alors nous devrions au moins renommer drain_filter
en drain_where
, pour indiquer que la fermeture doit retourner true
pour supprimer l'élément !
@Boscop Les deux impliquent la même "polarité" de true
=> rendement. Personnellement, je me fiche de savoir si ça s'appelle drain_filter
ou drain_where
.
@Popog Pouvez-vous résumer les différences et les avantages et inconvénients ? Idéalement au niveau du filetage interne. Je pense que la fonctionnalité DoubleEndedIterator
pourrait être ajoutée de manière rétrocompatible avec une surcharge nulle ou faible (mais je n'ai pas testé cela).
Que diriez-vous drain_or_retain
? C'est une action grammaticalement significative, et elle signale qu'elle fait l'une ou l'autre.
@askeksa Mais cela ne précise pas si le retour true
de la fermeture signifie "vider" ou "conserver".
Je pense qu'avec un nom comme drain_where
, il est très clair que retourner true
draine, et il devrait être clair pour tout le monde que les éléments qui ne sont pas drainés sont conservés.
Ce serait bien s'il y avait un moyen de limiter/arrêter/annuler/interrompre la vidange. Par exemple, si je voulais vider les N premiers nombres pairs, ce serait bien de pouvoir simplement faire vec.drain_filter(|x| *x % 2 == 0).take(N).collect()
(ou une variante de cela).
Telle qu'elle est actuellement implémentée, la méthode DrainFilter
de drop
exécutera toujours le drain jusqu'à la fin ; il ne peut pas être interrompu (du moins je n'ai trouvé aucune astuce qui ferait cela).
Si vous voulez ce comportement, vous devez simplement fermer un état qui suit le nombre de fois que vous en avez vu et commencer à renvoyer false. L'exécution jusqu'à la fin de la suppression est nécessaire pour que les adaptateurs se comportent raisonnablement.
Je viens de remarquer que la façon dont drain_filter
est actuellement implémenté n'est pas unwind safe mais
en fait un danger pour la sécurité wrt. se détendre + reprendre la sécurité. De plus, cela provoque facilement un abordage, à la fois
dont sont des comportements qu'une méthode dans std ne devrait vraiment pas avoir. Et en écrivant ceci, j'ai remarquéque son implémentation actuelle n'est pas sûre
Je sais que Vec
n'est pas par défaut unwind safe, mais le comportement de drain_filer
lorsque le
predicate panics est bien surprenant car :
Un exemple de ce comportement est ici:
play.rust-lang.org
Alors que le point 2. devrait pouvoir être résolu, je pense que le premier point sur lui-même devrait
conduisent à reconsidérer le comportement de DrainFilter
pour courir jusqu'à la fin
en cas de chute, les raisons de changer cela incluent :
drain_filter
peut paniquer dans certaines circonstances (par exemple, un verrouDrainFilter
jusqu'à la fin, vous pourriez obtenirLes arguments en faveur de l'exécution complète incluent :
drain_filter
est similaire à ratain
qui est une fonction, donc les gens pourraient être surpris quand ilsDrainFilter
au lieu de l'exécuter jusqu'à la fin#[unused_must_use]
.for_each(drop)
qui ironiquementDrainFilter
fait à la baissedrain_filter
est souvent utilisé uniquement pour ses effets secondaires, il est donc trop verbeuxretain
&T
, drain_filter utilisé &mut T
find
, all
, any
ce dont j'ai une bonne raison pour conserver le comportement actuel.C'est peut-être juste moi ou j'ai raté un point, mais changer le comportement Drop
et
ajouter #[unused_must_use]
semble être préférable ?
Si .for_each(drop)
est trop long, nous pourrions plutôt envisager d'ajouter un RFC pour les itérateurs destinés à
il y a un effet secondaire en ajoutant une méthode comme complete()
à l'itérateur (ou bien drain()
mais ceci
est une discussion complètement différente)
autres??
Je ne trouve pas le raisonnement d'origine, mais je me souviens qu'il y avait aussi un problème avec les adaptateurs fonctionnant avec un DrainFilter
qui ne s'exécute pas jusqu'à la fin.
Voir aussi https://github.com/rust-lang/rust/issues/43244#issuecomment -394405057
Bon point, par exemple find
ferait vidanger le drain juste jusqu'à ce qu'il frappe le premier
match, similar all
, any
court-circuit, ce qui peut être assez déroutant
wrt. drainer.
Hm, je devrais peut-être changer d'avis. À travers cela pourrait être un problème général
avec des itérateurs ayant des effets secondaires et peut-être devrions-nous envisager une solution générale
(indépendamment de ce problème de suivi) comme un adaptateur .allways_complete()
.
Personnellement, je n'ai trouvé aucune raison de sécurité pour laquelle la vidange doit être terminée, mais comme je l'ai écrit ici quelques messages ci-dessus, les effets secondaires sur next()
interagissent de manière sous-optimale avec des adaptateurs tels que take_while
, peekable
et skip_while
.
Cela soulève également les mêmes problèmes que mon RFC sur le drain non auto-épuisant et son adaptateur iter auto-épuisant compagnon RFC .
Il est vrai que drain_filter
peut facilement provoquer des abandons, mais pouvez-vous montrer un exemple où cela viole la sécurité ?
Oui, je l'ai déjà fait : play.rust-lang.org
C'est quoi ça :
#![feature(drain_filter)]
use std::panic::catch_unwind;
struct PrintOnDrop {
id: u8
}
impl Drop for PrintOnDrop {
fn drop(&mut self) {
println!("dropped: {}", self.id)
}
}
fn main() {
println!("-- start --");
let _ = catch_unwind(move || {
let mut a: Vec<_> = [0, 1, 4, 5, 6].iter()
.map(|&id| PrintOnDrop { id })
.collect::<Vec<_>>();
let drain = a.drain_filter(|dc| {
if dc.id == 4 { panic!("let's say a unwrap went wrong"); }
dc.id < 4
});
drain.for_each(::std::mem::drop);
});
println!("-- end --");
//output:
// -- start --
// dropped: 0 <-\
// dropped: 1 \_ this is a double drop
// dropped: 0 _ <-/
// dropped: 5 \------ here 4 got leaked (kind fine)
// dropped: 6
// -- end --
}
Mais c'est une réflexion interne sur la mise en œuvre, qui a mal tourné.
Fondamentalement, la question ouverte est de savoir comment gérer le panic
d'une fonction de prédicat :
Une autre question est de savoir si c'est une bonne idée d'exécuter des fonctions qui peuvent être considérées comme une entrée utilisateur api sur drop
en général, mais c'est le seul moyen de ne pas rendre find
, any
, etc. déroutants.
Peut-être qu'une considération pourrait être quelque chose comme:
next
, le désactiver avant de revenir de next
Il y a peut-être une meilleure solution.
Quel qu'il soit, il doit être documenté dans rustdoc une fois implémenté.
@dathinab
Ouais, je l'ai déjà fait
Les fuites sont indésirables mais bonnes et peuvent être difficiles à éviter ici, mais une double chute ne l'est certainement pas. Bonne prise! Souhaitez-vous signaler un problème distinct concernant ce problème de sécurité ?
drain_filter
effectue-t-il des réaffectations chaque fois qu'il supprime un élément de la collection ? Ou il ne réaffecte qu'une seule fois et fonctionne comme std::remove
et std::erase
(par paire) en C++ ? Je préférerais un tel comportement à cause d'exactement une allocation: nous plaçons simplement nos éléments à la fin de la collection, puis nous les supprimons à la taille appropriée.
Aussi, pourquoi il n'y a pas try_drain_filter
? Qui renvoie le type Option
et la None
si nous devons nous arrêter ? J'ai une très grande collection et cela n'a aucun sens de continuer pour moi alors que j'ai déjà ce dont j'avais besoin.
La dernière fois que j'ai pris un code, il a fait quelque chose comme : a créé un "écart"
lors du déplacement des éléments et déplacer un élément qui n'est pas vidangé vers le
début de l'écart lorsqu'il en trouve un. Avec cela chaque élément qui doit être
déplacé (soit vers l'extérieur, soit vers un nouvel emplacement dans le tableau) n'est déplacé qu'une seule fois.
Aussi comme par exemple remove
il ne réaffecte pas. La partie libérée devient simplement
partie de la capacité inutilisée.
Le ven. 10 août 2018, 07:11, Victor Polevoy [email protected] a écrit :
Est-ce que drain_filter effectue des réallocations à chaque fois qu'il supprime un élément de
collection? Ou il ne réaffecte qu'une seule fois et fonctionne comme std :: remove
et std::erase (par paire) en C++ ? Je préférerais un tel comportement à cause de
exactement une affectation : nous mettons simplement nos éléments en fin de collection
puis supprime le rétrécir à la bonne taille.Aussi, pourquoi il n'y a pas de try_drain_filter ? Qui renvoie le type d'option, et
Aucune valeur si nous devions arrêter? J'ai une très grande collection et c'est
inutile de continuer pour moi alors que j'ai déjà ce dont j'avais besoin.—
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-411977001 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AHR0kdOZm4bj6iR9Hj831Qh72d36BxQSks5uPRYNgaJpZM4OY1me
.
@rustonaut merci. Quelle est votre opinion sur try_drain_filter
? :)
PS Je viens de regarder le code aussi, il semble que cela fonctionne comme nous le voulions.
Il avance élément par élément lors de l'itération, donc normalement vous pouvez vous attendre
qu'il arrête d'itérer lorsqu'il est abandonné, mais il a été jugé trop
déroutant de sorte qu'il s'écoule jusqu'à la fin lorsqu'il est lâché.
(Ce qui augmente considérablement le capot probable des doubles paniques et des trucs
comme ça).
Il est donc peu probable que vous obteniez une version d'essai qui se comporte comme vous
attendre.
Par souci d'équité, s'arrêter tôt lors de l'itération peut être déroutant dans
certaines situations, par exemple thing.drain_where(|x| x.is_malformed()).any(|x|
x.is_dangerus())
ne draineraient pas tous les malformés mais juste jusqu'à ce que l'un des
trouvé qui est aussi dangereux. (L'implémentation actuelle draine tous les fichiers malformés
en poursuivant l'égouttage goutte à goutte).
Le ven. 10 août 2018, 10 h 52, Victor Polevoy [email protected] a écrit :
@rustonaut https://github.com/rustonaut merci. Quel est ton opinion
à propos de try_drain_filter ? :)—
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-412020490 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AHR0kcEMHCayqvhI6D4LK4ITG2x5di-9ks5uPUnpgaJpZM4OY1me
.
Je pense que ce serait plus polyvalent:
fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>
Salut, je cherchais la fonctionnalité drain_filter
pour HashMap
mais elle n'existe pas, et on m'a demandé d'ouvrir un problème quand j'ai trouvé celui-ci. Cela devrait-il être dans un numéro séparé?
Est-ce que quelque chose bloque actuellement cette stabilisation ? Est-ce toujours se détendre comme indiqué ci-dessus ?
Cela semble être une fonctionnalité assez petite, et elle est dans les limbes depuis plus d'un an.
Je pense que ce serait plus polyvalent:
fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>
Je ne pense pas que ce soit mieux qu'une composition de drain_filter
et map
.
Est-ce toujours se détendre comme indiqué ci-dessus ?
Il semble y avoir un choix difficile entre ne pas vider tous les éléments correspondants si l'itération s'arrête tôt, ou une panique potentielle pendant le déroulement si le filtrage et le drainage jusqu'à la fin sont effectués à la baisse d'un DrainFilter
.
Je pense que cette fonctionnalité est en proie à des problèmes de toute façon et en tant que telle ne devrait pas être stabilisée.
Y a-t-il un problème particulier à ce qu'il se comporte différemment lors du déroulement ?
Possibilités :
Le contre-argument le plus compréhensible auquel je puisse penser est ce code drop
qui dépend de l'invariant généralement fourni par drain_filter
(que, à la fin, les éléments du vec seront exactement ceux qui a échoué la condition) peut être arbitrairement éloigné du code (probablement normal, code sûr) qui utilise drain_filter
.
Cependant, supposons qu'il y ait eu un tel cas. Ce code sera bogué, peu importe comment l'utilisateur l'écrit. Par exemple, s'ils écrivent une boucle impérative qui est allée à l'envers et des éléments supprimés par échange, alors si leur condition peut paniquer et que leur implémentation de suppression dépend fortement de la condition de filtre fausse, le code a toujours un bogue. Avoir une fonction comme drop_filter
dont la documentation peut attirer l'attention sur ce cas limite semble être une amélioration en comparaison.
De plus, merci, j'ai trouvé cet exemple de terrain de jeu publié plus tôt dans le fil qui démontre que l'implémentation actuelle double toujours les éléments. (il ne peut donc certainement pas être stabilisé tel quel !)
Cela vaut peut-être la peine d'ouvrir un problème séparé pour le bogue de solidité? Cela peut alors être marqué comme I-malsain.
Autant que je sache, vous ne pouvez pas marquer ou aussi malsain que la double panique _est sonore_
juste très gênant car il avorte. Aussi loin que je m'en souvienne
possibilité de double panique n'est pas un bug mais le comportement implicitement mais
sciemment choisi.
Les options sont essentiellement :
Les problèmes sont :
Ou avec d'autres termes 1. conduit à la confusion dans les cas d'utilisation normaux, 2. peut conduire
abandonner si le prédicat peut paniquer 3.,4. fais en sorte que tu ne puisses pas vraiment
utilisez-le dans une méthode drop, mais comment faites-vous maintenant une fonction que vous utilisez là-bas
ne l'utilise pas en interne.
À la suite de cette option 3.,4. sont interdits. Les problèmes avec l'option 2. sont
plus rares que celles de 1. donc 2. a été choisi.
À mon humble avis, il serait préférable d'avoir une API drain + drain_filter qui ne fonctionne pas
à l'achèvement sur drop + un combinateur d'itérateur général qui s'exécute jusqu'à
complétion sur chute + une méthode qui complète un itérateur mais supprime tout
éléments restants. Le problème est que drain est déjà stable, l'itérateur
le combinateur ajoute une surcharge car il doit fusionner l'itérateur interne et le drain
n'est peut-être pas le nom le plus approprié.
Le lundi 20 mai 2019, 09h28, Ralf Jung [email protected] a écrit :
Cela vaut peut-être la peine d'ouvrir un problème séparé pour le bogue de solidité? Qui peut
alors être marqué comme I-unsound.—
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/43244?email_source=notifications&email_token=AB2HJEL7FS6AA2A2KF5U2S3PWJHK7A5CNFSM4DTDLGPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVX5RWA#issuecom
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AB2HJEMHQFRTCH6RCQQ64DTPWJHK7ANCNFSM4DTDLGPA
.
Les doubles gouttes ne sont cependant pas saines.
Créé #60977 pour le problème de solidité
Merci, je me sens stupide d'avoir lu double drop comme double panique :man_facepalming: .
- => Différence inattendue entre chute dans et hors panique. Juste
considérez quelqu'un _utilisant_ drain_filter dans une fonction drop.
3.,4. fais en sorte que tu ne puisses pas vraiment
utilisez-le dans une méthode drop, mais comment faites-vous maintenant une fonction que vous utilisez là-bas
ne l'utilise pas en interne.
Ce n'est toujours pas un gros problème pour moi.
Si quelqu'un utilise drain_filter
dans une implémentation Drop avec une condition qui peut paniquer, le problème n'est pas qu'il a choisi d'utiliser drain_filter
;De même, peu importe qu'une méthode utilise drain_filter
en interne ;
Désolé, j'ai répondu trop tôt. Je pense que je vois ce que tu veux dire maintenant. Je vais y réfléchir un peu plus.
D'accord, donc votre argument est que le code à l'intérieur d'un drop
impl qui utilise drain_filter
peut mystérieusement se casser s'il s'exécute pendant le déroulement. (il ne s'agit pas de paniquer drain_filter
, mais d' autres paniques de code qui provoquent l'exécution de drain_filter
) :
impl Drop for Type {
fn drop(&mut self) {
self.vec.drain_filter(|x| x == 3);
// Do stuff that assumes the vector has no 3's
...
}
}
Cet impl de chute se comporterait soudainement mal pendant le déroulement.
C'est en effet un argument convaincant contre le fait que DrainFilter détecte naïvement si le thread actuel panique.
La terminologie drain_filter
fait le plus depuis pour moi. Étant donné que nous avons déjà drain
pour supprimer tous les éléments, sélectionner les éléments à supprimer serait un filter
. Lorsqu'ils sont associés, la dénomination semble très cohérente.
Le correctif pour le problème de solidité de la double panique laisse le reste du Vec
intact en cas de panique dans le prédicat. Les éléments sont décalés pour combler le vide, mais sont autrement laissés seuls (et abandonnés via Vec::drop
pendant le déroulement ou autrement manipulés par l'utilisateur si la panique est prise).
Laisser tomber un vec::DrainFilter
prématurément continue de se comporter comme s'il était entièrement consommé (ce qui équivaut à vec::Drain
). Si le prédicat panique pendant vec::Drain::drop
, les éléments restants sont décalés normalement, mais aucun autre élément n'est supprimé et le prédicat n'est pas appelé à nouveau. Il se comporte essentiellement de la même manière, qu'une panique dans le prédicat se produise lors d'une consommation normale ou lorsque le vec::DrainFilter
est abandonné.
En supposant que le correctif pour le trou de solidité est correct, quoi d'autre retient la stabilisation de cette fonctionnalité ?
Vec::drain_filter
peut-il être stabilisé indépendamment de LinkedList::drain_filter
?
Le problème avec la terminologie drain_filter
, c'est qu'avec drain_filter
, il y a vraiment deux sorties : la valeur de retour et la collection d'origine, et il n'est pas vraiment clair de quel côté le "filtré" les articles continuent. Je pense que même filtered_drain
est un peu plus clair.
il n'est pas vraiment clair de quel côté les éléments "filtrés" vont
Vec::drain
crée un précédent. Vous spécifiez la plage des éléments que vous souhaitez _supprimer_. Vec::drain_filter
fonctionne de la même manière. Vous identifiez les éléments que vous souhaitez _supprimer_.
et il n'est pas vraiment clair de quel côté les éléments "filtrés" vont
Je suis d'avis que c'est déjà vrai pour Iterator::filter
, donc je me suis résigné à devoir regarder les docs / écrire un test chaque fois que j'utilise ça. Cela ne me dérange pas la même chose pour drain_filter
.
J'aurais aimé que nous ayons choisi la terminologie select
et reject
de Ruby, mais ce navire a navigué depuis longtemps.
Des progrès à ce sujet? Le nom est-il la seule chose qui reste dans les limbes ?
Y a-t-il quelque chose qui empêche que cela se stabilise?
Il semble que l'impl DrainFilter
de Drop
perdra des éléments si l'un de leurs destructeurs panique le prédicat panique. C'est la cause première de https://github.com/rust-lang/rust/issues/52267. Sommes-nous sûrs de vouloir stabiliser une API comme ça ?
Peu importe, cela a été corrigé par https://github.com/rust-lang/rust/pull/61224 apparemment.
Je vais me pencher un peu sur ce problème de suivi aussi, j'aimerais voir cette fonctionnalité stable :D Y a-t-il des bloqueurs ?
cc @Dylan-DPC
Une décision a-t-elle déjà été prise en faveur ou contre le fait que drain_filter
prenne un paramètre RangeBounds
, comme le fait drain
? Passer ..
semble assez facile lorsque vous voulez filtrer le vecteur entier, donc je serais probablement en faveur de l'ajouter.
Je pense que ce serait plus polyvalent:
fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>
Presque, la version plus générale prendrait un FnMut(T) -> Option<U>
, comme le font Iterator::{filter_map, find_map, map_while}
. Je n'ai aucune idée si cela vaut la peine de généraliser filter_map
de cette façon, mais cela pourrait valoir la peine d'être considéré.
Je suis arrivé ici car je cherchais plus ou moins exactement la méthode @jethrogb suggérée plus haut :
fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F>
where F: FnMut(T) -> Option<T>
La seule différence avec ce que j'avais en tête (que j'appelais update
dans ma tête) était que je n'avais pas pensé à lui faire renvoyer un itérateur drainant, mais cela semble être une nette amélioration, car il fournit une interface unique et raisonnablement simple qui prend en charge la mise à jour des éléments en place, la suppression des éléments existants et la livraison des éléments supprimés à l'appelant.
Presque, la version plus générale prendrait un
FnMut(T) -> Option<U>
, comme le fontIterator::{filter_map, find_map, map_while}
. Je n'ai aucune idée si cela vaut la peine de généraliserfilter_map
de cette façon, mais cela pourrait valoir la peine d'être considéré.
La fonction doit retourner Option<T>
car les valeurs qu'elle produit sont stockées dans un Vec<T>
.
@ johnw42 Je ne suis pas sûr de suivre, la Some
ne serait-elle pas immédiatement renvoyée par l'itérateur?
En fait, je suppose que la valeur d'entrée de cette fonction doit toujours être &T
ou &mut T
au lieu de T
au cas où vous ne voudriez pas la vider. Ou peut-être que la fonction pourrait être quelque chose comme FnMut(T) -> Result<U, T>
. Mais je ne vois pas pourquoi le type d'élément ne pourrait pas être un autre type.
@timvermeulen Je pense que nous interprétons la proposition différemment.
La façon dont je l'ai interprété, la Some
est stockée dans le Vec
, et None
signifie que la valeur d'origine est renvoyée par l'itérateur. Cela permet à la fermeture de mettre à jour la valeur en place ou de la déplacer hors de Vec
. En écrivant ceci, j'ai réalisé que ma version n'ajoutait vraiment rien car vous pouvez l'implémenter en termes de drain_filter
:
fn drain_filter_map<F>(
&mut self,
mut f: F,
) -> DrainFilter<T, impl FnMut(&mut T) -> bool>
where
F: FnMut(&T) -> Option<T>,
{
self.drain_filter(move |value| match f(value) {
Some(new_value) => {
*value = new_value;
false
}
None => true,
})
}
Inversement, je pensais que votre interprétation n'est pas très utile car elle équivaut à mapper le résultat de drain_filter
, mais j'ai essayé de l'écrire, et ce n'est pas le cas, pour la même raison que filter_map
n'est pas t équivalent à appeler filter
suivi de map
.
@ johnw42 Ah, oui, je pensais que vous vouliez que None
signifie que la valeur devrait rester dans le Vec
.
Il semble donc que FnMut(T) -> Result<U, T>
serait le plus général, même si ce n'est probablement pas très ergonomique. FnMut(&mut T) -> Option<U>
n'est pas vraiment une option car cela ne vous permettrait pas de vous approprier les T
dans le cas général. Je pense que FnMut(T) -> Result<U, T>
et FnMut(&mut T) -> bool
sont les seules options.
@timvermeulen J'ai commencé à dire quelque chose plus tôt à propos d'une solution "la plus générale", et ma solution "la plus générale" était différente de la vôtre, mais je suis arrivé à la même conclusion, à savoir qu'essayer de rendre une fonction trop générale aboutit à quelque chose que vous ne voudrait pas vraiment utiliser.
Bien qu'il soit peut-être encore utile de créer une méthode très générale sur laquelle les utilisateurs avancés peuvent créer des abstractions plus agréables. Pour autant que je sache, l'intérêt de drain
et drain_filter
n'est pas qu'il s'agisse d'API particulièrement ergonomiques - ce n'est pas le cas - mais qu'elles prennent en charge les cas d'utilisation qui se produisent dans pratique, et qui ne peut pas être écrit autrement sans beaucoup de mouvements redondants (ou en utilisant des opérations non sûres).
Avec drain
, vous obtenez les belles propriétés suivantes :
Vec
n'a pas besoin de prendre en charge Copy
ou Clone
.Vec
lui-même n'a besoin d'être allouée ou libérée.Vec
sont déplacées au plus une fois.Avec drain_filter
, vous avez la possibilité de supprimer un ensemble arbitraire d'éléments du Vec
plutôt qu'une simple plage contiguë. Un avantage moins évident est que même si une plage contiguë d'éléments est supprimée, drain_filter
peut toujours offrir une amélioration des performances si trouver la plage à passer à drain
impliquerait de faire une passe séparée sur le Vec
pour inspecter son contenu. Parce que l'argument de la fermeture est un &mut T
, il est même possible de mettre à jour les éléments laissés dans le Vec
. Hourra !
Voici quelques autres choses que vous voudrez peut-être faire avec une opération sur place comme drain_filter
:
Et voici mon analyse de chacun:
Vec
.Vec
sont déplacés au plus une fois, et il peut être nécessaire de réallouer le tampon. Si vous avez besoin de faire quelque chose comme ça, ce n'est pas nécessairement plus efficace que de simplement construire un tout nouveau Vec
, et cela pourrait être pire.Vec
est organisé de telle manière que vous pouvez simplement ignorer une grande partie des éléments sans vous arrêter pour les inspecter. Je ne l'ai pas inclus dans mon exemple de code ci-dessous, mais il pourrait être pris en charge en modifiant la fermeture pour renvoyer un usize
supplémentaire en spécifiant le nombre d'éléments suivants à sauter avant de continuer.usize
ci-dessus pourrait être remplacé par un choix de Keep(usize)
ou Drop(usize)
(où Keep(0)
et Drop(0)
sont sémantiquement équivalent).Je pense que nous pouvons prendre en charge les cas d'utilisation essentiels en faisant en sorte que la fermeture renvoie une énumération avec 4 cas:
fn super_drain(&mut self, f: F) -> SuperDrainIter<T>
where F: FnMut(&mut T) -> DrainAction<T>;
enum DrainAction<T> {
/// Leave the item in the Vec and don't return anything through
/// the iterator.
Keep,
/// Remove the item from the Vec and return it through the
/// iterator.
Remove,
/// Remove the item from the Vec, return it through the iterator,
/// and swap a new value into the location of the removed item.
Replace(T),
/// Leave the item in place, don't return any more items through
/// the iterator, and don't call the closure again.
Stop,
}
Une dernière option que j'aimerais présenter est de se débarrasser complètement de l'itérateur, de passer des éléments à la fermeture par valeur et de permettre à l'appelant de laisser un élément inchangé en le remplaçant par lui-même :
fn super_drain_by_value(&mut self, f: F)
where F: FnMut(T) -> DrainAction<T>;
enum DrainAction<T> {
/// Don't replace the item removed from the Vec.
Remove,
/// Replace the item removed from the Vec which a new item.
Replace(T),
Stop,
}
J'aime cette approche parce qu'elle est simple et qu'elle prend en charge tous les mêmes cas d'utilisation. L'inconvénient potentiel est que même si la plupart des éléments sont laissés en place, ils doivent toujours être déplacés dans le cadre de la pile de la fermeture, puis reculés lorsque la fermeture revient. On pourrait espérer que ces mouvements pourraient être optimisés de manière fiable lorsque la fermeture renvoie juste son argument, mais je ne suis pas sûr que ce soit quelque chose sur lequel nous devrions compter. Si d'autres personnes l'aiment suffisamment pour l'inclure, je pense que update
serait un bon nom, car si je ne me trompe pas, il peut être utilisé pour implémenter n'importe quelle mise à jour sur place en un seul passage d'un Vec
.
(Au fait, j'ai complètement ignoré les listes liées ci-dessus parce que je les avais complètement oubliées jusqu'à ce que je regarde le titre de ce numéro. Si nous parlons d'une liste liée, cela change l'analyse des points 4-6, donc je pense que c'est différent L'API serait appropriée pour les listes liées.)
@johnw42 vous pouvez déjà faire 3. si vous avez une référence mutable, en utilisant mem::replace
ou mem::take
.
@johnw42 @jplatte
(3) n'a vraiment de sens que si nous permettons au type d'élément du Iterator
renvoyé d'être différent du type d'élément de la collection.
(3) est un cas particulier, car vous renvoyez à la fois l'élément du Iterator
et remettez un nouvel élément dans le Vec
.
Bikeshedding : j'inverserais un peu la fonction de Replace(T)
et la remplacerais par PushOut(T)
, dans le but de "soumettre" la valeur interne de PushOut
à l'itérateur, tout en gardant le élément d'origine (paramètre) dans le Vec
.
Stop
devrait probablement avoir la capacité de retourner un type Error
(ou fonctionner un peu comme try_fold
?).
J'ai implémenté ma fonction super_drain_by_value
hier soir, et j'ai appris plusieurs choses.
L'élément principal devrait probablement être que, au moins par rapport Vec
, tout ce dont nous parlons ici est dans la catégorie « agréable à avoir » (par opposition à l'ajout d'une capacité fondamentalement nouvelle), parce que Vec
fournit déjà essentiellement un accès direct en lecture et en écriture à tous ses champs via l'API existante. Dans la version stable, il y a une petite mise en garde que vous ne pouvez pas observer le champ de pointeur d'un Vec
vide, mais la méthode instable into_raw_parts
supprime cette restriction. Ce dont nous parlons vraiment, c'est d'élargir l'ensemble des opérations qui peuvent être effectuées efficacement par un code sécurisé.
En termes de génération de code, j'ai trouvé que dans les cas faciles (par exemple Vec<i32>
), les mouvements redondants dans et hors de Vec
ne posent aucun problème, et les appels qui reviennent à des choses simples comme un no-op ou la troncation des Vec
sont transformés en code trop simple pour être amélioré (zéro et trois instructions, respectivement). La mauvaise nouvelle est que pour les cas les plus difficiles, ma proposition et la méthode drain_filter
font beaucoup de copies inutiles, ce qui va largement à l'encontre de l'objectif des méthodes. J'ai testé cela en regardant le code assembleur généré pour un Vec<[u8; 1024]>
, et dans les deux cas, chaque itération a deux appels à memcpy
qui ne sont pas optimisés. Même un appel sans opération finit par copier le tampon entier deux fois !
Au niveau de l'ergonomie, mon API, qui a l'air plutôt sympa à première vue, l'est moins en pratique ; renvoyer une valeur enum à partir de la fermeture devient assez verbeux dans tous les cas sauf les plus simples, et la variante que j'ai proposée où la fermeture renvoie une paire de valeurs enum est encore plus laide.
J'ai également essayé d'étendre DrainAction::Stop
pour porter une R
qui est renvoyée de super_drain_by_value
en tant que Option<R>
, et c'est encore pire, car dans le (vraisemblablement typique) cas où la valeur renvoyée n'est pas nécessaire, le compilateur ne peut pas déduire R
et vous devez annoter explicitement le type d'une valeur que vous n'utilisez même pas. Pour cette raison, je ne pense pas que ce soit une bonne idée de prendre en charge le retour d'une valeur de la fermeture à l'appelant de super_drain_by_value
; c'est à peu près analogue à la raison pour laquelle une construction loop {}
peut renvoyer une valeur, mais tout autre type de boucle évalue à ()
.
En ce qui concerne la généralité, j'ai réalisé qu'il y avait en fait deux cas de résiliation prématurée : un où le reste du Vec
est abandonné, et un autre là où il est laissé en place. Si la terminaison prématurée ne porte pas de valeur (comme je pense qu'elle ne devrait pas), elle devient sémantiquement équivalente à retourner Keep(n)
ou Drop(n)
, où n
est le nombre de éléments non encore examinés. Cependant, je pense que la résiliation prématurée doit être traitée comme un cas distinct, car par rapport à l'utilisation Keep
/ Drop
, il est plus facile à utiliser via un chemin de code plus simple.
Pour rendre l'API un peu plus conviviale, je pense qu'une meilleure option serait de faire en sorte que la fermeture renvoie ()
et de lui passer un objet d'assistance (que j'appellerai ici un "updater") qui peut être utilisé pour inspecter chaque élément du Vec
et contrôler ce qui lui arrive. Ces méthodes peuvent avoir des noms familiers comme borrow
, borrow_mut
et take
, avec des méthodes supplémentaires comme keep_next(n)
ou drop_remainder()
. En utilisant ce type d'API, la fermeture est beaucoup plus simple dans les cas simples et pas plus complexe dans les cas complexes. En faisant en sorte que la plupart des méthodes de mise à jour prennent self
par valeur, il est facile d'empêcher l'appelant de faire des choses comme appeler take
plus d'une fois, ou donner des instructions contradictoires sur ce qu'il faut faire dans les itérations suivantes.
Mais on peut encore mieux faire ! Je me suis rendu compte ce matin que, comme c'est si souvent le cas, ce problème est analogue à celui qui a été définitivement résolu dans les langages fonctionnels, et nous pouvons le résoudre avec une solution analogue. Je parle des API "zipper", décrites pour la première fois dans ce court article avec un exemple de code en OCaml, et décrites ici avec du code Haskell et des liens vers d'autres articles pertinents. Les fermetures à glissière fournissent un moyen très général de parcourir une structure de données et de la mettre à jour "sur place" en utilisant toutes les opérations prises en charge par cette structure de données particulière. Une autre façon de penser est qu'une fermeture éclair est une sorte d'itérateur turbocompressé avec des méthodes supplémentaires pour effectuer des opérations sur un type spécifique de structure de données.
Dans Haskell, vous obtenez une sémantique "en place" en faisant de la fermeture éclair une monade ; dans Rust, vous pouvez faire la même chose en utilisant des durées de vie en faisant en sorte que la fermeture éclair contienne une référence mut
au Vec
. Une fermeture éclair pour un Vec
est très similaire au programme de mise à jour que j'ai décrit ci-dessus, sauf qu'au lieu de le passer à plusieurs reprises à une fermeture, le Vec
fournit simplement une méthode pour créer une fermeture éclair sur lui-même, et la fermeture éclair a un accès exclusif au Vec
tant qu'il existe. L'appelant devient alors responsable de l'écriture d'une boucle pour parcourir le tableau, appelant une méthode à chaque étape pour soit supprimer l'élément actuel du Vec
, soit le laisser en place. Une résiliation anticipée peut être implémentée en appelant une méthode qui consomme la fermeture éclair. Étant donné que la boucle est sous le contrôle de l'appelant, il devient possible de faire des choses comme traiter plus d'un élément à chaque itération de la boucle, ou gérer un nombre fixe d'éléments sans utiliser de boucle du tout.
Voici un exemple très artificiel montrant certaines des choses qu'une fermeture éclair peut faire :
/// Keep the first 100 items of `v`. In the next 100 items of `v`,
/// double the even values, unconditionally keep anything following an
/// even value, discard negative values, and move odd values into a
/// new Vec. Leave the rest of `v` unchanged. Return the odd values
/// that were removed, along with a boolean flag indicating whether
/// the loop terminated early.
fn silly(v: &mut Vec<i32>) -> (bool, Vec<i32>) {
let mut odds = Vec::new();
// Create a zipper, which get exclusive access to `v`.
let mut z = v.zipper();
// Skip over the first 100 items, leaving them unchanged.
z.keep_next(100);
let stopped_early = loop {
if let Some(item /* &mut i32 */) = z.current_mut() {
if *item < 0 {
// Discard the value and advance the zipper.
z.take();
} else if *item % 2 == 0 {
// Update the item in place.
*item *= 2;
// Leave the updated item in `v`. This has the
// side-effect of advancing `z` to the next item.
z.keep();
// If there's another value, keep it regardless of
// what it is.
if z.current().is_some() {
z.keep();
}
} else {
// Move an odd value out of `v`.
odds.push(z.take());
}
if z.position() >= 200 {
// This consumes `z`, so we must break out of the
// loop!
z.keep_rest();
break true;
}
} else {
// We've reached the end of `v`.
break false;
}
}
(stopped_early, odds)
// If the zipper wasn't already consumed by calling
// `z.keep_rest()`, the zipper is dropped here, which will shift
// the contents of `v` to fill in any gaps created by removing
// values.
}
À titre de comparaison, voici plus ou moins la même fonction utilisant drain_filter
, sauf qu'elle prétend seulement s'arrêter tôt. C'est à peu près la même quantité de code, mais à mon humble avis, c'est beaucoup plus difficile à lire car la signification de la valeur renvoyée par la fermeture n'est pas évidente, et elle utilise des drapeaux booléens mutables pour transporter des informations d'une itération à la suivante, où la fermeture éclair version réalise la même chose avec le flux de contrôle. Étant donné que les éléments supprimés sont toujours générés par l'itérateur, nous avons besoin d'une étape de filtrage distincte pour supprimer les valeurs négatives de la sortie, ce qui signifie que nous devons vérifier les valeurs négatives à deux endroits au lieu d'un. C'est aussi un peu moche qu'il doive garder une trace de la position dans v
; l'implémentation de drain_filter
a cette information, mais l'appelant n'y a pas accès.
fn drain_filter_silly(v: &mut Vec<i32>) -> (bool, Vec<i32>) {
let mut position: usize = 0;
let mut keep_next = false;
let mut stopped_early = false;
let removed = v.drain_filter(|item| {
position += 1;
if position <= 100 {
false
} else if position > 200 {
stopped_early = true;
false
} else if keep_next {
keep_next = false;
false
} else if *item >= 0 && *item % 2 == 0 {
*item *= 2;
false
} else {
true
}
}).filter(|item| item >= 0).collect();
(stopped_early, removed)
}
@ johnw42 Votre message précédent m'a rappelé la caisse scanmut
, en particulier la structure Remover
, et le concept de "fermeture éclair" que vous avez mentionné semble très similaire ! Cela semble beaucoup plus ergonomique qu'une méthode qui prend une fermeture lorsque vous voulez un contrôle total.
Quoi qu'il en soit, cela n'est probablement pas très pertinent pour savoir si drain_filter
doit être stabilisé, car nous pouvons toujours échanger les éléments internes plus tard. drain_filter
lui-même sera toujours très utile en raison de sa commodité. Le seul changement que j'aimerais encore voir avant la stabilisation est un paramètre RangeBounds
.
@timvermeulen Je pense qu'il est logique d'ajouter un paramètre RangeBounds
, mais conservez la signature de fermeture actuelle ( F: FnMut(&mut T) -> bool
).
Vous pouvez toujours post-traiter les éléments drainés avec filter_map
ou ce que vous voulez.
(Pour moi, il est très important que la fermeture permette de muter l'élément, car retain
ne le permet pas (il a été stabilisé avant que cette erreur ne soit découverte).)
Oui, cela semble être le parfait équilibre entre commodité et utilité.
@timvermeulen Ouais, je m'écartais assez du sujet principal.
Une chose que j'ai remarquée et qui est pertinente pour le sujet d'origine est qu'il est un peu difficile de se rappeler ce que signifie la valeur de retour de la fermeture - indique-t-elle s'il faut conserver l'élément ou le supprimer ? Je pense qu'il serait utile que les docs précisent que v.drain_filter(p)
équivaut à v.iter().filter(p)
avec des effets secondaires.
Avec filter
, l'utilisation d'une valeur booléenne n'est toujours pas idéale pour plus de clarté, mais c'est une fonction très connue, et à mon humble avis, il est au moins quelque peu intuitif que le prédicat réponde à la question "devrais-je garder cela?" plutôt que "devrais-je jeter cela?" Avec drain_filter
, la même logique s'applique si vous y réfléchissez du point de vue de l'itérateur, mais si vous y réfléchissez du point de vue de l'entrée Vec
, la question devient "ne devrais-je PAS garde ça?"
Quant à la formulation exacte, je propose de renommer le paramètre filter
en predicate
(pour correspondre à Iterator::filter
) et d'ajouter cette phrase quelque part dans la description :
Pour se rappeler comment la valeur de retour de
predicate
est utilisée, il peut être utile de garder à l'esprit quedrain_filter
est identique àIterator::filter
avec l'effet secondaire supplémentaire de supprimer la valeur sélectionnée articles à partir deself
.
@ johnw42 Oui, bon point. Je pense qu'un nom comme drain_where
serait beaucoup plus clair.
Si vous voulez nommer le bikeshedding; assurez- vous d'avoir lu tous les commentaires ; même cachés. De nombreuses variantes ont déjà été proposées, par exemple https://github.com/rust-lang/rust/issues/43244#issuecomment -331559537
Mais… il faut qu'il s'appelle draintain()
! Aucun autre nom n'est aussi beau !
Je suis assez intéressé par cette question, et j'ai lu tout le fil, alors autant essayer de résumer ce que tout le monde a dit, dans l'espoir d'aider à stabiliser cela. J'ai ajouté certains de mes propres commentaires en cours de route, mais j'ai essayé de les garder aussi neutres que possible.
Voici un résumé sans opinion des noms que j'ai vus proposés :
drain_filter
: Le nom utilisé dans l'implémentation actuelle. Cohérent avec d'autres noms tels que filter_map
. A l'avantage d'être analogue à drain().filter()
, mais avec plus d'effets secondaires.drain_where
: a l'avantage d'indiquer si true
entraîne un drainage _out_ ou un filtrage _in_, ce qui peut être difficile à retenir avec d'autres noms. Il n'y a pas de précédent dans std
pour le suffixe _where
, mais il y a beaucoup de précédents pour des suffixes similaires.drain().where()
, puisque where
est déjà un mot-clé.drain_retain
: cohérent avec retain
, mais retain
et drain
ont des interprétations opposées des valeurs booléennes renvoyées par la fermeture, ce qui peut prêter à confusion.filtered_drain
drain_if
drain_when
remove_if
Il peut être utile d'ajouter un argument de plage pour la cohérence avec drain
.
Deux formats de fermeture ont été suggérés, FnMut(&mut T) -> bool
et FnMut(T) -> Result<T, U>
. Ce dernier est plus souple, mais aussi plus maladroit.
L'inversion de la condition booléenne ( true
signifie "garder dans les Vec
") pour être cohérent avec retain
a été discuté, mais cela ne serait pas cohérent avec drain
( true
signifie "vider du Vec
").
Lorsque la fermeture du filtre panique, l'itérateur DrainFilter
est abandonné. L'itérateur doit alors finir de vider le Vec
, mais pour ce faire, il doit appeler à nouveau la fermeture du filtre, risquant une double panique. Il existe des solutions, mais toutes sont des compromis :
Ne finissez pas de vidanger en tombant. Ceci est assez contre-intuitif lorsqu'il est utilisé avec des adaptateurs tels que find
ou all
. De plus, cela rend l'idiome v.drain_filter(...);
inutile puisque les itérateurs sont paresseux.
Terminez toujours la vidange à la goutte. Cela risque de provoquer des paniques doubles (qui se traduisent par des abandons), mais rend le comportement cohérent.
Ne terminez la vidange qu'en cas de chute si vous ne vous déroulez pas actuellement. Cela corrige entièrement les doubles paniques, mais rend le comportement de drain_filter
imprévisible : déposer DrainFilter
dans un destructeur peut _parfois_ ne pas faire son travail.
Ne terminez la vidange sur goutte que si la fermeture du filtre n'a pas paniqué. C'est le compromis actuel fait par drain_filter
. Une belle propriété de cette approche est qu'elle panique dans le "court-circuit" de fermeture du filtre, ce qui est sans doute assez intuitif.
Notez que l'implémentation actuelle est saine et ne fuit jamais tant que la structure DrainFilter
est supprimée (bien que cela puisse provoquer un abandon). Les implémentations précédentes n'étaient cependant pas sûres / sans fuite.
DrainIter
pourrait soit finir de drainer le vecteur source lorsqu'il est supprimé, soit il ne pourrait drainer que lorsque next
est appelé (itération paresseuse).
Arguments en faveur du drain-on-drop :
Cohérent avec le comportement de drain
.
Interagit bien avec d'autres adaptateurs tels que all
, any
, find
, etc...
Active l'idiome vec.drain_filter(...);
.
La fonctionnalité paresseuse pourrait être explicitement activée via des méthodes de style drain_lazy
ou un adaptateur lazy()
sur DrainIter
(et même sur Drain
, car il est rétrocompatible avec ajouter des méthodes).
Arguments en faveur de l'itération paresseuse :
Cohérent avec presque tous les autres itérateurs.
La fonctionnalité "drain-on-drop" peut être explicitement activée via des adaptateurs sur DrainIter
, ou même via un adaptateur général Iterator::exhausting
(voir RFC #2370 ).
J'ai peut-être raté certaines choses, mais au moins j'espère que cela aidera les nouveaux arrivants à parcourir le fil.
@negamartin
L'option drain-on-drop
n'exigerait-elle pas que l'itérateur renvoie une référence à l'élément au lieu de la valeur possédée ? Je pense que cela rendrait impossible l'utilisation drain_filter
comme mécanisme pour supprimer et prendre possession des éléments correspondant à une condition spécifique (ce qui était mon cas d'utilisation d'origine).
Je ne pense pas, puisque le comportement de l'implémentation actuelle est précisément drain-on-drop tout en produisant des valeurs possédées. Quoi qu'il en soit, je ne vois pas en quoi la vidange sur goutte nécessiterait des éléments d'emprunt, donc je pense que nous avons deux idées différentes sur ce que signifie la vidange sur goutte.
Juste pour être clair, quand je dis drain-on-drop, je veux seulement dire le comportement lorsque l'itérateur n'est pas entièrement consommé : tous les éléments correspondant à la fermeture doivent-ils être drainés même si l'itérateur n'est pas entièrement consommé ? Ou seulement jusqu'à l'élément qui a été consommé, laissant le reste intact ?
C'est notamment la différence entre :
let mut v = vec![1, 5, 3, 6, 4, 7];
v.drain_where(|e| *e > 4).find(|e| *e == 6);
// Drain-on-drop
assert_eq!(v, &[1, 3, 4]);
// Lazy
assert_eq!(v, &[1, 3, 4, 7]);
Juste lancer une idée, mais une autre API possible pourrait être quelque chose comme :
fn drain_filter_into<F, D>(&mut self, filter: F, drain: D)
where F: FnMut(&mut T) -> bool,
D: Extend<T>
{ ... }
C'est moins flexible que les autres options, mais cela évite le problème de savoir quoi faire quand DrainFilter
est supprimé.
J'ai l'impression que tout cela me ressemble de moins en moins à retain_mut()
( retain()
avec une référence mutable passée à la fermeture), ce qu'il était avant tout destiné à fournir . Pourrions-nous fournir retain_mut()
pour l'instant en plus de travailler sur la conception d'un drain filtré ? Ou est-ce que je manque quelque chose?
@BartMassey
c'est ce qu'il était avant tout destiné à fournir.
Je ne pense pas que ce soit le cas. J'utilise spécifiquement drain_filter
pour m'approprier des éléments en fonction des critères de filtrage. Drain
et DrainFilter
produisent l'élément alors que la conservation ne le fait pas.
@negamartin
Juste pour être clair, quand je dis drain-on-drop, je veux seulement dire le comportement lorsque l'itérateur n'est pas entièrement consommé
Ah ok. C'est mon erreur. J'ai mal compris ta définition. Je l'avais interprété comme "rien n'est retiré du vec jusqu'à ce qu'il soit abandonné", ce qui n'a pas vraiment de sens.
Arguments en faveur de l'itération paresseuse
Je pense qu'il doit être cohérent avec drain
. La RFC Iterator::exhausting
n'a pas été acceptée et il serait très étrange que drain
et drain_filter
aient des comportements de drain apparemment opposés.
@negamartin
drain_filter : le nom utilisé dans l'implémentation actuelle. Cohérent avec d'autres noms tels que filter_map. A l'avantage d'être analogue à
drain().filter()
, mais avec plus d'effets secondaires.
Ce n'est pas analogue (c'est pourquoi nous avons besoin retain_mut
/ drain_filter
):
drain().filter()
viderait même les éléments pour lesquels la fermeture du filtre renvoie false
!
Je viens de remarquer une petite ligne dans un commentaire de l'équipe lib dans #RFC 2870 :
Les possibilités peuvent inclure la "surcharge" d'une méthode en la rendant générique, ou un modèle de générateur.
Est-il rétrocompatible de rendre une méthode générique si elle accepte toujours le type concret précédent ? Si c'est le cas, je pense que ce serait la meilleure voie à suivre.
(Le modèle de construction est un peu peu intuitif avec les itérateurs, car les méthodes sur les itérateurs sont généralement des adaptateurs, pas des changeurs de comportement. De plus, il n'y a pas de précédents, par exemple chunks
et chunks_exact
sont deux méthodes distinctes , pas un combo chunks().exact()
.)
Non, pas avec la conception actuelle pour autant que je sache comme inférence de type qui
travaillait auparavant pouvait désormais échouer en raison d'une ambiguïté de type. Génétique avec défaut
les types pour les fonctions aideraient mais sont très difficiles à faire correctement.
Le ven. 12 juin 2020, 21:21, negamartin [email protected] a écrit :
Je viens de remarquer une petite ligne dans un commentaire de l'équipe lib dans #RFC 2870
https://github.com/rust-lang/rfcs/pull/2369 :Les possibilités peuvent inclure la "surcharge" d'une méthode en la rendant générique,
ou un modèle de constructeur.Est-il rétrocompatible de rendre une méthode générique si elle accepte toujours
le type de béton précédent? Si c'est le cas, je pense que ce serait la meilleure façon
effronté.(Le modèle de construction est un peu peu intuitif avec les itérateurs, car les méthodes sur
les itérateurs sont généralement des adaptateurs, pas des modificateurs de comportement. D'ailleurs il y a
aucun précédent, par exemple chunks et chunks_exact sont deux éléments distincts
méthodes, pas un combo chunks().exact().)—
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-643444213 ,
ou désabonnez-vous
https://github.com/notifications/unsubscribe-auth/AB2HJELPWXNXJMX2ZDA6F63RWJ53FANCNFSM4DTDLGPA
.
Est-il rétrocompatible de rendre une méthode générique si elle accepte toujours le type concret précédent ? Si c'est le cas, je pense que ce serait la meilleure voie à suivre.
Non, car cela casse l'inférence de type dans certains cas. Par exemple, foo.method(bar.into())
fonctionnera avec un argument de type concret, mais pas avec un argument générique.
Je pense que drain_filter tel qu'il est implémenté maintenant est très utile. Pourrait-il être stabilisé tel quel ? Si de meilleures abstractions sont découvertes à l'avenir, rien n'empêche de les introduire également.
Quel processus dois-je lancer pour essayer d'ajouter retain_mut()
, indépendamment de ce qui se passe avec drain_filter()
? Il me semble que les exigences ont divergé et qu'il serait toujours utile d'avoir retain_mut()
indépendamment de ce qui se passe avec drain_filter()
.
@BartMassey pour les nouvelles API de bibliothèque instables, je pense que faire un PR avec une implémentation devrait convenir. Il y a des instructions sur https://rustc-dev-guide.rust-lang.org/implementing_new_features.html pour implémenter la fonctionnalité et sur https://rustc-dev-guide.rust-lang.org/getting-started.html #building -and -testing-stdcorealloctestproc_macroetc pour tester vos modifications.
J'ai eu du mal avec les différences d'API entre HashMap
et BTreeMap
aujourd'hui et je voulais juste partager un avertissement que je pense qu'il est important que diverses collections s'efforcent de maintenir une API cohérente à chaque fois sens, quelque chose qui à ce stade n'est pas toujours le cas.
Par exemple, String, Vec, HashMap, HashSet, BinaryHeap et VecDeque ont une méthode retain
, mais pas LinkedList et BTreeMap. Je trouve cela particulièrement étrange car retain
semble être une méthode plus naturelle pour une LinkedList ou une Map que pour les vecteurs où la suppression aléatoire est une opération très coûteuse.
Et quand vous creusez un peu plus, c'est encore plus déroutant : la fermeture HashMap::retain
reçoit la valeur dans une référence mutable, mais les autres collections obtiennent une référence immuable (et String obtient un simple char
).
Maintenant, je vois que de nouvelles API comme drain_filter
sont ajoutées qui 1/ semblent se chevaucher avec retain
et 2/ ne sont pas stabilisées pour toutes les collections en même temps :
HashMap::drain_filter
est dans le dépôt en amont mais n'est pas encore livré avec le std AFAIK de Rust (il n'apparaît pas dans la documentation)BTreeMap::drain_filter
, Vec::drain_filter
, LinkedList::drain_filter
sont dans la norme de Rust, mais disposent d'un portailVecDeque::drain_filter
ne semble pas exister du tout, il n'apparaît pas dans la documentationString::drain_filter
n'existe pas non plusJe n'ai pas d'opinion bien arrêtée sur la meilleure façon d'implémenter ces fonctionnalités, ou si nous avons besoin drain_filter
, retain
ou les deux, mais je crois fermement que ces API doivent rester cohérentes entre les collections .
Et peut-être plus important encore, des méthodes similaires de différentes collections devraient avoir la même sémantique. Quelque chose que les implémentations actuelles de retain
violent l'OMI.
Commentaire le plus utile
Y a-t-il quelque chose qui empêche que cela se stabilise?