Rust: Problème de suivi pour Vec::drain_filter et LinkedList::drain_filter

Créé le 15 juil. 2017  ·  119Commentaires  ·  Source: rust-lang/rust

    /// 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.

A-collections B-unstable C-tracking-issue Libs-Tracked T-libs

Commentaire le plus utile

Y a-t-il quelque chose qui empêche que cela se stabilise?

Tous les 119 commentaires

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.

@Gankro existe-t-il un précédent pour l'utilisation _filter n'importe où ? Je ne sais pas non plus si c'est une raison pour ne pas utiliser la terminologie la moins ambiguë? D'autres endroits de la bibliothèque standard utilisent déjà une variété de suffixes tels que _until et _while .

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 :

  1. C'est une forme de drain donc nous utilisons drain dans le nom

  2. 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

  3. 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 :

  1. vous avez des noms de fonctions utilisant des prédicats auxquels sont associés des concepts de production/consommation (par rapport à la rouille, aux itérations). Par exemple drain , collect , fold , all , take , ...
  2. ces prédicats ont parfois des modificateurs, par exemple *_where , *_while
  3. vous avez des noms de fonctions utilisant des prédicats qui ont des propriétés de modification ( map , filter , skip , ...)

    • ici, il est vague s'il s'agit d'un élément ou d'une modification d'itération ( map vs. filter / skip )

  4. noms de fonctions chaînant plusieurs prédicats en utilisant des propriétés de modification, par exemple filter_map

    • avoir un concept d'environ apply modifier_1 and then apply modifier_2 , juste qu'il est plus rapide ou plus flexible de le faire en une seule étape

Vous pouvez parfois avoir :

  1. noms de fonctions combinant des prédicats producteurs/consommateurs avec des prédicats modificateurs (par exemple drain_filter )

    • _mais_ la plupart du temps, il est préférable/moins déroutant de les combiner avec des modificateurs (par exemple drain_where )

Normalement tu n'as pas :

  1. deux des prédicats de production / consommation combinés en un seul nom, c'est-à-dire que nous n'avons pas de pensées comme take_collect car cela prête facilement à confusion

drain_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

  1. 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 retain() , drain() . Il n'y a pas de confusion avec les méthodes Iterator qui transforment un itérateur en un autre itérateur.
  2. AFAIK, le consensus était que 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é
    une fonction. Les suffixes de ce type qui sont actuellement utilisés dans std sont principalement
    _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

  1. 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.
  2. 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 _" 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 :

  • Faut-il ajouter un paramètre R: RangeArgument ?
  • La valeur booléenne doit-elle être inversée ? (Je pense que la logique actuelle a du sens : renvoyer true du rappel entraîne l'inclusion de cet élément dans l'itérateur.)
  • Appellation. (J'aime 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 :

  • Une sous-gamme (via RangeArgument alias RangeBounds ) par rapport à la collection entière (bien que cette dernière puisse être obtenue en passant .. , une valeur de type RangeFull ).
  • Vider tout (éventuellement dans cette plage) contre uniquement les éléments qui correspondent à un prédicat booléen
  • Auto-épuisant sur drop vs not (laissant le reste des éléments dans la collection).

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 :

  1. il continuera d'appeler la fermeture qui a paniqué lors de la chute
    si la fermeture s'affole à nouveau cela provoquera un embarquement et alors que certaines personnes
    comme toutes les paniques d'être à bord d'autres travaux avec des modèles de noyau d'erreur et pour eux
    se retrouver avec un à bord est assez mauvais
  2. si ne poursuivra pas correctement la vidange potentiellement une valeur
    et contenant une valeur déjà abandonnée pouvant conduire à une utilisation après la libération

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 :

  • les itérateurs sont paresseux dans la rouille, l'exécution d'un itérateur lors de la suppression est un comportement un peu inattendu
    découlant de ce qui est normalement attendu
  • le prédicat passé à drain_filter peut paniquer dans certaines circonstances (par exemple, un verrou
    a été empoisonné), auquel cas il est probable qu'il panique à nouveau lorsqu'il est appelé pendant le drop.
    à une double panique et donc à bord, ce qui est assez mauvais pour quiconque utilise le noyau d'erreur
    modèles ou voulant enfin s'arrêter de manière contrôlée, c'est bien si vous utilisez panique=à bord de toute façon
  • si vous avez des effets secondaires dans le prédicat et que vous n'exécutez pas DrainFilter jusqu'à la fin, vous pourriez obtenir
    bogues surprenants lorsqu'il est ensuite exécuté jusqu'à la fin lorsqu'il est abandonné (mais vous avez peut-être fait
    l'autre pense entre le vider jusqu'à un certain point et le laisser tomber)
  • vous ne pouvez pas désactiver ce comportement sans modifier le prédicat qui lui est transmis, ce que vous
    pourrait ne pas être en mesure de le faire sans l'envelopper, d'un autre côté, vous pouvez toujours vous inscrire pour exécuter
    à la fin en exécutant simplement l'itérateur jusqu'à la fin (oui, ce dernier argument est un peu
    main)

Les 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 ils
    "juste" déposer DrainFilter au lieu de l'exécuter jusqu'à la fin

    • cet argument a été contré plusieurs fois dans d'autres RFC et c'est pourquoi #[unused_must_use]

      existent, qui dans certaines situations recommandent déjà d'utiliser .for_each(drop) qui ironiquement

      se trouve être ce que DrainFilter fait à la baisse

  • drain_filter est souvent utilisé uniquement pour ses effets secondaires, il est donc trop verbeux

    • l'utiliser de cette façon le rend à peu près égal à retain



      • mais conservez l'utilisation &T , drain_filter utilisé &mut T



  • autres??
  • [MODIFIER, AJOUTÉ PLUS TARD, THX @tmccombs ]: ne pas terminer le dépôt peut être très déroutant lorsqu'il est combiné avec des adaptateurs comme 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 :

  1. sauter l'élément sur lequel il a paniqué, le fuir et augmenter le compteur de suppression

    • nécessite une certaine forme de détection de panique

  2. ne pas avancer idx avant d'appeler le prédicat

    • mais cela signifie que le drop l'appellera à nouveau avec le même 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:

  1. définir un indicateur lors de la saisie next , le désactiver avant de revenir de next
  2. à la baisse si le drapeau est toujours défini, nous savons que nous avons paniqué et donc une fuite
    les éléments restants OU déposez tous les éléments restants

    1. peut être une fuite assez importante avec des effets secondaires inattendus si, par exemple, vous faites fuir un arc

    2. peut être très surprenant si vous avez Arc et Weak

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 :

  • Il peut s'exécuter normalement jusqu'à la fin, mais laisser des éléments correspondants lors du déroulement (de sorte que tous les éléments restants sont supposés ne pas correspondre).
  • Il pourrait s'exécuter normalement jusqu'à la fin, mais tronquer le vecteur après la dernière position écrite lors du déroulement (de sorte que tous les éléments restants soient supposés correspondre).
  • Il pourrait s'exécuter normalement jusqu'à la fin, mais tronquer le vecteur à la longueur 0 lors du déroulement.

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 :

  1. Ne pas exécuter jusqu'à la fin lors de la chute.
  2. Exécuter jusqu'à la fin mais abandonner potentiellement en raison d'une double panique
  3. Déposez tous les éléments "non cochés" en cas de panique.
  4. Ne terminez pas en cas de chute pendant la panique.

Les problèmes sont :

  1. => Comportement inattendu dans de nombreux cas d'utilisation.
  2. => Abandon inattendu si le prédicat peut paniquer, surtout si vous l'utilisez
    pour "simplement" supprimer des éléments, c'est-à-dire que vous n'utilisez pas l'itérateur renvoyé.
  3. => Différence inattendue entre chute dans et hors panique. Juste
    considérez quelqu'un _utilisant_ drain_filter dans une fonction drop.
  4. => Voir 3.

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: .

  1. => 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 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é.

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 :

  • Toute sélection contiguë d'éléments peut être supprimée.
  • La suppression des éléments supprimés est aussi simple que la suppression de l'itérateur renvoyé.
  • Les éléments supprimés n'ont pas à être supprimés ; l'appelant peut examiner chacun individuellement et choisir quoi en faire.
  • Le contenu de Vec n'a pas besoin de prendre en charge Copy ou Clone .
  • Aucune mémoire pour le Vec lui-même n'a besoin d'être allouée ou libérée.
  • Les valeurs laissées dans les 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 :

  1. Transformez les éléments supprimés avant de les renvoyer via l'itérateur.
  2. Abandonnez l'opération plus tôt et signalez une erreur.
  3. Au lieu de simplement supprimer l'élément ou de le laisser en place (tout en le transformant éventuellement), ajoutez la possibilité de supprimer l'élément et de le remplacer par une nouvelle valeur (qui peut être un clone de la valeur d'origine, ou autre chose entièrement).
  4. Remplacez l'élément supprimé par plusieurs nouveaux éléments.
  5. Après avoir fait quelque chose avec l'élément actuel, ignorez un certain nombre d'éléments suivants, en les laissant en place.
  6. Après avoir fait quelque chose avec l'élément actuel, supprimez un certain nombre d'éléments suivants sans les inspecter au préalable.

Et voici mon analyse de chacun:

  1. Cela n'ajoute rien d'utile car l'appelant peut déjà transformer les éléments au fur et à mesure qu'ils sont renvoyés par l'itérateur. Cela va également à l'encontre du but de l'itérateur, qui est d'éviter de cloner des valeurs en les livrant à l'appelant uniquement après qu'elles ont été supprimées du Vec .
  2. Pouvoir avorter tôt pourrait potentiellement améliorer la complexité asymptotique dans certains cas. Signaler une erreur dans le cadre de l'API n'ajoute rien de nouveau car vous pourriez faire la même chose en faisant muter la fermeture d'une variable capturée, et il n'est même pas clair comment le faire, car la valeur ne sera pas produite avant que l'itérateur ait été consommé.
  3. Cela, je pense, ajoute une réelle généralité.
  4. C'est ce que j'allais initialement proposer comme option "évier de cuisine", mais j'ai décidé que ce n'était pas utile, car si un seul élément peut être remplacé par plusieurs éléments, il est impossible de conserver la propriété que les éléments du 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.
  5. Cela pourrait être utile si le 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.
  6. Cela semble complémentaire à l'item 5, mais ce n'est pas très utile si vous avez encore besoin de renvoyer les éléments supprimés via l'itérateur. Cependant, cela peut toujours être une optimisation utile si les éléments que vous supprimez n'ont pas de destructeur et que vous souhaitez simplement les faire disparaître. Dans ce cas, le 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 que drain_filter est identique à Iterator::filter avec l'effet secondaire supplémentaire de supprimer la valeur sélectionnée articles à partir de self .

@ 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.

Appellation

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.
  • Une variation de 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

Paramètres

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 ").

Se détendre

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.

Vidange goutte à goutte

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 portail
  • VecDeque::drain_filter ne semble pas exister du tout, il n'apparaît pas dans la documentation
  • String::drain_filter n'existe pas non plus

Je 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.

Cette page vous a été utile?
0 / 5 - 0 notes