Rust: Problema de seguimiento para Vec::drain_filter y LinkedList::drain_filter

Creado en 15 jul. 2017  ·  119Comentarios  ·  Fuente: 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,
    { ... }

Estoy seguro de que hay un problema para esto en alguna parte, pero no puedo encontrarlo. Alguien nerd me disparó para que lo implementara. PR entrante.

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

Comentario más útil

¿Hay algo que impida que esto se estabilice?

Todos 119 comentarios

Tal vez esto no necesite incluir el fregadero de la cocina, pero _podría_ tener un parámetro de rango, de modo que sea como un superconjunto de drenaje. ¿Alguna desventaja de eso? Supongo que agregar límites para verificar el rango es un inconveniente, es otra cosa que puede causar pánico. Pero Drain_filter (..., f) no puede.

¿Hay alguna posibilidad de que esto se estabilice de alguna forma en un futuro no muy lejano?

Si el compilador es lo suficientemente inteligente como para eliminar las comprobaciones de límites
en el caso drain_filter(.., f) optaría por hacer esto.

(Y estoy bastante seguro de que puedes implementarlo de una manera
lo que hace que el compilador sea lo suficientemente inteligente, en el peor de los casos
caso de que pudiera tener una "especialización en función",
básicamente algo así como if Type::of::<R>() == Type::of::<RangeFull>() { dont;do;type;checks; return } )

Sé que hasta cierto punto se trata de un desprendimiento de bicicletas, pero ¿cuál fue el motivo para nombrarlo drain_filter en lugar de drain_where ? Para mí, lo primero implica que todo el Vec se vaciará, pero que también ejecutaremos un filter sobre los resultados (cuando lo vi por primera vez, pensé: "¿cómo es que esto no es solo .drain(..).filter() ?"). El primero, por otro lado, indica que solo drenamos elementos donde se cumple alguna condición.

Ni idea, pero drain_where suena mucho mejor y es mucho más intuitivo.
¿Todavía hay posibilidad de cambiarlo?

.remove_if también ha sido una sugerencia anterior

Creo que drain_where lo explica mejor. Al igual que drenar, devuelve valores, pero no drena/elimina todos los valores, sino solo aquellos en los que una condición dada es verdadera.

remove_if se parece mucho a una versión condicional de remove que simplemente elimina un solo elemento por índice si una condición es verdadera, por ejemplo letters.remove_if(3, |n| n < 10); elimina la letra en el índice 3 si es < 10 .

drain_filter por otro lado es ligeramente ambiguo, lo hace drain luego filter de una manera más optimizada (como filter_map) o lo hace si se drena para que se devuelva un iterador comparable a el iterador filter devolvería,
y si es así, ¿no debería llamarse filtered_drain ya que el filtro se usa lógicamente antes?

No hay precedentes de usar _where o _if en ninguna parte de la biblioteca estándar.

@Gankro , ¿hay un precedente para usar _filter cualquier lugar? Tampoco sé si esa es una razón para no usar la terminología menos ambigua. Otros lugares en la biblioteca estándar ya usan una variedad de sufijos como _until y _while .

El código "dicho equivalente" en el comentario no es correcto... tienes que restar uno de i en el sitio "tu código aquí", o suceden cosas malas.

En mi opinión, no es filter ese es el problema. Después de haber buscado esto (y ser un novato), drain parece ser bastante no estándar en comparación con otros idiomas.

Nuevamente, solo desde la perspectiva de un novato, las cosas que buscaría si tratara de encontrar algo para hacer lo que propone este problema serían delete (como en delete_if ), remove , filter o reject .

De hecho, _busqué_ filter , vi drain_filter y _seguí buscando_ sin leer porque drain no parecía representar lo simple que quería hacer.

Parece que una función simple llamada filter o reject sería mucho más intuitiva.

En una nota separada, no siento que esto deba mutar el vector al que se llama. Evita el encadenamiento. En un escenario ideal, uno querría poder hacer algo como:

        vec![
            "",
            "something",
            a_variable,
            function_call(),
            "etc",
        ]
            .reject(|i| { i.is_empty() })
            .join("/")

Con la implementación actual, lo que se uniría serían los valores rechazados.

Me gustaría ver un accept y un reject . Ninguno de los cuales muta el valor original.

Ya puedes hacer el encadenamiento con filter solo. El objetivo de drain_filter es mutar el vector.

@rpjohnst así que busqué aquí , ¿me estoy perdiendo filter en alguna parte?

Sí, es miembro de Iterator , no Vec .

Drenar es una terminología novedosa porque representaba un cuarto tipo de propiedad en Rust que solo se aplica a los contenedores, mientras que generalmente también es una distinción sin sentido en casi cualquier otro idioma (en ausencia de semántica de movimiento, no hay necesidad de combinar iteración y eliminación en una sola operación ""atómica"").

Aunque Drain_filter mueve la terminología de desagüe a un espacio que interesaría a otros idiomas (dado que evitar los retrocesos es relevante en todos los idiomas).

Encontré drain_filter en documentos como un resultado de Google para rust consume vec . Sé que debido a la inmutabilidad predeterminada en Rust, el filtro no consume los datos, simplemente no podía recordar cómo abordarlo para poder administrar mejor la memoria.

drain_where está bien, pero siempre que el usuario sepa lo que hacen drain y filter , creo que está claro que el método drena los datos en función de un filtro predicado.

Todavía siento que drain_filter implica que se drena (es decir, se vacía) y luego se filtra. drain_where por otro lado, parece que drena los elementos donde se cumple la condición dada (que es lo que hace la función propuesta).

¿No debería linked_list::DrainFilter implementar Drop también, para eliminar cualquier elemento restante que coincida con el predicado?

¿Por qué exactamente dejar caer el iterador hace que se ejecute hasta el final? Creo que es un comportamiento sorprendente para un iterador y también podría, si se desea, hacerlo explícitamente. Por otro lado, lo contrario de eliminar solo tantos elementos como necesite es imposible porque mem::forget ing el iterador se encuentra con la amplificación de fugas.

He estado usando mucho esta función y siempre tengo que recordar devolver true para las entradas que quiero vaciar, lo que parece contrario a la intuición en comparación con retain() / retain_mut() .
En un nivel lógico intuitivo, tendría más sentido devolver true para las entradas que quiero conservar, ¿alguien más se siente así? (Especialmente considerando que retain() ya funciona de esta manera)
¿Por qué no hacer eso y cambiar el nombre drain_filter() a retain_iter() o retain_drain() (o drain_retain() )?
¡Entonces también reflejaría retain() más de cerca!

Por eso propuse cambiarle el nombre a
drain_where pues queda claro que:

  1. Es una forma de drenaje, por lo que usamos drenaje en el nombre.

  2. Al usar where en combinación con drain está claro que el
    los elementos _donde_ el predicado es verdadero se drenan, es decir, se eliminan y se devuelven

  3. Está más sincronizado con otros nombres en std, normalmente si tiene un
    función que consta de dos predicados, puede emularla (más o menos) usando
    funciones que representan cada uno de los predicados en forma de "y luego", por ejemplo
    filter_map se puede emular (aproximadamente) como filter an then map . La corriente
    el nombre indica que es drain and then filter , pero ni siquiera se le acerca
    ya que no hace un drenaje completo en absoluto.

El domingo, 25 de febrero de 2018, 17:04 Boscop [email protected] escribió:

He estado usando mucho esta función y siempre tengo que recordar
volver verdadero para las entradas que quiero drenar, lo que se siente
contrario a la intuición en comparación con sustain_mut().
En un nivel primario, tendría más sentido devolver verdadero para las entradas I
quiero mantener, ¿alguien más se siente así?
¿Por qué no hacer eso y cambiar el nombre de drenar_filtro() a retener_filtro()?


Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368320990 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AHR0kfwaNvz8YBwCE4BxDkeHgGxLvcWxks5tYYRxgaJpZM4OY1me
.

Pero con drain_where() el cierre aún tendría que devolver verdadero para los elementos que deberían eliminarse, que es lo opuesto a retain() lo que lo hace inconsistente.
¿Tal vez retain_where ?
Pero creo que tiene razón en que tiene sentido tener "drenar" en el nombre, así que creo que drain_retain() tiene más sentido: es como drain() pero conservando los elementos donde regresa el cierre true .

Si bien no cambia, que tiene que volver verdadero, deja en claro que
tienes que hacerlo

Yo personalmente usaría cualquiera:

una. drain_where
B. retain_where
C. retain

No usaría drain_retain .
Drenar y retener hablan sobre el mismo tipo de proceso pero desde el lado opuesto.
perspectivas, drenaje habla de lo que quitas (y devuelves) retienes
habla de lo que guardas (en la forma en que ya se usa en std).

Actualmente retaim ya está implementado en std con la gran diferencia
que está descartando elementos mientras que drain está devolviendo, lo que hace
retain (lamentablemente) inadecuado para ser usado en el nombre (excepto si desea llamar
es retain_and_return o similar).

Otro punto que habla por drenaje es la facilidad de migración, es decir, migrar
a drain_where es tan fácil como ejecutar una búsqueda basada en palabras y reemplazar en
el código, mientras que cambiarlo para retener necesitaría una negación adicional de
todos los predicados/funciones de filtro usados.

El domingo, 25 de febrero de 2018, 18:01 Boscop [email protected] escribió:

Pero con Drain_where() el cierre aún tendría que volver verdadero para
elementos que deben eliminarse, que es lo contrario de retener () que
lo hace inconsistente..
¿Quizás retener_dónde?
Pero creo que tienes razón en que tiene sentido tener "drenaje" en el nombre,
así que creo que es más sensato drenar_retener(): es como drenar() pero
conservando los elementos donde el cierre devuelve verdadero.


Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368325374 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AHR0kfG4oZHxGfpOSK8DjXW3_2O1Eo3Rks5tYZHxgaJpZM4OY1me
.

Pero, ¿con qué frecuencia migraría de drain() a drain_filter() ?
En todos los casos hasta ahora, migré de retain() a drain_filter() porque no hay retain_mut() en std y necesito mutar el elemento. Entonces tuve que invertir el valor de retorno de cierre.
Creo que drain_retain() tiene sentido porque el método drain() drena incondicionalmente todos los elementos en el rango, mientras que drain_retain() retiene los elementos donde el cierre devuelve true , combina los efectos de los métodos drain() y retain() .

Lo siento, quiero migrar de drain_filter a drain_where .

Que tiene una solución usando retain y luego necesita usar
drain_filter es un aspecto que aún no he considerado.

El domingo, 25 de febrero de 2018, 19:12 Boscop [email protected] escribió:

Pero, ¿por qué migraría de drenaje() a drenaje_filtro()?
En todos los casos hasta ahora, migré de retener() a drenaje_filtro()
¡porque no hay sustain_mut() en std y necesito mutar el elemento!
Creo que Drain_retain() tiene sentido porque el método Drain() drena
incondicionalmente todos los elementos en el rango, mientras que Drain_retain() retiene
los elementos donde el cierre devuelve verdadero.


Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368330896 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AHR0kSayIk_fbp5M0RsZW5pYs3hDICQIks5tYaJ0gaJpZM4OY1me
.

Ah, sí, pero creo que el "precio" de invertir los cierres en el código actual que usa drain_filter() vale la pena, para obtener una API consistente e intuitiva en std y luego en estable.
Es solo un pequeño costo fijo (y aliviado por el hecho de que iría junto con un cambio de nombre de la función, por lo que el error del compilador podría decirle al usuario que el cierre debe invertirse, por lo que no introduciría un error en silencio) , en comparación con el costo de estandarizar drain_filter() y luego las personas siempre tienen que invertir el cierre al migrar de retain() a drain_filter() .. (además del costo mental de recordar hacer eso, y los costos de hacer que sea más difícil encontrarlo en los documentos, viniendo de retain() y buscando "algo así como retain() pero pasando &mut a su cierre, que es por eso que creo que tiene sentido que el nuevo nombre de esta función tenga "retener" en el nombre, para que la gente lo encuentre al buscar en los documentos).

Algunos datos anecdóticos: en mi código, siempre solo necesitaba el aspecto retain_mut() de drain_filter() (a menudo usaban retain() antes), nunca tuve casos de uso en los que necesitaba procesar los valores drenados. Creo que este también será el caso de uso más común para otros en el futuro (dado que retain() no pasa &mut a su cierre, por lo que drain_filter() tiene que cubrir ese caso de uso , también, y es un caso de uso más común que la necesidad de procesar los valores drenados).

La razón por la que estoy nuevamente en contra drain_retain es por la forma en que los nombres se usan actualmente en std wrt. colecciones:

  1. tiene nombres de funciones que usan predicados que tienen conceptos de producción/consumo asociados con ellos (wrt. rust, iteraciones). Por ejemplo drain , collect , fold , all , take , ...
  2. estos predicados a veces tienen modificadores, por ejemplo *_where , *_while
  3. tiene nombres de funciones que usan predicados que tienen propiedades de modificación ( map , filter , skip , ...)

    • aquí es vago si se trata de una modificación de elementos o iteraciones ( map vs. filter / skip )

  4. nombres de funciones que encadenan varios predicados mediante la modificación de propiedades, por ejemplo filter_map

    • tener un concepto de aproximadamente apply modifier_1 and then apply modifier_2 , solo que es más rápido o más flexible hacerlo en un solo paso

A veces puede tener:

  1. nombres de funciones que combinan predicados de producción/consumo con predicados de modificación (por ejemplo drain_filter )

    • _pero_ la mayoría de las veces es mejor/menos confuso combinarlos con modificadores (por ejemplo drain_where )

Normalmente no tienes:

  1. dos de los predicados de producción/consumo combinados en un solo nombre, es decir, no tenemos pensamientos como take_collect ya que es fácil de confundir

drain_retain tiene un poco de sentido, pero cae en la última categoría, mientras que probablemente puedas adivinar lo que hace, básicamente dice remove and return all elements "somehow specified" and then keep all elements "somehow specified" discarding other elements .


Por otro lado, no sé por qué no debería haber retain_mut tal vez abriendo un RFC rápido introduciendo retain_mut como una forma eficiente de combinar modify + retain Tengo el presentimiento de que podría ser más rápido
estabilizada entonces esta función. Hasta entonces, podría considerar escribir un rasgo de extensión proporcionando
posee retain_mut usando iter_mut + un bool-array (o bitarray, o...) para realizar un seguimiento de qué elementos
tiene que ser quitado. O proporcionando su propio drain_retain que usa internamente drain_filer / drain_where
pero envuelve el predicado en un no |ele| !predicate(ele) .

@dathinab

  1. Estamos hablando de un método en colecciones aquí, no en Iterator. map, filter, filter_map, skip, take_while, etc. son todos métodos en Iterator. Por cierto, ¿a qué métodos te refieres con el uso *_where ?
    Así que tenemos que comparar el esquema de nombres con los métodos que ya existen en las colecciones, por ejemplo, retain() , drain() . No hay confusión con los métodos Iterator que transforman un iterador en otro iterador.
  2. AFAIK, el consenso fue que retain_mut() no se agregaría a std porque ya se agregarán drain_filter() y se recomendó a las personas que lo usen. Lo que nos lleva de nuevo al caso de uso de migrar de retain() a drain_filter() siendo muy común, por lo que debería tener un nombre y una API similares (el cierre que devuelve true significa mantener la entrada )..

No sabía que retain_mut ya se discutió.

Estamos hablando de esquemas generales de nombres en std principalmente wrt. a
colecciones, que incluye iteradores, ya que son uno de los principales
métodos de acceso para colecciones en rust, especialmente cuando se trata de
modificando más de una entrada.

  • _where es solo un ejemplo de sufijos para expresar un
    función. Los sufijos de este tipo que actualmente se utilizan en std son principalmente
    _until , _while , _then , _else , _mut y _back .

La razón por la que drain_retain es confuso es que no está claro si es
basado en drenaje o retención, si está basado en drenaje, devolver verdadero eliminaría
el valor, si se basa en retenerlo, lo mantendría. Usar _where lo convierte en
último claro lo que se espera de la función pasada.

El lunes, 26 de febrero de 2018 a las 00:25, Boscop [email protected] escribió:

@dathinab https://github.com/dathinab

  1. Estamos hablando de un método en colecciones aquí, no en Iterator.
    map, filter, filter_map, skip, take_while, etc. son todos métodos en Iterator.
    Por cierto, ¿a qué métodos te refieres con usar *_where?
    Así que tenemos que comparar el esquema de nombres con los métodos que ya existen.
    en colecciones, por ejemplo, retener(), drenaje(). No hay confusión con
    Métodos de iterador que transforman el iterador en otro iterador.
  2. AFAIK, el consenso fue que retener_mut () no se agregaría a std
    porque Drain_filter() ya se agregará y se aconsejó a las personas
    para usar eso Lo que nos lleva de nuevo al caso de uso de migrar de
    reten() a drenaje_filtro() siendo muy común, por lo que debería tener un
    nombre y API similares (el cierre que devuelve verdadero significa mantener la entrada).


Estás recibiendo esto porque te mencionaron.

Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368355110 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AHR0kfkRAZ5OtLFZ-SciAmjHDEXdgp-0ks5tYevegaJpZM4OY1me
.

He estado usando mucho esta función y siempre tengo que recordar devolver verdadero para las entradas que quiero drenar, lo que parece contrario a la intuición en comparación con retener()/retener_mut().

FWIW, creo que retain es el nombre contrario a la intuición aquí. Por lo general, me encuentro queriendo eliminar ciertos elementos de un vector, y con retain siempre tengo que invertir esa lógica.

Pero retain() ya está estable, así que tenemos que vivir con eso... Y entonces mi intuición se acostumbró a eso...

@Boscop : y también lo es drain que es el inverso de retain pero también devuelve los elementos eliminados y el uso de sufijos como _until , _while para hacer funciones disponibles que son solo una versión ligeramente modificada de una funcionalidad existente.

Quiero decir, si describiera el drenaje, sería algo así como:

_eliminar y devolver todos los elementos especificados "de alguna manera", mantener todos los demás elementos_
donde _"de alguna manera"_ es _"por rebanado"_ para todos los tipos de colección rebanables y _"todos"_ para el resto.

La descripción de la función discutida aquí es _la misma_ excepto que
_"de alguna manera"_ es _" donde un predicado dado devuelve verdadero"_.

Por otro lado, la descripción que daría retener es:
_solo retener (es decir, mantener) elementos donde un predicado dado devuelve verdadero, descartar el resto_

(sí, retener podría haberse usado de una manera en la que no descarta el resto, lamentablemente no fue así)


Creo que hubiera sido muy bueno si retain hubiera
pasó &mut T al predicado y tal vez devolvió los valores eliminados.
Porque creo que retain es una base de nombres más intuitiva.

Pero independientemente de esto, también creo que ambos drain_filter / drain_retain son subóptimos
ya que no aclaran si el predicado tiene que devolver verdadero/falso para mantener/drenar una entrada.
(Drenar indica que verdadero lo elimina, ya que habla de eliminar elementos mientras filtra
y volver a entrenar habla sobre qué elementos mantener, por fin en óxido)


Al final, no es _tan_ importante cuál de los nombres se usa, sería bueno si se estabilizara.

¿Hacer una encuesta y/o dejar que alguien del equipo lingüístico decida podría ser la mejor manera de hacer avanzar las ideas?

Creo que algo como drain_where , drain_if , o drain_when , es mucho más claro que drain_filter .

@tmccombs De esos 3, creo que drain_where tiene más sentido. (Porque if implica either do the whole thing (in this case draining) or not y when es temporal).
En comparación con drain_filter el valor de retorno de cierre es el mismo que con drain_where ( true para eliminar un elemento), pero ese hecho se hace más claro/explícito por el nombre, por lo que elimina el riesgo de interpretar accidentalmente el significado del valor de retorno de cierre de forma incorrecta.

Creo que es más que hora de estabilizarse. Resumen de este hilo:

  • ¿Debe agregarse un parámetro R: RangeArgument ?
  • ¿Debe invertirse el valor booleano? (Creo que la lógica actual tiene sentido: devolver true desde la devolución de llamada hace que ese elemento se incluya en el iterador).
  • Denominación. (Me gusta drain_where .)

@Gankro , ¿qué opinas?

El equipo de libs discutió esto y el consenso fue no estabilizar más métodos similares a drain en este momento. (El método drain_filter existente puede permanecer en Nightly como inestable). https://github.com/rust-lang/rfcs/pull/2369 propone agregar otro iterador similar a drenaje que no hace nada cuando se cae (en lugar de consumir el iterador hasta el final).

Nos gustaría ver la experimentación para intentar generalizar en una superficie API más pequeña varias combinaciones de drenaje:

  • Un sub-rango (a través RangeArgument también conocido como RangeBounds ) frente a la colección completa (aunque este último podría lograrse pasando .. , un valor de tipo RangeFull ).
  • Drenando todo (posiblemente dentro de ese rango) vs solo elementos que coincidan con un predicado booleano
  • Autoagotable en caída vs no (dejando el resto de elementos en la colección).

Las posibilidades pueden incluir "sobrecargar" un método haciéndolo genérico o un patrón de construcción.

Una restricción es que el método drain es estable. Posiblemente se pueda generalizar, pero solo en formas compatibles con versiones anteriores.

Nos gustaría ver la experimentación para intentar generalizar en una superficie API más pequeña varias combinaciones de drenaje:

¿Cómo y dónde prevé el equipo que suceda este tipo de experimentación?

Cómo: idear y proponer un diseño de API concreto, posiblemente con una implementación de prueba de concepto (que se puede hacer fuera del árbol a través de al menos Vec::as_mut_ptr y Vec::set_len ). Dónde no importa demasiado. Podría ser un nuevo RFC o un hilo en https://internals.rust-lang.org/c/libs y vincularlo desde aquí.

He estado jugando con esto por un tiempo. Voy a abrir un hilo sobre internos en los próximos días.

Creo que una API general que funciona así tiene sentido:

    v.drain(a..b).where(pred)

Por lo tanto, es una API de estilo constructor: si no se agrega .where(pred) , agotará todo el rango incondicionalmente.
Esto cubre las capacidades del método actual .drain(a..b) así como .drain_filter(pred) .

Si el nombre drain no se puede usar porque ya está en uso, debería ser un nombre similar como drain_iter .

El método where no debe llamarse *_filter para evitar confusiones con el filtrado del iterador resultante, especialmente cuando where y filter se usan en combinación como esta:

    v.drain(..).where(pred1).filter(pred2)

Aquí, utilizará pred1 para decidir qué se drenará (y pasará al iterador) y pred2 se utilizará para filtrar el iterador resultante.
Cualquier elemento que pred1 devuelva true pero pred2 devuelva false todavía se drenará de v pero no se obtendrá por este iterador combinado.

¿Qué opinas sobre este tipo de enfoque API de estilo constructor?

Por un segundo olvidé que where no se puede usar como nombre de función porque ya es una palabra clave :/

Y drain ya está estabilizado, por lo que tampoco se puede usar el nombre.

Entonces creo que la segunda mejor opción general es mantener el drain actual y cambiar el nombre drain_filter a drain_where , para evitar la confusión con .drain(..).filter() .

(Como dijo jonhoo arriba:)

¿Cuál fue el razonamiento detrás de nombrar este filtro de drenaje en lugar de drenaje_dónde? Para mí, lo primero implica que se drenará todo el Vec, pero que también ejecutaremos un filtro sobre los resultados (cuando lo vi por primera vez, pensé: "¿cómo es que esto no es solo .drain(...).filter() ?"). El primero, por otro lado, indica que solo drenamos elementos donde se cumple alguna condición.

He abierto un hilo sobre internos .
El TLDR es que creo que el no autoagotamiento es una lata de gusanos más grande de lo esperado en el caso general y que deberíamos estabilizar drain_filter más temprano que tarde con un parámetro RangeBounds . A menos que alguien tenga una buena idea para resolver los problemas descritos allí.

Editar: he subido mi código experimental: drenar experimentos
También hay bancos de drenaje y limpieza y algunas pruebas, pero no espere un código limpio.

Me perdí totalmente este hilo. He tenido un impl antiguo que arreglé un poco y copié y pegué para reflejar algunas de las opciones descritas en este hilo. Lo bueno del impl que creo que no será controvertido es que implementa DoubleEndedIterator . Véalo aquí .

@Emerentius , pero al menos deberíamos cambiar el nombre drain_filter a drain_where , para indicar que el cierre debe devolver true para eliminar el elemento.

@Boscop Ambos implican la misma 'polaridad' de true => rendimiento. Personalmente, no me importa si se llama drain_filter o drain_where .

@Popog ¿Puede resumir las diferencias y los pros y los contras? Idealmente en la rosca interna. Creo que la funcionalidad DoubleEndedIterator podría agregarse de forma retrocompatible con cero o poca sobrecarga (pero no lo he probado).

¿Qué tal drain_or_retain ? Es una acción gramaticalmente significativa y señala que hace una cosa o la otra.

@askeksa Pero eso no deja claro si devolver true del cierre significa "drenar" o "retener".
Creo que con un nombre como drain_where , está muy claro que devolver true lo agota, y debería quedar claro para todos que los elementos que no se agotan se retienen.

Sería bueno si hubiera alguna forma de limitar/detener/cancelar/abortar el drenaje. Por ejemplo, si quisiera drenar los primeros N números pares, sería bueno poder hacer vec.drain_filter(|x| *x % 2 == 0).take(N).collect() (o alguna variante de eso).

Tal como está implementado actualmente, DrainFilter método drop siempre ejecutará el drenaje hasta su finalización; no se puede abortar (al menos no he descubierto ningún truco que lo haga).

Si desea ese comportamiento, debe cerrar algún estado que rastree cuántos ha visto y comience a devolver falso. Es necesario ejecutar hasta el final en la caída para que los adaptadores se comporten de manera razonable.

Me acabo de dar cuenta de que la forma en que se implementa actualmente drain_filter no es segura para relajarse, pero
en realidad un peligro para la seguridad wrt. relajarse + reanudar la seguridad. Además, provoca fácilmente un aborto, tanto
de los cuales son comportamientos que un método en std realmente no debería tener. Y mientras escribía esto me di cuentaque su implementación actual no es segura

Sé que Vec por defecto no es seguro para relajarse, pero el comportamiento de drain_filer cuando el
predicar pánicos es bien sorprendente porque:

  1. seguirá llamando al cierre que entró en pánico cuando cayó
    si el cierre vuelve a entrar en pánico esto provocará un a bordo y mientras algunas personas
    como todos los pánicos de estar a bordo de otros trabajos con patrones de kernel de error y para ellos
    terminar con un a bordo es bastante malo
  2. si no continuará correctamente el drenaje potencialmente un valor
    y que contiene un valor que ya se eliminó y que potencialmente conduce al uso después de la liberación

Un ejemplo de este comportamiento está aquí:
play.rust-lang.org

Si bien el 2. punto debería tener solución, creo que el primer punto en sí mismo debería
conducir a una reconsideración del comportamiento de DrainFilter para ejecutar hasta la finalización
on drop, las razones para cambiar esto incluyen:

  • los iteradores son perezosos en el óxido, ejecutar un iterador cuando se suelta es un comportamiento inesperado
    derivado de lo que normalmente se espera
  • el predicado pasado a drain_filter podría entrar en pánico bajo algunas circunstancias (por ejemplo, un bloqueo
    se envenenó), en cuyo caso es probable que vuelva a entrar en pánico cuando se le llame durante el lanzamiento.
    a un pánico doble y, por lo tanto, a bordo, lo cual es bastante malo para cualquiera que use el kernel de error
    patrones o por fin querer apagarse de forma controlada, está bien si usa panic = aboard de todos modos
  • si tiene efectos secundarios en el predicado y no ejecuta DrainFilter hasta completarlo, es posible que obtenga
    errores sorprendentes cuando se ejecuta hasta el final cuando se cae (pero es posible que haya hecho
    otro piensa entre drenarlo hasta un punto y dejarlo caer)
  • no puede optar por no participar en este comportamiento sin modificar el predicado que se le pasa, que
    es posible que no pueda hacerlo sin envolverlo, por otro lado, siempre puede optar por ejecutar
    completarlo simplemente ejecutando el iterador hasta su finalización (sí, este último argumento es un poco
    ondulado a mano)

Los argumentos para ejecutar hasta el final incluyen:

  • drain_filter es similar a ratain que es una función, por lo que la gente puede sorprenderse cuando
    "simplemente" suelte DrainFilter en lugar de ejecutarlo hasta completarlo

    • este argumento fue contrarrestado muchas veces en otros RFC y es por eso que #[unused_must_use]

      exist's, que en algunas situaciones ya recomiendan usar .for_each(drop) que irónicamente

      pasa a ser lo que DrainFilter hace al soltar

  • drain_filter a menudo se usa solo por su efecto secundario, por lo que es demasiado detallado

    • usarlo de esa manera lo hace aproximadamente igual a retain



      • pero retenga el uso &T , el filtro de drenaje usó &mut T



  • ¿¿otros??
  • [EDITAR, AÑADIR MÁS TARDE, THX @tmccombs ]: no completar al soltar puede ser muy confuso cuando se combina con adaptadores como find , all , any lo cual es una muy buena razón mantener el comportamiento actual.

Podría ser solo yo o me perdí en algún punto, pero cambié el comportamiento Drop y
agregar #[unused_must_use] parece ser preferible?

Si .for_each(drop) es demasiado largo, podríamos considerar agregar un RFC para iteradores destinados a
hay un efecto secundario al agregar un método como complete() al iterador (o bien drain() pero esto
es una discusión completamente diferente)

¿¿otros??

No puedo encontrar el razonamiento original, pero recuerdo que también hubo un problema con los adaptadores que funcionan con un DrainFilter que no se ejecuta hasta el final.

Consulte también https://github.com/rust-lang/rust/issues/43244#issuecomment -394405057

Buen punto, por ejemplo, find haría que el drenaje se drene solo hasta que llegue al primero
partido, similar all , any hacer cortocircuito, lo que puede ser bastante confuso
wrt. desagüe.

Hm, tal vez debería cambiar mi opinión. A través de esto podría ser un problema general
con iteradores que tienen efectos secundarios y tal vez deberíamos considerar una solución general
(independiente de este problema de seguimiento) como un adaptador .allways_complete() .

Personalmente, no he encontrado ninguna razón de seguridad por la que el drenaje deba ejecutarse hasta el final, pero como he escrito aquí un par de publicaciones más arriba, los efectos secundarios en next() interactúan de manera subóptima con adaptadores como take_while , peekable y skip_while .

Esto también plantea los mismos problemas que mi RFC en el drenaje que no se agota automáticamente y su adaptador de iter de escape automático complementario RFC .

Es cierto que drain_filter puede causar abortos fácilmente, pero ¿puede mostrar un ejemplo de dónde viola la seguridad?

Sí, ya lo hice: play.rust-lang.org

Cual es este:

#![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 --

}

Pero ese es un pensamiento interno de implementación, que salió mal.
Básicamente, la pregunta abierta es cómo manejar el panic de una función de predicado:

  1. omita el elemento en el que entró en pánico, fíltrelo y aumente el contador del

    • requiere algún tipo de detección de pánico

  2. no avance idx antes de llamar al predicado

    • pero esto significa que al soltar lo llamará de nuevo con el mismo predicado

Otra pregunta es si es una buena idea ejecutar funciones que se pueden ver como entrada de usuario de API al soltar
en general, pero esta es la única manera de no hacer que find , any , etc. se comporten de forma confusa.

Tal vez una consideración podría ser algo como:

  1. establezca una bandera al ingresar next , desactívela antes de regresar de next
  2. al soltar, si la bandera aún está configurada, sabemos que entramos en pánico y, por lo tanto, filtramos
    los elementos restantes O soltar todos los elementos restantes

    1. puede ser una fuga bastante grande con efectos secundarios inesperados si, por ejemplo, filtra un arco

    2. puede ser muy sorprendente si tienes Arc y Weak's

Tal vez haya una solución mejor.
A través de lo que sea, debe documentarse en rustdoc una vez implementado.

@dathinab

Sí, ya lo hice

Las fugas no son deseables pero están bien y pueden ser difíciles de evitar aquí, pero una doble caída definitivamente no lo es. ¡Buena atrapada! ¿Le gustaría reportar un problema por separado sobre este problema de seguridad?

¿ drain_filter realiza reasignaciones cada vez que elimina un artículo de la colección? ¿O se reasigna solo una vez y funciona como std::remove y std::erase (en pares) en C++? Preferiría tal comportamiento debido a exactamente una asignación: simplemente colocamos nuestros elementos al final de la colección y luego los eliminamos y los encogemos al tamaño adecuado.

Además, ¿por qué no hay try_drain_filter ? ¿Qué devuelve el tipo Option y None si debemos detenernos? Tengo una colección muy grande y no tiene sentido continuar para mí cuando ya tengo lo que necesitaba.

La última vez que analicé el código, hizo algo como: creó una "brecha"
al sacar elementos y mover un elemento que no está drenado al
comienzo de la brecha cuando encuentra uno. Con esto cada elemento que tiene que ser
movido (ya sea hacia afuera o hacia un nuevo lugar en la matriz) solo se mueve una vez.
También como, por ejemplo remove no se reasigna. La parte liberada simplemente se convierte
parte de la capacidad no utilizada.

El viernes, 10 de agosto de 2018 a las 07:11, Victor Polevoy [email protected] escribió:

¿drain_filter realiza reasignaciones cada vez que elimina un elemento de
¿colección? O realiza la reasignación solo una vez y funciona como std::remove
y std::erase (en pares) en C++? Preferiría tal comportamiento debido a
exactamente una asignación: simplemente ponemos nuestros elementos al final de la colección
y luego lo quita, lo encoge al tamaño adecuado.

Además, ¿por qué no hay try_drain_filter? Que devuelve el tipo de opción, y
¿Ningún valor si debemos parar? Tengo una colección muy grande y es
no tiene sentido continuar para mí cuando ya tengo lo que necesitaba.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-411977001 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AHR0kdOZm4bj6iR9Hj831Qh72d36BxQSks5uPRYNgaJpZM4OY1me
.

@rustonaut gracias. ¿Cuál es tu opinión sobre try_drain_filter ? :)

PD También miré el código, parece que funciona de la manera que queríamos.

Avanza elemento por elemento durante la iteración, por lo que normalmente podría esperar
para que dejara de iterar cuando se descartaba, pero se consideró que era demasiado
confuso, por lo que en realidad se drena hasta el final cuando se cae.
(Lo que aumenta drásticamente la probabilidad de doble pánico y esas cosas
como eso).

Por lo tanto, es poco probable que obtenga una versión de prueba que se comporte como usted
suponer.

Para ser justos, la detención anticipada cuando la iteración realmente puede ser confusa en
algunas situaciones, por ejemplo thing.drain_where(|x| x.is_malformed()).any(|x| x.is_dangerus()) no agotarían todos los malformados, sino solo hasta que uno de
encontrado que también es peligroso. (El actual Impl. drena todos los malformados
continuando el drenaje gota a gota).

El viernes, 10 de agosto de 2018 a las 10:52, Victor Polevoy [email protected] escribió:

@rustonaut https://github.com/rustonaut gracias. Cuál es tu opinión
acerca de try_drain_filter? :)


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-412020490 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AHR0kcEMHCayqvhI6D4LK4ITG2x5di-9ks5uPUnpgaJpZM4OY1me
.

Creo que esto sería más versátil:

fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>

Hola, estaba buscando la funcionalidad drain_filter para HashMap pero no existe, y me pidieron que abriera un problema cuando encontré este. ¿Debería estar en un número aparte?

¿Hay algo que bloquee actualmente la estabilización de esto? ¿Sigue siendo inseguro para relajarse como se informó anteriormente?

Esto parece una característica bastante pequeña, y ha estado en el limbo durante más de un año.

Creo que esto sería más versátil:

fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>

No creo que esto sea mejor que una composición de drain_filter y map .

¿Sigue siendo inseguro para relajarse como se informó anteriormente?

Parece haber una elección difícil entre no drenar todos los elementos coincidentes si la iteración se detiene antes de tiempo, o un pánico potencial durante la relajación si el filtrado y el drenaje hasta el final se realizan al soltar un DrainFilter .
Creo que esta característica está plagada de problemas de cualquier manera y, como tal, no debe estabilizarse.

¿Hay algún problema en particular con que se comporte de manera diferente al relajarse?

Posibilidades:

  • Podría ejecutarse hasta completarse normalmente, pero dejar elementos coincidentes al desenrollarse (de modo que se suponga que todos los elementos restantes no coinciden).
  • Podría ejecutarse hasta completarse normalmente, pero truncar el vector después de la última posición escrita al desenrollar (para que se suponga que todos los elementos restantes coinciden).
  • Podría ejecutarse hasta completarse normalmente, pero truncar el vector a la longitud 0 al relajarse.

El contraargumento más comprensible que se me ocurre es el código drop que depende de la invariante que suele proporcionar drain_filter (que, al final, los elementos del vec serán exactamente los mismos que falló la condición) puede estar arbitrariamente distante del código (muy probablemente normal, código seguro) que usa drain_filter .

Sin embargo, supongamos que hubiera tal caso. Este código tendrá errores sin importar cómo lo escriba el usuario. Por ejemplo, si escriben un ciclo imperativo que fue en reversa y elementos eliminados por intercambio, entonces si su condición puede entrar en pánico y su impl de caída depende en gran medida de que la condición del filtro sea falsa, el código todavía tiene un error. Tener una función como drop_filter cuya documentación puede llamar la atención sobre este caso extremo parece una mejora en comparación.

Además, gracias, encontré este ejemplo de patio de recreo publicado anteriormente en el hilo que demuestra que la implementación actual aún elimina elementos dos veces. (¡así que definitivamente no se puede estabilizar como está!)

¿Valdría la pena abrir un número separado para el error de solidez? Eso se puede marcar como I-unsound.

Hasta donde yo sé, no puedes marcar o tan poco sonido como el doble pánico _es sonido_
simplemente muy inconveniente ya que aborta. También por lo que recuerdo el
posibilidad de pánico doble no es un error, sino el comportamiento implícitamente, pero
elegido a sabiendas.

Las opciones son básicamente:

  1. No corra hasta el final en la caída.
  2. Ejecutar hasta el final, pero potencialmente abortar debido al doble pánico
  3. Deje caer todos los elementos "no marcados" durante el pánico.
  4. No complete al caer durante el pánico.

Los problemas son:

  1. => Comportamiento inesperado en muchos casos de uso.
  2. => Aborto inesperado si el predicado puede entrar en pánico, especialmente si lo usa
    para "simplemente" eliminar elementos, es decir, no utiliza el iterador devuelto.
  3. => Diferencia inesperada entre caer dentro y fuera de un pánico. Sólo
    considere a alguien _usando_ filtro_de_drenaje en una función de eliminación.
  4. => Ver 3.

O con otras palabras 1. conduce a confusión en casos de uso normal, 2. puede conducir
abortar si el predicado puede entrar en pánico 3.,4. hazlo para que realmente no puedas
úselo en un método de soltar, pero ¿cómo sabe ahora una función que usa allí?
no lo usa internamente.

Como resultado de esta opción 3.,4. no van. Los problemas con la opción 2. son
más raros que los del 1. por lo que se eligió el 2.

En mi humilde opinión, sería mejor tener una API de drenaje + drenaje_filtro que no se ejecute
hasta completarse al soltar + un combinador iterador general que se ejecuta para
finalización al soltar + un método que completa un iterador pero simplemente descarta todo
artículos restantes. El problema es que el drenaje ya es estable, el iterador
el combinador agrega sobrecarga ya que necesita fusionar el iterador interno y drenar
podría no ser el nombre más apropiado.

El lunes 20 de mayo de 2019 a las 09:28 Ralf Jung [email protected] escribió:

¿Valdría la pena abrir un número separado para el error de solidez? Eso puede
entonces ser marcado como I-unsound.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/43244?email_source=notifications&email_token=AB2HJEL7FS6AA2A2KF5U2S3PWJHK7A5CNFSM4DTDLGPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVX5RWA#issuecomment
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AB2HJEMHQFRTCH6RCQQ64DTPWJHK7ANCNFSM4DTDLGPA
.

Sin embargo, las caídas dobles no son buenas.

Creado #60977 para el problema de solidez

Gracias, me siento estúpido por leer double drop como double panic :man_facepalming: .

  1. => Diferencia inesperada entre caer dentro y fuera de un pánico. Sólo
    considere a alguien _usando_ filtro_de_drenaje en una función de eliminación.

3.,4. hazlo para que realmente no puedas
úselo en un método de soltar, pero ¿cómo sabe ahora una función que usa allí?
no lo usa internamente.

Esto para mí todavía no es gran cosa.

Si alguien usa drain_filter en una implementación Drop con una condición que puede entrar en pánico, el problema no es que haya elegido usar drain_filter ; Del mismo modo, no importa si un método usa drain_filter internamente;

Lo siento, respondí demasiado pronto. Creo que entiendo lo que quieres decir ahora. Reflexionaré sobre esto un poco más.

Muy bien, su argumento es que el código dentro de un impl drop que usa drain_filter puede romperse misteriosamente si se ejecuta durante el desenrollado. (No se trata de que drain_filter pánico, sino de otro código que entra en pánico y hace que se ejecute 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
        ...
    }
}

Este impl de caída repentinamente se comportaría mal durante la relajación.

De hecho, este es un argumento convincente en contra de que DrainFilter detecte ingenuamente si el hilo actual está entrando en pánico.

La terminología drain_filter es la que más me beneficia desde entonces. Dado que ya tenemos drain para eliminar todos los elementos, seleccionar qué elementos eliminar sería filter . Cuando se combinan, la denominación se siente muy consistente.

La solución para el problema de solidez de pánico doble deja el resto de Vec intacto en caso de pánico en el predicado. Los elementos se desplazan hacia atrás para llenar el espacio, pero de lo contrario se dejan solos (y se dejan caer a través Vec::drop durante el desenrollado o el usuario los maneja de otra manera si se detecta el pánico).

Dejar caer un vec::DrainFilter prematuramente sigue comportándose como si se hubiera consumido por completo (que es lo mismo que vec::Drain ). Si el predicado entra en pánico durante vec::Drain::drop , los elementos restantes se desplazan hacia atrás normalmente, pero no se eliminan más elementos y el predicado no se vuelve a llamar. Esencialmente, se comporta de la misma manera, independientemente de si se produce un pánico en el predicado durante el consumo normal o cuando se eliminan los vec::DrainFilter .

Suponiendo que la solución para el agujero de sonido sea correcta, ¿qué más está frenando la estabilización de esta función?

¿Se puede estabilizar Vec::drain_filter independientemente de LinkedList::drain_filter ?

El problema con la terminología drain_filter es que con drain_filter , en realidad hay dos salidas: el valor de retorno y la colección original, y no está muy claro de qué lado está "filtrado". los artículos continúan. Creo que incluso filtered_drain es un poco más claro.

no está muy claro de qué lado van los elementos "filtrados"

Vec::drain sienta un precedente. Usted especifica el rango de elementos que desea _eliminar_. Vec::drain_filter funciona de la misma manera. Usted identifica los elementos que desea _eliminar_.

y no está muy claro de qué lado van los elementos "filtrados"

Soy de la opinión de que esto ya es cierto para Iterator::filter , así que me he resignado a tener que mirar los documentos/escribir una prueba cada vez que uso eso. No me importa lo mismo para drain_filter .


Ojalá hubiéramos elegido la terminología select y reject de Ruby, pero ese barco hace mucho que zarpó.

¿Algún progreso en esto? ¿Es el nombre lo único que sigue en el limbo?

¿Hay algo que impida que esto se estabilice?

Parece que DrainFilter Drop impl perderá elementos si alguno de sus destructores entra en el predicado entra en pánico. Esta es la causa raíz de https://github.com/rust-lang/rust/issues/52267. ¿Estamos seguros de que queremos estabilizar una API como esa?

No importa, eso fue arreglado por https://github.com/rust-lang/rust/pull/61224 aparentemente.

También voy a tocar un poco este problema de seguimiento, me encantaría ver que esta función se estabilice: D ¿Hay algún bloqueador?

cc @Dylan-DPC

¿Alguna vez se tomó una decisión a favor o en contra de que drain_filter tome un parámetro RangeBounds , como lo hace drain ? Pasar .. parece bastante fácil cuando desea filtrar todo el vector, por lo que probablemente estaría a favor de agregarlo.

Creo que esto sería más versátil:

fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>

Casi, la versión más general tomaría FnMut(T) -> Option<U> , como Iterator::{filter_map, find_map, map_while} . No tengo idea si vale la pena generalizar filter_map de esta manera, pero podría valer la pena considerarlo.

Llegué aquí porque estaba buscando más o menos exactamente el método que @jethrogb sugirió arriba:

fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F>
    where F: FnMut(T) -> Option<T>

La única diferencia con lo que tenía en mente (que en mi cabeza llamaba update ) era que no había pensado en hacer que devolviera un iterador agotador, pero parece una clara mejora, ya que proporciona una interfaz única y razonablemente sencilla que admite la actualización de elementos en su lugar, la eliminación de elementos existentes y la entrega de los elementos eliminados a la persona que llama.

Casi, la versión más general tomaría un FnMut(T) -> Option<U> , como Iterator::{filter_map, find_map, map_while} . No tengo idea si vale la pena generalizar filter_map de esta manera, pero podría valer la pena considerarlo.

La función tiene que devolver Option<T> porque los valores que produce se almacenan en Vec<T> .

@ johnw42 No estoy seguro de seguir, ¿el iterador no arrojaría inmediatamente el valor Some ?

En realidad, supongo que el valor de entrada de esa función aún debe ser &T o &mut T en lugar de T en caso de que no quieras drenarlo. O tal vez la función podría ser algo como FnMut(T) -> Result<U, T> . Pero no veo por qué el tipo de elemento no podría ser de otro tipo.

@timvermeulen Creo que estamos interpretando la propuesta de manera diferente.

De la forma en que lo interpreté, el Some se almacena nuevamente en Vec , y None significa que el iterador produce el valor original. Eso permite que el cierre actualice el valor en su lugar o lo saque del Vec . Al escribir esto, me di cuenta de que mi versión realmente no agrega nada porque puedes implementarla en términos 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,
    })
}

Por el contrario, estaba pensando que su interpretación no es muy útil porque es equivalente a mapear el resultado de drain_filter , pero traté de escribirlo y no lo es, por la misma razón que filter_map no lo es t equivalente a llamar a filter seguido de map .

@johnw42 Ah, sí, pensé que querías que None significara que el valor debería permanecer en Vec .

Entonces parece que FnMut(T) -> Result<U, T> sería el más general, aunque probablemente no sea muy ergonómico. FnMut(&mut T) -> Option<U> no es realmente una opción porque eso no le permitiría tomar posesión de los T en el caso general. Creo que FnMut(T) -> Result<U, T> y FnMut(&mut T) -> bool son las únicas opciones.

@timvermeulen Comencé a decir algo antes sobre una solución "más general", y mi solución "más general" era diferente a la suya, pero llegué a la misma conclusión, que es que intentar hacer una función demasiado general da como resultado algo que usted en realidad no querría usar.

Aunque tal vez todavía tenga algún valor crear un método muy general sobre el que los usuarios avanzados puedan construir mejores abstracciones. Por lo que puedo decir, el punto de drain y drain_filter no es que sean API particularmente ergonómicas, no lo son, sino que admiten casos de uso que ocurren en práctica, y que no se puede escribir de otra manera sin muchos movimientos redundantes (o usando operaciones inseguras).

Con drain , obtienes las siguientes buenas propiedades:

  • Se puede eliminar cualquier selección contigua de elementos.
  • Descartar los elementos eliminados es tan simple como descartar el iterador devuelto.
  • Los elementos eliminados no tienen que ser eliminados; la persona que llama puede examinar cada uno individualmente y elegir qué hacer con él.
  • El contenido de Vec no necesita admitir Copy o Clone .
  • No es necesario asignar ni liberar memoria para el Vec en sí.
  • Los valores que quedan en Vec se mueven como máximo una vez.

Con drain_filter , obtiene la capacidad de eliminar un conjunto arbitrario de elementos de Vec en lugar de solo un rango contiguo. Una ventaja menos obvia es que incluso si se elimina un rango contiguo de elementos, drain_filter aún puede ofrecer un aumento de rendimiento si encontrar el rango para pasar a drain implicaría realizar un pase separado sobre Vec para inspeccionar su contenido. Debido a que el argumento del cierre es un &mut T , incluso es posible actualizar los elementos que quedan en el Vec . ¡Hurra!

Aquí hay algunas otras cosas que quizás desee hacer con una operación en el lugar como drain_filter :

  1. Transforme los elementos eliminados antes de devolverlos a través del iterador.
  2. Cancele la operación antes de tiempo e informe un error.
  3. En lugar de simplemente eliminar el elemento o dejarlo en su lugar (y posiblemente mutarlo), agregue la capacidad de eliminar el elemento y reemplazarlo con un nuevo valor (que podría ser un clon del valor original, o algo completamente diferente).
  4. Reemplace el elemento eliminado con varios elementos nuevos.
  5. Después de hacer algo con el elemento actual, omita una cantidad de elementos siguientes, dejándolos en su lugar.
  6. Después de hacer algo con el elemento actual, elimine algunos elementos siguientes sin inspeccionarlos primero.

Y aquí está mi análisis de cada uno:

  1. Esto no agrega nada útil porque la persona que llama ya puede transformar los elementos a medida que el iterador los devuelve. También anula el propósito del iterador, que es evitar la clonación de valores al entregarlos a la persona que llama solo después de que se hayan eliminado de Vec .
  2. Poder abortar temprano podría mejorar potencialmente la complejidad asintótica en algunos casos. Informar un error como parte de la API no agrega nada nuevo porque podría hacer lo mismo haciendo que el cierre mute una variable capturada, y no está claro cómo hacerlo, porque el valor no se producirá hasta después de que el iterador haya sido consumido.
  3. Esto, creo, añade cierta generalidad real.
  4. Esto es lo que originalmente iba a proponer como la opción de "fregadero de cocina", pero decidí que no es útil, porque si un solo artículo puede ser reemplazado por varios artículos, es imposible mantener la propiedad de los artículos en el Vec se mueven como máximo una vez y podría ser necesario reasignar el búfer. Si necesita hacer algo así, no es necesariamente más eficiente que simplemente construir un Vec completamente nuevo, y podría ser peor.
  5. Esto podría ser útil si el Vec está organizado de tal manera que pueda omitir una gran parte de los elementos sin detenerse a inspeccionarlos. No lo incluí en mi código de muestra a continuación, pero podría admitirse cambiando el cierre para devolver usize adicionales especificando cuántos de los siguientes elementos saltar antes de continuar.
  6. Esto parece complementario al elemento 5, pero no es muy útil si aún necesita devolver los elementos eliminados a través del iterador. Sin embargo, aún podría ser una optimización útil si los elementos que está eliminando no tienen destructor y solo desea hacerlos desaparecer. En ese caso, el usize anterior podría reemplazarse con una opción de Keep(usize) o Drop(usize) (donde Keep(0) y Drop(0) son semánticamente equivalente).

Creo que podemos respaldar los casos de uso esenciales haciendo que el cierre devuelva una enumeración con 4 casos:

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,
}

Una última opción que me gustaría presentar es deshacerme del iterador por completo, pasar elementos al cierre por valor y permitir que la persona que llama deje un elemento sin cambios reemplazándolo consigo mismo:

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,
}

Me gusta más este enfoque porque es simple y admite los mismos casos de uso. La desventaja potencial es que, incluso si la mayoría de los artículos se dejan en su lugar, aún deben moverse al marco de pila del cierre y luego regresar cuando el cierre regresa. Uno esperaría que esos movimientos pudieran optimizarse de manera confiable cuando el cierre simplemente devuelve su argumento, pero no estoy seguro de si eso es algo con lo que deberíamos contar. Si a otras personas les gusta lo suficiente como para incluirlo, creo que update sería un buen nombre porque, si no me equivoco, se puede usar para implementar cualquier actualización in situ de un solo paso de un Contenido Vec .

(Por cierto, ignoré por completo las listas enlazadas anteriores porque me olvidé de ellas por completo hasta que miré el título de este número. Si estamos hablando de una lista enlazada, cambia el análisis de los puntos 4-6, así que creo que es diferente). La API sería apropiada para las listas enlazadas).

@johnw42 ya puede hacer 3. si tiene una referencia mutable, usando mem::replace o mem::take .

@johnw42 @jplatte

(3) solo tiene sentido si permitimos que el tipo de elemento del Iterator devuelto sea diferente del tipo de elemento de la colección.
(3) es un caso especial, porque ambos devuelven el elemento de Iterator y colocan un nuevo elemento en Vec .

Bikeshedding: invertiría un poco la función de Replace(T) y la reemplazaría con PushOut(T) , con el propósito de "enviar" el valor interno de PushOut al iterador, manteniendo el elemento original (parámetro) en Vec .

Stop probablemente debería tener la capacidad de devolver un tipo Error (¿o funcionar un poco como try_fold ?).

Anoche implementé mi función super_drain_by_value y aprendí varias cosas.

El elemento principal probablemente debería ser que, al menos con Vec , todo lo que estamos hablando aquí está en la categoría "agradable de tener" (en lugar de agregar una capacidad fundamentalmente nueva), porque Vec ya proporciona esencialmente acceso directo de lectura y escritura a todos sus campos a través de la API existente. En la versión estable, hay una pequeña advertencia de que no puede observar el campo de puntero de un Vec vacío, pero el método inestable into_raw_parts elimina esa restricción. De lo que realmente estamos hablando es de expandir el conjunto de operaciones que se pueden realizar de manera eficiente mediante un código seguro.

En términos de generación de código, descubrí que en los casos fáciles (por ejemplo Vec<i32> ), los movimientos redundantes dentro y fuera de Vec no son un problema, y ​​las llamadas equivalen a cosas simples como un no-op o truncar los Vec se transforman en un código que es demasiado simple para mejorarlo (cero y tres instrucciones, respectivamente). La mala noticia es que, para los casos más difíciles, tanto mi propuesta como el método drain_filter hacen muchas copias innecesarias, lo que anula en gran medida el propósito de los métodos. Probé esto mirando el código ensamblador generado para un Vec<[u8; 1024]> y, en ambos casos, cada iteración tiene dos llamadas a memcpy que no están optimizadas. ¡Incluso una llamada no operativa termina copiando todo el búfer dos veces!

En términos de ergonomía, mi API, que se ve muy bien a primera vista, no lo es tanto en la práctica; devolver un valor de enumeración del cierre se vuelve bastante detallado en todos los casos excepto en los más simples, y la variante que propuse donde el cierre devuelve un par de valores de enumeración es aún más fea.

También intenté extender DrainAction::Stop para llevar un R que se devuelve desde super_drain_by_value como Option<R> , y eso es aún peor, porque en el (presumiblemente típico) En caso de que no se necesite el valor devuelto, el compilador no puede inferir R y debe anotar explícitamente el tipo de valor que ni siquiera está usando. Por esta razón, no creo que sea una buena idea apoyar la devolución de un valor del cierre a la persona que llama de super_drain_by_value ; es más o menos análogo a por qué una construcción loop {} puede devolver un valor, pero cualquier otro tipo de ciclo se evalúa como () .

Con respecto a la generalidad, me di cuenta de que en realidad hay dos casos de terminación prematura: uno donde el resto de Vec se descarta y otro donde se deja en su lugar. Si la terminación prematura no tiene un valor (como creo que no debería), se vuelve semánticamente equivalente a devolver Keep(n) o Drop(n) , donde n es el número de elementos aún no examinados. Sin embargo, creo que la terminación prematura debe tratarse como un caso separado, porque en comparación con el uso de Keep / Drop , es más fácil de usar a través de una ruta de código más simple.

Para hacer que la API sea un poco más amigable, creo que una mejor opción sería hacer que el cierre devuelva () y pasarle un objeto auxiliar (al que me referiré aquí como un "actualizador") que puede ser se usa para inspeccionar cada elemento del Vec y controlar lo que le sucede. Estos métodos pueden tener nombres familiares como borrow , borrow_mut y take , con métodos adicionales como keep_next(n) o drop_remainder() . Usando este tipo de API, el cierre es mucho más simple en casos simples y no más complejo en casos complejos. Al hacer que la mayoría de los métodos del actualizador tomen self por valor, es fácil evitar que la persona que llama haga cosas como llamar a take más de una vez o dar instrucciones contradictorias sobre qué hacer en iteraciones posteriores.

¡Pero aún podemos hacerlo mejor! Esta mañana me di cuenta de que, como suele ocurrir, este problema es análogo a uno que se ha resuelto definitivamente en los lenguajes funcionales, y podemos resolverlo con una solución análoga. Me refiero a las API "cremalleras", descritas por primera vez en este breve documento con código de muestra en OCaml, y descritas aquí con código Haskell y enlaces a otros documentos relevantes. Las cremalleras proporcionan una forma muy general de atravesar una estructura de datos y actualizarla "en el lugar" utilizando cualquier operación que admita esa estructura de datos en particular. Otra forma de verlo es que una cremallera es una especie de iterador turboalimentado con métodos adicionales para realizar operaciones en un tipo específico de estructura de datos.

En Haskell, obtienes la semántica "en el lugar" al hacer que la cremallera sea una mónada; en Rust, puede hacer lo mismo usando vidas haciendo que la cremallera contenga una referencia mut al Vec . Un cierre para un Vec es muy similar al actualizador que describí anteriormente, excepto que en lugar de pasarlo repetidamente a un cierre, el Vec solo proporciona un método para crear un cierre en sí mismo, y la cremallera tiene acceso exclusivo a los Vec mientras exista. Luego, la persona que llama se vuelve responsable de escribir un bucle para atravesar la matriz, llamando a un método en cada paso para eliminar el elemento actual de Vec o dejarlo en su lugar. La terminación anticipada se puede implementar llamando a un método que consume la cremallera. Debido a que el ciclo está bajo el control de la persona que llama, es posible hacer cosas como procesar más de un elemento en cada iteración del ciclo o manejar una cantidad fija de elementos sin usar ningún ciclo.

Aquí hay un ejemplo muy artificial que muestra algunas de las cosas que puede hacer una cremallera:

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

A modo de comparación, aquí hay más o menos la misma función usando drain_filter , excepto que solo pretende detenerse antes de tiempo. Se trata de la misma cantidad de código, pero en mi humilde opinión, es mucho más difícil de leer porque el significado del valor devuelto por el cierre no es obvio, y está usando banderas booleanas mutables para llevar información de una iteración a la siguiente, donde la cremallera versión logra lo mismo con control de flujo. Debido a que el iterador siempre produce los elementos eliminados, necesitamos un paso de filtro separado para eliminar los valores negativos de la salida, lo que significa que debemos verificar los valores negativos en dos lugares en lugar de uno. También es un poco feo que tenga que realizar un seguimiento de la posición en v ; la implementación de drain_filter tiene esa información, pero la persona que llama no tiene acceso a ella.

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 Su publicación anterior me recordó la caja scanmut , específicamente la estructura Remover , ¡y el concepto de "cremallera" que mencionó parece muy similar! Eso parece mucho más ergonómico que un método que requiere un cierre para cuando desea un control total.

De cualquier manera, esto probablemente no sea muy relevante para estabilizar drain_filter , ya que siempre podemos cambiar las partes internas más tarde. drain_filter en sí mismo siempre será muy útil debido a lo conveniente que es. El único cambio que me gustaría ver antes de la estabilización es un parámetro RangeBounds .

@timvermeulen Creo que tiene sentido agregar un parámetro RangeBounds , pero mantener la firma de cierre actual ( F: FnMut(&mut T) -> bool ).
Siempre puede posprocesar los elementos drenados con filter_map o lo que desee.
(Para mí, es muy importante que el cierre permita mutar el elemento, porque retain no lo permite (se estabilizó antes de que se descubriera este error).)

Sí, ese parece ser el equilibrio perfecto entre conveniencia y utilidad.

@timvermeulen Sí, me estaba desviando bastante del tema principal.

Una cosa que noté que es relevante para el tema original es que es un poco difícil recordar lo que significa el valor de retorno del cierre: ¿dice si mantener el elemento o eliminarlo? Creo que sería útil que los documentos señalaran que v.drain_filter(p) es equivalente a v.iter().filter(p) con efectos secundarios.

Con filter , el uso de un valor booleano sigue siendo menos que ideal para la claridad, pero es una función muy conocida, y en mi humilde opinión es al menos algo intuitivo que el predicado responde a la pregunta "¿debería mantener esto?" en lugar de "¿debería descartar esto?" Con drain_filter , se aplica la misma lógica si lo piensa desde la perspectiva del iterador, pero si lo piensa desde la perspectiva de la entrada Vec , la pregunta es "¿debería NO ¿guarda esto?"

En cuanto a la redacción exacta, propongo cambiar el nombre del parámetro filter a predicate (para que coincida con Iterator::filter ) y agregar esta oración en algún lugar de la descripción:

Para recordar cómo se usa el valor de retorno de predicate , puede ser útil tener en cuenta que drain_filter es idéntico a Iterator::filter con el efecto secundario adicional de eliminar el valor seleccionado. artículos desde self .

@ johnw42 Sí, buen punto. Creo que un nombre como drain_where sería mucho más claro.

Si te vas a poner a nombrar bikeshedding; por favor asegúrese de haber leído todos los comentarios; incluso los ocultos. Ya se han propuesto muchas variantes, por ejemplo, https://github.com/rust-lang/rust/issues/43244#issuecomment -331559537

Pero… ¡ tiene que llamarse draintain() ! ¡Ningún otro nombre es tan hermoso!

Estoy bastante interesado en este problema y leí todo el hilo, así que también podría tratar de resumir lo que todos dijeron, con la esperanza de ayudar a que esto se estabilice. He agregado algunos de mis propios comentarios en el camino, pero he tratado de mantenerlos lo más neutrales posible.

Denominación

Aquí hay un resumen sin opiniones de los nombres que vi propuestos:

  • drain_filter : El nombre utilizado en la implementación actual. Consistente con otros nombres como filter_map . Tiene la ventaja de ser análogo a drain().filter() , pero con más efectos secundarios.
  • drain_where : Tiene la ventaja de indicar si true da como resultado drenar _out_ o filtrar _in_, lo que puede ser difícil de recordar con otros nombres. No hay precedentes en std para el sufijo _where , pero hay muchos precedentes para sufijos similares.
  • Una variación de drain().where() , ya que where ya es una palabra clave.
  • drain_retain : Consistente con retain , pero retain y drain tienen interpretaciones opuestas de los valores booleanos devueltos por el cierre, lo que puede resultar confuso.
  • filtered_drain
  • drain_if
  • drain_when
  • remove_if

Parámetros

Podría valer la pena agregar un argumento de rango para mantener la coherencia con drain .

Se han sugerido dos formatos de cierre, FnMut(&mut T) -> bool y FnMut(T) -> Result<T, U> . Este último es más flexible, pero también más torpe.

Se discutió la inversión de la condición booleana ( true significa "mantener en Vec ") para ser consistente con retain , pero entonces no sería consistente con drain ( true significa "drenar de los Vec ").

relajarse

Cuando el cierre del filtro entra en pánico, se descarta el iterador DrainFilter . Luego, el iterador debería terminar de drenar Vec , pero para hacerlo debe volver a llamar al cierre del filtro, arriesgándose a un pánico doble. Hay algunas soluciones, pero todas ellas son compromisos:

  • No termine de drenar en gota. Esto es bastante contradictorio cuando se usa con adaptadores como find o all . Además, hace que el modismo v.drain_filter(...); sea ​​inútil ya que los iteradores son perezosos.

  • Siempre termine de drenar en gota. Esto corre el riesgo de pánicos dobles (que resultan en abortos), pero hace que el comportamiento sea consistente.

  • Solo termine de drenar al soltar si no se está desenrollando actualmente. Esto corrige los pánicos dobles por completo, pero hace que el comportamiento de drain_filter sea ​​impredecible: dejar caer DrainFilter en un destructor podría _a veces_ no hacer su trabajo.

  • Solo termine de drenar por goteo si el cierre del filtro no entró en pánico. Este es el compromiso actual realizado por drain_filter . Una buena propiedad de este enfoque es que entra en pánico en el "cortocircuito" del cierre del filtro, lo que podría decirse que es bastante intuitivo.

Tenga en cuenta que la implementación actual es sólida y nunca se filtra mientras se elimine la estructura DrainFilter (aunque puede provocar un aborto). Sin embargo, las implementaciones anteriores no eran seguras ni estaban libres de fugas.

Drenaje en gota

DrainIter podría terminar de drenar el vector de origen cuando se suelta, o solo podría drenarse cuando se llama a next (iteración diferida).

Argumentos a favor del drenaje en gota:

  • Consistente con el comportamiento de drain .

  • Interactúa bien con otros adaptadores como all , any , find , etc...

  • Habilita el modismo vec.drain_filter(...); .

  • La funcionalidad perezosa podría habilitarse explícitamente a través de métodos de estilo drain_lazy o un adaptador lazy() en DrainIter (e incluso en Drain , ya que es compatible con versiones anteriores de añadir métodos).

Argumentos a favor de la iteración perezosa:

  • Consistente con casi todos los otros iteradores.

  • La funcionalidad "drain-on-drop" podría habilitarse explícitamente a través de adaptadores en DrainIter , o incluso a través de un adaptador general Iterator::exhausting (ver RFC #2370 ).

Es posible que me haya perdido algunas cosas, pero al menos espero que ayude a los recién llegados al hojear el hilo.

@negamartin

¿La opción drain-on-drop no requeriría que el iterador devuelva una referencia al elemento en lugar del valor de propiedad? Creo que eso haría imposible usar drain_filter como un mecanismo para eliminar y tomar posesión de elementos que coincidan con una condición específica (que era mi caso de uso original).

No lo creo, ya que el comportamiento de la implementación actual es precisamente drenaje-sobre-caída mientras produce valores propios. De cualquier manera, no veo cómo el drenaje sobre la gota requeriría elementos prestados, así que creo que tenemos dos ideas diferentes sobre lo que significa el drenaje sobre la gota.

Para que quede claro, cuando digo drenaje en caída solo me refiero al comportamiento cuando el iterador no se consume por completo: ¿Deberían drenarse todos los elementos que coincidan con el cierre incluso si el iterador no se consume por completo? ¿O solo hasta el elemento que se consumió, dejando el resto intacto?

En particular, es la diferencia 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]);

Solo estoy lanzando una idea, pero otra posible API podría ser algo como:

 fn drain_filter_into<F, D>(&mut self, filter: F, drain: D)
        where F: FnMut(&mut T) -> bool, 
                   D: Extend<T>
    { ... }

Es menos flexible que las otras opciones, pero evita el problema de qué hacer cuando se descarta DrainFilter .

Me parece que todo esto se parece cada vez menos a retain_mut() ( retain() con una referencia mutable pasada al cierre), que es lo que se pretendía, ante todo, proporcionar . ¿Podríamos proporcionar retain_mut() por ahora además de trabajar en el diseño del desagüe filtrado? ¿O me estoy perdiendo algo?

@BartMassey

que es lo que se pretendía en primer lugar y sobre todo proporcionar.

No creo que ese sea el caso. Uso específicamente drain_filter para tomar posesión de los elementos según los criterios de filtro. Drain y DrainFilter dan como resultado el artículo mientras que retener no lo hace.

@negamartin

Para que quede claro, cuando digo drenaje en gota solo me refiero al comportamiento cuando el iterador no se consume por completo

Está bien. Ese es mi error. No entendí bien tu definición. Lo había interpretado como "nada se elimina del vec hasta que se cae", lo que realmente no tiene ningún sentido.

Argumentos a favor de la iteración perezosa

Creo que debe ser consistente con drain . No se aceptó Iterator::exhausting RFC y sería muy extraño que drain y drain_filter tuvieran comportamientos de drenaje aparentemente opuestos.

@negamartin

Drain_filter: el nombre utilizado en la implementación actual. De acuerdo con otros nombres como filter_map. Tiene la ventaja de ser análogo a drain().filter() , pero con más efectos secundarios.

No es análogo (es por eso que necesitamos retain_mut / drain_filter ):
¡ drain().filter() drenaría incluso aquellos elementos para los cuales el cierre del filtro devuelve false !

Acabo de notar una pequeña línea en un comentario del equipo lib en #RFC 2870 :

Las posibilidades pueden incluir "sobrecargar" un método haciéndolo genérico o un patrón de construcción.

¿Es compatible con versiones anteriores hacer que un método sea genérico si todavía acepta el tipo concreto anterior? Si es así, creo que sería la mejor manera de avanzar.

(El patrón de construcción es poco intuitivo con los iteradores, ya que los métodos en los iteradores suelen ser adaptadores, no modificadores de comportamiento. Además, no hay precedentes, por ejemplo, chunks y chunks_exact son dos métodos separados , no un combo chunks().exact() ).

No, no con el diseño actual hasta donde yo sé como tipo de inferencia que
trabajado antes ahora podría fallar debido a la ambigüedad del tipo. Genética con defecto
los tipos de funciones ayudarían, pero son muy difíciles de hacer bien.

El viernes, 12 de junio de 2020 a las 21:21, negamartin [email protected] escribió:

Acabo de notar una pequeña línea en un comentario del equipo lib en #RFC 2870
https://github.com/rust-lang/rfcs/pull/2369 :

Las posibilidades podrían incluir "sobrecargar" un método haciéndolo genérico,
o un patrón constructor.

¿Es compatible con versiones anteriores hacer que un método sea genérico si todavía acepta
el tipo de hormigón anterior? Si es así, creo que sería la mejor manera.
adelante.

(El patrón del constructor es un poco poco intuitivo con los iteradores, ya que los métodos en
los iteradores suelen ser adaptadores, no modificadores de comportamiento. Además, hay
sin precedentes, por ejemplo, chunks y chunks_exact son dos
métodos, no un combo chunks().exact().)


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-643444213 ,
o darse de baja
https://github.com/notifications/unsubscribe-auth/AB2HJELPWXNXJMX2ZDA6F63RWJ53FANCNFSM4DTDLGPA
.

¿Es compatible con versiones anteriores hacer que un método sea genérico si todavía acepta el tipo concreto anterior? Si es así, creo que sería la mejor manera de avanzar.

No, ya que rompe la inferencia de tipos en algunos casos. Por ejemplo foo.method(bar.into()) funcionará con un argumento de tipo concreto, pero no con un argumento genérico.

Creo que Drain_filter, tal como está implementado ahora, es muy útil. ¿Se podría estabilizar como está? Si se descubren mejores abstracciones en el futuro, nada impide que se introduzcan también.

¿Qué proceso debo iniciar para tratar de agregar retain_mut() , independientemente de lo que suceda con drain_filter() ? Me parece que los requisitos han divergido, y que retain_mut() todavía sería útil independientemente de lo que suceda con drain_filter() .

@BartMassey para nuevas API de bibliotecas inestables, creo que hacer un PR con una implementación debería estar bien. Hay instrucciones en https://rustc-dev-guide.rust-lang.org/implementing_new_features.html para implementar la función y en https://rustc-dev-guide.rust-lang.org/getting-started.html #building -and-testing-stdcorealloctestproc_macroetc para probar sus cambios.

He estado luchando con las diferencias de API entre HashMap y BTreeMap hoy y solo quería compartir una advertencia de que creo que es importante que varias colecciones se esfuercen por mantener una API coherente cada vez que se hace sentido, algo que a estas alturas no siempre es así.

Por ejemplo, String, Vec, HashMap, HashSet, BinaryHeap y VecDeque tienen un método retain , pero LinkedList y BTreeMap no. Lo encuentro especialmente extraño ya que retain parece un método más natural para una lista enlazada o un mapa que para vectores donde la eliminación aleatoria es una operación muy costosa.

Y cuando profundiza un poco más, es aún más desconcertante: al cierre HashMap::retain se le pasa el valor en una referencia mutable, pero las otras colecciones obtienen una referencia inmutable (y String obtiene un simple char ).

Ahora veo que se están agregando nuevas API como drain_filter que 1/ parecen superponerse con retain y 2/ no están estabilizadas para todas las colecciones al mismo tiempo:

  • HashMap::drain_filter está en el repositorio ascendente pero aún no se envió con el AFAIK estándar de Rust (no aparece en los documentos)
  • BTreeMap::drain_filter , Vec::drain_filter , LinkedList::drain_filter están en Rust's std, pero cuentan con funciones cerradas
  • VecDeque::drain_filter no parece existir en absoluto, no aparece en los documentos
  • String::drain_filter tampoco existe

No tengo una opinión firme sobre la mejor manera de implementar estas características, o si necesitamos drain_filter , retain o ambos, pero creo firmemente que estas API deben permanecer consistentes en todas las colecciones. .

Y quizás lo más importante, los métodos similares de diferentes colecciones deberían tener la misma semántica. Algo que las implementaciones actuales de retain violan IMO.

¿Fue útil esta página
0 / 5 - 0 calificaciones