Rust: Problema de rastreamento para Vec::drain_filter e LinkedList::drain_filter

Criado em 15 jul. 2017  ·  119Comentários  ·  Fonte: 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,
    { ... }

Tenho certeza de que há um problema para isso em algum lugar, mas não consigo encontrá-lo. Alguém nerd me chamou para implementá-lo. PR entrante.

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

Comentários muito úteis

Existe alguma coisa impedindo que isso seja estabilizado?

Todos 119 comentários

Talvez isso não precise incluir a pia da cozinha, mas _poderia_ ter um parâmetro de alcance, de modo que é como um superconjunto de ralo. Alguma desvantagem nisso? Acho que adicionar verificação de limites para o intervalo é uma desvantagem, é outra coisa que pode entrar em pânico. Mas drain_filter(.., f) não pode.

Existe alguma chance de isso se estabilizar de alguma forma em um futuro não muito distante?

Se o compilador for inteligente o suficiente para eliminar as verificações de limites
no caso drain_filter(.., f) eu optaria por fazer isso.

(E eu tenho certeza que você pode implementá-lo de uma maneira
o que torna o compilador bastante inteligente, na pior das hipóteses
caso você possa ter uma "especialização em função",
basicamente algo como if Type::of::<R>() == Type::of::<RangeFull>() { dont;do;type;checks; return } )

Eu sei que isso é uma queda de bicicleta até certo ponto, mas qual foi o raciocínio por trás de nomear isso drain_filter em vez de drain_where ? Para mim, o primeiro implica que todo o Vec será drenado, mas que também executamos um filter sobre os resultados (quando o vi pela primeira vez, pensei: "como isso não é apenas .drain(..).filter() ?"). O primeiro, por outro lado, indica que apenas drenamos elementos onde alguma condição se mantém.

Não faço ideia, mas drain_where soa muito melhor e é muito mais intuitivo.
Ainda há chance de mudar?

.remove_if também foi uma sugestão anterior

Acho que drain_where explica melhor. Como drenar, ele retorna valores, mas não drena/remove todos os valores, mas apenas quando uma determinada condição é verdadeira.

remove_if soa muito como uma versão condicional de remove que apenas remove um único item por índice se uma condição for verdadeira, por exemplo letters.remove_if(3, |n| n < 10); remove a letra no índice 3 se for < 10 .

drain_filter por outro lado é um pouco ambíguo, faz drain então filter de uma maneira mais otimizada (como filter_map) ou se drena para que um iterador seja retornado comparável a o iterador filter retornaria,
e se sim, não deveria ser chamado filtered_drain já que o filtro é usado logicamente antes ...

Não há precedente para usar _where ou _if em qualquer lugar na biblioteca padrão.

@Gankro existe um precedente para usar _filter em qualquer lugar? Também não sei se essa é uma razão para não usar a terminologia menos ambígua? Outros lugares na biblioteca padrão já usam uma variedade de sufixos como _until e _while .

O código "disse equivalente" no comentário não está correto ... você tem que menos um de i no site "seu código aqui", ou coisas ruins acontecem.

IMO não é filter esse é o problema. Tendo acabado de pesquisar por isso (e sendo um novato), drain parece ser bastante fora do padrão em comparação com outros idiomas.

Novamente, apenas de uma perspectiva de novato, as coisas que eu procuraria se tentasse encontrar algo para fazer o que este problema propõe seria delete (como em delete_if ), remove , filter ou reject .

Na verdade eu _procurei_ por filter , vi drain_filter e _continuei procurando_ sem ler porque drain não parecia representar a coisa simples que eu queria fazer.

Parece que uma função simples chamada filter ou reject seria muito mais intuitiva.

Em uma nota separada, não sinto que isso deva alterar o vetor que é chamado. Impede o encadeamento. Em um cenário ideal, alguém gostaria de poder fazer algo como:

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

Com a implementação atual, o que estaria juntando seriam os valores rejeitados.

Eu gostaria de ver um accept e um reject . Nenhum dos quais altera o valor original.

Você já pode fazer o encadeamento com filter sozinho. O ponto inteiro de drain_filter é alterar o vetor.

@rpjohnst então pesquisei aqui , estou perdendo filter em algum lugar?

Sim, é membro de Iterator , não de Vec .

Drain é uma terminologia nova porque representou um quarto tipo de propriedade em Rust que se aplica apenas a contêineres, embora geralmente seja uma distinção sem sentido em quase qualquer outra linguagem (na ausência de semântica de movimento, não há necessidade de combinar iteração e remoção em uma única operação ""atômica"").

Embora o filtro_dreno mova a terminologia de drenagem para um espaço com o qual outros idiomas se importariam (já que evitar retrocessos é relevante em todos os idiomas).

Encontrei drain_filter em documentos como resultado do Google para rust consume vec . Eu sei que devido à imutabilidade por padrão na ferrugem, o filtro não consome os dados, apenas não conseguia lembrar como abordá-lo para que eu pudesse gerenciar melhor a memória.

drain_where é legal, mas desde que o usuário esteja ciente do que drain e filter fazem, acho que está claro que o método drena os dados com base em um filtro de predicado.

Eu ainda sinto que drain_filter implica que ele drena (ou seja, esvazia) e depois filtra. drain_where por outro lado, parece que drena os elementos onde a condição dada é válida (que é o que a função proposta faz).

linked_list::DrainFilter também não deveria implementar Drop para remover quaisquer elementos restantes que correspondam ao predicado?

sim

Por que exatamente descartar o iterador faz com que ele seja executado até o final? Acho que esse é um comportamento surpreendente para um iterador e também pode ser, se desejado, feito explicitamente. O inverso de retirar apenas quantos elementos você precisar é impossível, por outro lado, porque mem::forget ing o iterador é executado em amplificação de vazamento.

Eu tenho usado muito essa função e sempre tenho que lembrar de retornar true para as entradas que quero drenar, o que parece contra-intuitivo em comparação com retain() / retain_mut() .
Em um nível lógico intuitivo, faria mais sentido retornar true para entradas que quero manter, alguém mais se sente assim? (Principalmente considerando que retain() já funciona assim)
Por que não fazer isso e renomear drain_filter() para retain_iter() ou retain_drain() (ou drain_retain() )?
Então também espelharia retain() mais de perto!

É por isso que propus renomeá-lo para
drain_where , pois fica claro que:

  1. É uma forma de dreno, então usamos dreno no nome

  2. Ao usar where em combinação com drain fica claro que o
    elementos _onde_ o predicado é verdadeiro são drenados, ou seja, removidos e retornados

  3. Está mais em sincronia com outros nomes em std, normalmente se você tiver um
    função que consiste em dois predicados, você pode emulá-la (aproximadamente) usando
    funções que representam cada um dos predicados de uma forma "e depois", por exemplo
    filter_map pode ser emulado (aproximadamente) como filter an then map . O actual
    name indica que é drain and then filter , mas não chega nem perto disso
    pois não faz uma drenagem completa.

No domingo, 25 de fevereiro de 2018, 17:04 Boscop [email protected] escreveu:

Eu tenho usado muito essa função e sempre tenho que lembrar de
retorne true para as entradas que quero drenar, o que parece
contra-intuitivo em comparação com reter_mut().
Em um nível primal, faria mais sentido retornar true para entradas I
quer manter, mais alguém se sente assim?
Por que não fazer isso e renomear drain_filter() para retain_filter()?


Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368320990 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AHR0kfwaNvz8YBwCE4BxDkeHgGxLvcWxks5tYYRxgaJpZM4OY1me
.

Mas com drain_where() o fechamento ainda teria que retornar true para elementos que deveriam ser removidos, o que é o oposto de retain() que o torna inconsistente.
Talvez retain_where ?
Mas acho que você está certo de que faz sentido ter "dreno" no nome, então acho que drain_retain() faz mais sentido: é como drain() mas mantendo os elementos onde o fechamento retorna true .

Enquanto isso não muda, que você tem que retornar true deixa claro que
você tem que fazer isso.

Eu pessoalmente usaria:

uma. drain_where
b. retain_where
c. retain

Eu não usaria drain_retain .
Drenar e reter falam sobre o mesmo tipo de processo, mas de lados opostos.
perspectivas, dreno fala sobre o que você remove (e devolve) retém
fala sobre o que você guarda (da forma como já é usado em std).

Atualmente retaim já está implementado em std com a grande diferença
que está descartando elementos enquanto drain os está retornando, o que torna
retain (infelizmente) inadequado para ser usado no nome (exceto se você quiser chamar
retain_and_return ou similar).

Outro ponto que fala por dreno é a facilidade de migração, ou seja, migrar
para drain_where é tão fácil quanto executar uma pesquisa baseada em palavras e substituir em
o código, enquanto alterá-lo para reter precisaria de uma negação adicional de
todos os predicados/funções de filtro usados.

No domingo, 25 de fevereiro de 2018, 18:01 Boscop [email protected] escreveu:

Mas com drain_where() o encerramento ainda teria que retornar true para
elementos que devem ser removidos, que é o oposto de reter() que
torna incoerente..
Talvez reter_onde?
Mas acho que você está certo que faz sentido ter "dreno" no nome,
então eu acho que drain_retain() faz mais sentido: é como drenar() mas
retendo os elementos onde o encerramento retorna true.


Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368325374 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AHR0kfG4oZHxGfpOSK8DjXW3_2O1Eo3Rks5tYZHxgaJpZM4OY1me
.

Mas com que frequência você migraria de drain() para drain_filter() ?
Em todos os casos até agora, migrei de retain() para drain_filter() porque não há retain_mut() em std e preciso alterar o elemento! Então eu tive que inverter o valor de retorno do encerramento ..
Acho que drain_retain() faz sentido porque o método drain() drena incondicionalmente todos os elementos no intervalo, enquanto drain_retain() retém os elementos onde o encerramento retorna true , ele combina os efeitos dos métodos drain() e retain() .

Desculpe, pretendo migrar de drain_filter para drain_where .

Que você tem uma solução usando retain e então precisa usar
drain_filter é um aspecto que ainda não considerei.

No domingo, 25 de fevereiro de 2018, 19:12 Boscop [email protected] escreveu:

Mas por que você migraria de Drain() para Drain_filter()?
Em todos os casos até agora, migrei de keep() para drain_filter()
porque não há retentor_mut() em std e preciso alterar o elemento!
Eu acho que drain_retain() faz sentido porque o método drain() drena
incondicionalmente todos os elementos no intervalo, enquanto que drain_retain() retém
os elementos em que o encerramento retorna verdadeiro.


Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368330896 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AHR0kSayIk_fbp5M0RsZW5pYs3hDICQIks5tYaJ0gaJpZM4OY1me
.

Ah sim, mas acho que o "preço" de inverter os closures no código atual que usa drain_filter() vale a pena, para obter uma API consistente e intuitiva em std e depois em stable.
É apenas um pequeno custo fixo (e facilitado pelo fato de que acompanharia uma renomeação da função, para que o erro do compilador pudesse informar ao usuário que o fechamento deve ser invertido, para que não introduzisse silenciosamente um bug) , comparado ao custo de padronizar drain_filter() e então as pessoas sempre tendo que inverter o fechamento ao migrar de retain() para drain_filter() .. (além do custo mental de lembrar de fazer isso, e os custos de tornar mais difícil encontrá-lo nos documentos, vindo de retain() e procurando por "algo como retain() mas passando &mut para seu fechamento, o que é por isso que acho que faz sentido que o novo nome dessa função tenha "reter" no nome, para que as pessoas o encontrem ao pesquisar nos documentos).

Alguns dados anedóticos: No meu código, sempre precisei apenas do aspecto retain_mut() de drain_filter() (geralmente eles estavam usando retain() antes), nunca tive casos de uso em que precisei processar os valores drenados. Eu acho que este também será o caso de uso mais comum para outros no futuro (já que retain() não passa &mut para seu fechamento, então drain_filter() tem que cobrir esse caso de uso , também, e é um caso de uso mais comum do que a necessidade de processar os valores drenados).

A razão pela qual sou contra drain_retain é por causa da maneira como os nomes são usados ​​atualmente em std wrt. coleções:

  1. você tem nomes de funções usando predicados que têm conceitos de produção/consumo associados a eles (wrt. rust, iterações). Por exemplo drain , collect , fold , all , take , ...
  2. esses predicados às vezes têm modificadores, por exemplo *_where , *_while
  3. você tem nomes de funções usando predicados que modificam propriedades ( map , filter , skip , ...)

    • aqui é vago se é modificação de elemento ou iteração ( map vs. filter / skip )

  4. nomes de funções encadeando vários predicados usando propriedades modificadoras, por exemplo filter_map

    • ter um conceito de aproximadamente apply modifier_1 and then apply modifier_2 , apenas que é mais rápido ou mais flexível fazer isso em uma etapa

Você às vezes pode ter:

  1. nomes de funções combinando predicados de produção/consumo com predicados de modificação (por exemplo drain_filter )

    • _mas_ na maioria das vezes é melhor/menos confuso combiná-los com modificadores (ex. drain_where )

Você normalmente não tem:

  1. dois dos predicados de produção/consumo combinados em um nome, ou seja, não temos pensamentos como take_collect pois é facilmente confuso

drain_retain meio que faz sentido, mas cai na última categoria, enquanto você provavelmente pode adivinhar o que faz, basicamente diz remove and return all elements "somehow specified" and then keep all elements "somehow specified" discarding other elements .


Por outro lado, não sei por que não deveria haver um retain_mut talvez abrindo uma RFC rápida apresentando retain_mut como uma maneira eficiente de combinar modify + retain Tenho um palpite que pode ser mais rápido
estabilizado então esta função. Até então, você poderia considerar escrever um traço de extensão fornecendo
você possui retain_mut usando iter_mut + um bool-array (ou bitarray, ou...) para acompanhar quais elementos
precisam ser removidos. Ou fornecer seu próprio drain_retain que usa internamente drain_filer / drain_where
mas envolve o predicado em um not |ele| !predicate(ele) .

@dathinab

  1. Estamos falando de um método em coleções aqui, não no Iterator. map, filter, filter_map, skip, take_while etc são todos métodos no Iterator. Aliás, quais métodos você quer dizer que usam *_where ?
    Portanto, temos que comparar o esquema de nomenclatura com métodos que já existem em coleções, por exemplo, retain() , drain() . Não há confusão com os métodos Iterator que transformam um iterador em outro iterador.
  2. AFAIK o consenso foi que retain_mut() não seria adicionado ao std porque drain_filter() já será adicionado e as pessoas foram aconselhadas a usar isso. O que nos traz de volta ao caso de uso de migrar de retain() para drain_filter() ser muito comum, então deve ter um nome e API semelhantes (fechamento retornando true significa manter a entrada )..

Eu não estava ciente de que retain_mut já foi discutido.

Estamos falando de esquemas gerais de nomenclatura em std principalmente wrt. para
coleções, que incluem Iteradores, pois são um dos principais
métodos de acesso para coleções em ferrugem, especialmente quando se trata de
modificar mais de uma entrada.

  • _where é apenas um exemplo de sufixos para expressar um
    função. Sufixos desse tipo que atualmente são usados ​​em std são principalmente
    _until , _while , _then , _else , _mut e _back .

A razão pela qual drain_retain é confuso é que não está claro se é
drenar ou reter com base, se for baseado em drenagem, retornar true removeria
o valor, se for baseado em retenção, manteria. Usar _where faz isso em
por último claro o que é esperado da função passada.

Em segunda-feira, 26 de fevereiro de 2018, 00:25 Boscop [email protected] escreveu:

@dathinab https://github.com/dathinab

  1. Estamos falando de um método em coleções aqui, não no Iterator.
    map, filter, filter_map, skip, take_while etc são todos métodos no Iterator.
    Aliás, quais métodos você quer dizer que usam *_where?
    Então temos que comparar o esquema de nomenclatura com métodos que já existem
    em coleções, por exemplo, reter(), drenar(). Não há confusão com
    Métodos de iterador que transformam o iterador em outro iterador.
  2. AFAIK o consenso era que reter_mut() não seria adicionado ao std
    porque o drain_filter() já será adicionado e as pessoas foram avisadas
    para usar isso. O que nos traz de volta ao caso de uso da migração de
    reter() para drain_filter() sendo muito comum, então deve ter um
    nome e API semelhantes (fechamento retornando true significa manter a entrada).


Você está recebendo isso porque foi mencionado.

Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368355110 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AHR0kfkRAZ5OtLFZ-SciAmjHDEXdgp-0ks5tYevegaJpZM4OY1me
.

Eu tenho usado muito essa função e sempre tenho que lembrar de retornar true para as entradas que quero drenar, o que parece contra-intuitivo em comparação com reter()/retain_mut().

FWIW, acho que retain é o nome contra-intuitivo aqui. Eu geralmente me pego querendo deletar certos elementos de um vetor, e com retain eu sempre tenho que inverter essa lógica.

Mas retain() já está estável, então temos que conviver com isso.. E então minha intuição se acostumou com isso..

@Boscop : e também drain que é o inverso de retain mas também retorna os elementos removidos e o uso de sufixos como _until , _while para fazer funções disponíveis que são apenas uma versão ligeiramente modificada de uma funcionalidade existente.

Quero dizer, se eu descrevesse o dreno, seria algo como:

_remover e retornar todos os elementos especificados "de alguma forma", manter todos os outros elementos_
onde _"de alguma forma"_ é _"por fatiar"_ para todos os tipos de coleção segmentáveis ​​e _"todos"_ para o resto.

A descrição da função discutida aqui é _a mesma_ exceto que
_"de alguma forma"_ é _" onde um determinado predicado retorna true"_.

Por outro lado, a descrição que eu daria para reter é:
_somente reter (ou seja, manter) elementos onde um determinado predicado retorna verdadeiro, descartar o resto_

(sim, reter poderia ter sido usado de uma forma que não descarta o resto, infelizmente não foi)


Eu acho que teria sido muito bom se retain
passou &mut T para o predicado e talvez retornou os valores removidos.
Porque eu acho que retain é uma base de nomes mais intuitiva.

Mas independente disso eu também acho que tanto drain_filter / drain_retain são abaixo do ideal
pois eles não deixam claro se o predicado deve retornar true/false para manter/drenar uma entrada.
(dreno indica que true o remove, pois fala sobre a remoção de elementos enquanto o filtro
e retreinar fala sobre quais elementos manter, finalmente em ferrugem)


No final, não é _tão_ importante qual dos nomes é usado, seria bom se estabilizasse.

Fazer uma enquete e/ou deixar que alguém da equipe de idiomas decida pode ser a melhor maneira de levar o pensamento adiante?

Acho que algo como drain_where , drain_if , ou drain_when , é muito mais claro do que drain_filter .

@tmccombs Desses 3, acho que drain_where faz mais sentido. (Porque if implica either do the whole thing (in this case draining) or not e when é temporal.)
Comparado com drain_filter o valor de retorno do closure é o mesmo com drain_where ( true para remover um elemento) mas esse fato fica mais claro/explícito pelo nome, então elimina o risco de interpretar erroneamente o significado do valor de retorno de fechamento.

Acho que está mais do que na hora de estabilizar. Resumo deste tópico:

  • Um parâmetro R: RangeArgument deve ser adicionado?
  • O valor booleano deve ser invertido? (Acho que a lógica atual faz sentido: retornar true do retorno de chamada faz com que esse item seja incluído no iterador.)
  • Nomeação. (Eu gosto de drain_where .)

@Gankro , o que você acha?

A equipe de libs discutiu isso e o consenso foi não estabilizar mais métodos do tipo drain no momento. (O método drain_filter existente pode permanecer no Nightly como instável.) https://github.com/rust-lang/rfcs/pull/2369 propõe adicionar outro iterador semelhante ao dreno que não faz nada quando descartado (em vez de consumir o iterador até o fim).

Gostaríamos de ver a experimentação para tentar generalizar em uma superfície de API menor várias combinações de drenagem:

  • Um sub-intervalo (através RangeArgument aka RangeBounds ) versus toda a coleção (embora o último possa ser alcançado passando .. , um valor do tipo RangeFull ).
  • Drenar tudo (possivelmente dentro desse intervalo) versus apenas elementos que correspondem a um predicado booleano
  • Auto-exaustivo na queda vs não (deixando o resto dos elementos na coleção).

As possibilidades podem incluir "sobrecarregar" um método tornando-o genérico ou um padrão de construtor.

Uma restrição é que o método drain é estável. Possivelmente, pode ser generalizado, mas apenas de maneira compatível com versões anteriores.

Gostaríamos de ver a experimentação para tentar generalizar em uma superfície de API menor várias combinações de drenagem:

Como e onde a equipe prevê que esse tipo de experimentação aconteça?

Como: crie e proponha um projeto de API concreto, possivelmente com uma implementação de prova de conceito (que pode ser feita fora da árvore por pelo menos Vec::as_mut_ptr e Vec::set_len ). Onde não importa muito. Pode ser um novo RFC ou um tópico em https://internals.rust-lang.org/c/libs e vincule-o a partir daqui.

Eu tenho brincado um pouco com isso. Vou abrir um tópico sobre os internos nos próximos dias.

Acho que uma API geral que funciona assim faz sentido:

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

Portanto, é uma API estilo construtor: se .where(pred) não for anexado, ele drenará todo o intervalo incondicionalmente.
Isso abrange os recursos do método .drain(a..b) atual, bem como .drain_filter(pred) .

Se o nome drain não puder ser usado porque já está em uso, deve ser um nome semelhante como drain_iter .

O método where não deve ser nomeado *_filter para evitar confusão com a filtragem do iterador resultante, especialmente quando where e filter são usados ​​em combinação assim:

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

Aqui, ele usará pred1 para decidir o que será drenado (e repassado no iterador) e pred2 será usado para filtrar o iterador resultante.
Quaisquer elementos que pred1 retorne true para mas pred2 retorne false para ainda serão drenados de v , mas não serão gerados por este iterador combinado.

O que você acha desse tipo de abordagem de API estilo construtor?

Por um segundo eu esqueci que where não pode ser usado como nome de função porque já é uma palavra-chave :/

E drain já está estabilizado então o nome também não pode ser usado..

Então eu acho que a segunda melhor opção geral é manter o atual drain e renomear drain_filter para drain_where , para evitar a confusão com .drain(..).filter() .

(Como jonhoo disse acima:)

qual foi o raciocínio por trás de nomear este filtro_dreno em vez de drenar_onde? Para mim, o primeiro implica que todo o Vec será drenado, mas que também executamos um filtro sobre os resultados (quando o vi pela primeira vez, pensei: "como isso não é apenas .drain(..).filter() ?"). O primeiro, por outro lado, indica que apenas drenamos elementos onde alguma condição se mantém.

Abri um tópico sobre internos .
O TLDR é que eu acho que não autoexaustão é uma lata de minhocas maior do que o esperado no caso geral e que devemos estabilizar drain_filter mais cedo ou mais tarde com um parâmetro RangeBounds . A menos que alguém tenha uma boa ideia para resolver os problemas descritos lá.

Edit: eu carreguei meu código experimental: experimentos de drenagem
Há também bancos de drenagem e limpeza e alguns testes, mas não espere um código limpo.

Totalmente perdido neste tópico. Eu tive um impl antigo que consertei um pouco e copiei e colei para refletir algumas das opções descritas neste tópico. A única coisa legal sobre o impl que eu acho que não será controverso é que ele implementa DoubleEndedIterator . Veja aqui .

@Emerentius , mas devemos pelo menos renomear drain_filter para drain_where , para indicar que o encerramento deve retornar true para remover o elemento!

@Boscop Ambos implicam a mesma 'polaridade' de true => rendimento. Eu pessoalmente não me importo se é chamado drain_filter ou drain_where .

@Popog Você pode resumir as diferenças e os prós e contras? Idealmente na rosca interna. Eu acho que a funcionalidade DoubleEndedIterator pode ser adicionada de forma compatível com zero ou baixa sobrecarga (mas eu não testei isso).

Que tal drain_or_retain ? É uma ação gramaticalmente significativa e sinaliza que faz uma ou outra.

@askeksa Mas isso não deixa claro se retornar true do fechamento significa "drenar" ou "reter".
Eu acho que com um nome como drain_where , é muito claro que retornar true o drena, e deve ficar claro para todos que os elementos que não são drenados são retidos.

Seria bom se houvesse alguma maneira de limitar/parar/cancelar/abortar o dreno. Por exemplo, se eu quisesse drenar os primeiros N números pares, seria bom poder fazer apenas vec.drain_filter(|x| *x % 2 == 0).take(N).collect() (ou alguma variante disso).

Como está implementado atualmente, o método $#$2$# DrainFilter drop sempre executará o dreno até a conclusão; não pode ser abortado (pelo menos não descobri nenhum truque que faria isso).

Se você deseja esse comportamento, deve apenas fechar sobre algum estado que rastreie quantos você viu e começar a retornar falso. A execução até a conclusão ao soltar é necessária para que os adaptadores se comportem razoavelmente.

Acabei de notar que a maneira drain_filter está atualmente implementada não é segura para relaxar, mas
na verdade, um wrt de risco de segurança. descontrair + retomar a segurança. Além disso, causa facilmente um aborto, tanto
dos quais são comportamentos que um método em std realmente não deveria ter. E enquanto escrevia isso eu noteique sua implementação atual não é segura

Eu sei que Vec por padrão não é seguro para relaxar, mas o comportamento de drain_filer quando o
pânicos de predicado é bem surpreendente porque:

  1. ele continuará chamando o fechamento que entrou em pânico quando cair
    se o fechamento entrar em pânico novamente, isso causará um a bordo e enquanto algumas pessoas
    como todos os panics para estar a bordo de outro trabalho com padrões de kernel de erro e para eles
    acabar com um a bordo é muito ruim
  2. se não continuar corretamente a drenagem potencialmente um valor
    e contendo um valor já descartado potencialmente levando ao uso após a liberação

Um exemplo desse comportamento está aqui:
play.rust-lang.org

Embora o 2. ponto deva ser solucionado, acho que o primeiro ponto em si deve
levar a uma reconsideração do comportamento de DrainFilter para correr até a conclusão
na queda, os motivos para alterar isso incluem:

  • iteradores são preguiçosos em ferrugem, executar um iterador ao soltar é um comportamento meio inesperado
    decorrente do que é normalmente esperado
  • o predicado passado para drain_filter pode entrar em pânico em algumas circunstâncias (por exemplo, um bloqueio
    foi envenenado) nesse caso, é provável que entre em pânico novamente quando chamado durante a liderança de queda
    para um pânico duplo e, portanto, a bordo, o que é muito ruim para quem usa kernel de erro
    padrões ou finalmente querendo desligar de forma controlada, tudo bem se você usar panic=aboard de qualquer maneira
  • se você tiver efeitos colaterais no predicado e não executar DrainFilter até a conclusão, poderá obter
    bugs surpreendentes quando ele é executado até a conclusão quando descartado (mas você pode ter feito
    outro pensa entre drená-lo até certo ponto e deixá-lo cair)
  • você não pode desativar esse comportamento sem modificar o predicado passado a ele, que você
    pode não ser capaz de fazer sem envolvê-lo, por outro lado, você sempre pode optar por executar
    até a conclusão apenas executando o iterador até a conclusão (sim, este último argumento é um pouco
    ondulado)

Os argumentos para executar até a conclusão incluem:

  • drain_filter é semelhante a ratain que é uma função, então as pessoas podem se surpreender quando
    "apenas" solte DrainFilter em vez de executá-lo até a conclusão

    • este argumento foi contestado muitas vezes em outras RFCs e é por isso que #[unused_must_use]

      existem, que em algumas situações já recomendam usar .for_each(drop) que ironicamente

      passa a ser o que DrainFilter faz na queda

  • drain_filter é frequentemente usado apenas por seu efeito colateral, por isso é detalhado

    • usá-lo dessa maneira o torna aproximadamente igual a retain



      • mas mantenha o uso &T , filter_filter usado &mut T



  • outras??
  • [EDIT, ADDED LATER, THX @tmccombs ]: não completar no drop pode ser muito confuso quando combinado com adaptadores como find , all , any que é um bom motivo manter o comportamento atual.

Pode ser apenas eu ou eu perdi algum ponto, mas mudar o comportamento Drop e
adicionar #[unused_must_use] parece ser preferível?

Se .for_each(drop) for muito longo, podemos considerar adicionar um RFC para iteradores destinados a
há efeito colateral adicionando um método como complete() ao iterador (ou bem drain() mas isso
é uma discussão completamente diferente)

outras??

Não consigo encontrar o raciocínio original, mas lembro que também havia algum problema com adaptadores trabalhando com um DrainFilter que não funcionava até a conclusão.

Veja também https://github.com/rust-lang/rust/issues/43244#issuecomment -394405057

Bom ponto, por exemplo, find faria com que o dreno fosse drenado apenas até ser o primeiro
match, similar all , any do curto circuito, o que pode ser bastante confuso
escrito ralo.

Hm, talvez eu devesse mudar minha opinião. Através disso pode ser um problema geral
com iteradores tendo efeitos colaterais e talvez devêssemos considerar uma solução geral
(independente deste problema de rastreamento) como um adaptador .allways_complete() .

Eu pessoalmente não encontrei nenhum motivo de segurança para que o dreno precise ser concluído, mas como escrevi aqui alguns posts acima, os efeitos colaterais em next() interagem de maneira subótima com adaptadores como take_while , peekable e skip_while .

Isso também traz os mesmos problemas que meu RFC no dreno não autoexaustor e seu adaptador iter autoexaustivo RFC .

É verdade que drain_filter pode facilmente causar abortos, mas você pode mostrar um exemplo de onde isso viola a segurança?

Sim, eu já fiz: play.rust-lang.org

Qual é 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 --

}

Mas isso é um pensamento interno de implementação, que deu errado.
Basicamente, a questão em aberto é como lidar com o panic de uma função de predicado:

  1. pule o elemento em que entrou em pânico, vaze e aumente o contador del

    • requer alguma forma de detecção de pânico

  2. não avance idx antes de chamar o predicado

    • mas isso significa que on drop irá chamá-lo novamente com o mesmo predicado

Outra questão é se é uma boa ideia executar funções que podem ser vistas como entrada do usuário da API ao soltar
em geral, mas essa é a única maneira de não fazer find , any , etc. se comportarem confusos.

Talvez uma consideração poderia ser algo como:

  1. defina um sinalizador ao inserir next , desmarque-o antes de retornar de next
  2. em queda, se a bandeira ainda estiver definida, sabemos que entramos em pânico e, portanto, vazamos
    os itens restantes OU elimine todos os itens restantes

    1. pode ser um grande vazamento com efeitos colaterais inesperados se você, por exemplo, vazar um Arc

    2. pode ser muito surpreendente se você tiver Arc e Weak's

Talvez haja uma solução melhor.
Seja qual for, deve ser documentado no rustdoc uma vez implementado.

@dathinab

Sim, eu já fiz

Vazamento é indesejável, mas bom e pode ser difícil de evitar aqui, mas uma gota dupla definitivamente não é. Boa pegada! Você gostaria de relatar um problema separado sobre esse problema de segurança?

drain_filter faz realocações toda vez que remove um item da coleção? Ou ele realoca apenas uma vez e funciona como std::remove e std::erase (em par) em C++? Eu preferiria esse comportamento por causa de exatamente uma alocação: simplesmente colocamos nossos elementos no final da coleção e, em seguida, removemos o tamanho adequado.

Além disso, por que não há try_drain_filter ? Qual retorna o tipo Option e None se pararmos? Tenho uma coleção muito grande e não faz sentido continuar para mim quando já tenho o que precisava.

A última vez que dei uma olhada no código, fez algo como: criou uma "lacuna"
ao retirar elementos e mover um elemento que não seja drenado para o
início da lacuna quando encontrar um. Com isso, cada elemento que deve ser
movido (seja para fora ou para um novo local na matriz) é movido apenas uma vez.
Também como, por exemplo remove não realoca. A parte liberada apenas se torna
parte da capacidade não utilizada.

Em sex, 10 de agosto de 2018, 07:11 Victor Polevoy [email protected] escreveu:

Drain_filter faz realocações toda vez que remove um item de
coleção? Ou ele faz realocação apenas uma vez e funciona como std::remove
e std::erase (em par) em C++? Eu preferiria tal comportamento por causa de
exatamente uma alocação: simplesmente colocamos nossos elementos no final da coleção
e, em seguida, remove reduzi-lo ao tamanho adequado.

Além disso, por que não há try_drain_filter ? Que retorna o tipo de opção e
Nenhum valor se devemos parar? Eu tenho uma coleção muito grande e é
sem sentido continuar para mim quando eu já tenho o que eu precisava.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-411977001 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AHR0kdOZm4bj6iR9Hj831Qh72d36BxQSks5uPRYNgaJpZM4OY1me
.

@rustonaut obrigado. Qual é a sua opinião sobre try_drain_filter ? :)

PS Acabei de olhar o código também, parece que funciona do jeito que queríamos.

Ele avança elemento por elemento durante a iteração, então normalmente você pode esperar
para parar de iterar ao ser descartado, mas foi considerado muito
confuso, então ele realmente drena até o fim quando descartado.
(O que aumenta drasticamente a provável capa de pânico duplo e outras coisas
Curtiu isso).

Portanto, é improvável que você obtenha uma versão de teste que se comporte como você
esperar.

Por justiça, dreno, parar cedo quando iterar pode ser confuso em
algumas situações, por exemplo thing.drain_where(|x| x.is_malformed()).any(|x| x.is_dangerus()) não drenaria todos os malformados, mas apenas até um dos
encontrado que também é perigoso. (O Impl. atual drena todos os
continuando a drenar em queda).

Em sex, 10 de agosto de 2018, 10:52 Victor Polevoy [email protected] escreveu:

@rustonaut https://github.com/rustonaut obrigado. Qual e sua OPINIAO
sobre try_drain_filter? :)


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-412020490 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AHR0kcEMHCayqvhI6D4LK4ITG2x5di-9ks5uPUnpgaJpZM4OY1me
.

Acho que seria mais versátil:

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

Oi, eu estava procurando a funcionalidade drain_filter para HashMap , mas ela não existe, e fui solicitado a abrir um problema quando encontrei este. Deveria ser em uma edição separada?

Existe algo atualmente bloqueando isso da estabilização? Ainda é descontrair-inseguro, conforme relatado acima?

Isso parece um recurso muito pequeno e está no limbo há mais de um ano.

Acho que seria mais versátil:

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

Eu não acho que isso seja melhor do que uma composição de drain_filter e map .

Ainda é descontrair-inseguro, conforme relatado acima?

Parece haver uma escolha difícil entre não drenar todos os elementos correspondentes se a iteração parar cedo ou um pânico potencial durante o desenrolar se a filtragem e a drenagem até o final forem feitas na queda de DrainFilter .
Eu acho que esse recurso está repleto de problemas de qualquer maneira e, como tal, não deve ser estabilizado.

Existe algum problema específico em fazer com que ele se comporte de maneira diferente no desenrolar?

Possibilidades:

  • Ele pode ser executado até a conclusão normalmente, mas deixar para trás os elementos correspondentes no desenrolamento (para que todos os elementos restantes sejam considerados como não correspondentes).
  • Ele pode ser executado até a conclusão normalmente, mas truncar o vetor após a última posição escrita no desenrolamento (de modo que todos os elementos restantes sejam considerados correspondentes).
  • Ele pode ser executado até a conclusão normalmente, mas truncar o vetor para o comprimento 0 no desenrolar.

O contra-argumento mais compreensível que consigo pensar é aquele código drop que depende da invariante normalmente fornecida por drain_filter (que, no final, os elementos no vec serão exatamente aqueles que falhou na condição) pode estar arbitrariamente distante do código (provavelmente código normal e seguro) que usa drain_filter .

No entanto, suponha que houvesse tal caso. Este código será bugado, não importa como o usuário o escreva. Por exemplo, se eles escrevem um loop imperativo que foi em reverso e elementos removidos por swap, então se sua condição pode entrar em pânico e seu drop impl depende muito da condição de filtro ser falsa, o código ainda tem um bug. Ter uma função como drop_filter cuja documentação pode chamar a atenção para este caso extremo parece uma melhoria em comparação.

Além disso, obrigado, encontrei este exemplo de playground postado anteriormente no tópico que demonstra que a implementação atual ainda descarta elementos duas vezes. (então definitivamente não pode ser estabilizado como está!)

Pode valer a pena abrir um problema separado para o bug de solidez? Isso pode então ser marcado como I-unsound.

Até onde eu sei, você não pode marcar ou tão doentio quanto pânico duplo _é som_
apenas muito inconveniente, pois aborta. Também pelo que me lembro do
possibilidade de pânico duplo não é um bug, mas o comportamento implicitamente, mas
conscientemente escolhido.

As opções são basicamente:

  1. Não corra até a conclusão na queda.
  2. Executar até a conclusão, mas potencialmente abortar devido ao pânico duplo
  3. Solte todos os elementos "desmarcados" ao soltar durante o pânico.
  4. Não complete na queda durante o pânico.

Os problemas são:

  1. => Comportamento inesperado em muitos casos de uso.
  2. => Aborto inesperado se o predicado entrar em pânico, especialmente se você o usar
    para "apenas" remover elementos, ou seja, você não usa o iterador retornado.
  3. => Diferença inesperada entre entrar e sair de um pânico. Somente
    considere alguém _usando_dreno_filtro em uma função drop.
  4. => Veja 3.

Ou com outras palavras 1. leva à confusão em casos de uso normais, 2. pode levar
abortar se o predicado puder entrar em pânico 3.,4. faça com que você não possa realmente
use-o em um método drop, mas como você agora uma função que você usa lá
não usa internamente.

Como resultado desta opção 3.,4. não vão. Os problemas com a opção 2. são
mais raros que os de 1. então 2. foi escolhido.

IMHO seria melhor ter uma API Drain+drain_filter que não roda
até a conclusão ao soltar + um combinador de iterador geral que é executado para
conclusão ao soltar + um método que completa um iterador, mas apenas descarta todos
Itens restantes. O problema é que o dreno já está estável, o iterador
combinador adiciona sobrecarga, pois precisa fundir o iterador interno e drenar
pode não ser o nome mais apropriado.

Em segunda-feira, 20 de maio de 2019, 09:28 Ralf Jung [email protected] escreveu:

Pode valer a pena abrir um problema separado para o bug de solidez? Que pode
então ser marcado como I-unsound.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/43244?email_source=notifications&email_token=AB2HJEL7FS6AA2A2KF5U2S3PWJHK7A5CNFSM4DTDLGPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVX5RWAissue#KTDN5WW2ZLOORPWSZGODVX5RWAissue#KTDN5WW2ZLOORPWSZGODVX5RWA
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AB2HJEMHQFRTCH6RCQQ64DTPWJHK7ANCNFSM4DTDLGPA
.

As gotas duplas não são sólidas.

Criado #60977 para o problema de solidez

Obrigado, me sinto estúpido por ler double drop como double panic :man_facepalming: .

  1. => Diferença inesperada entre entrar e sair de um pânico. Somente
    considere alguém _usando_dreno_filtro em uma função drop.

3.,4. faça com que você não possa realmente
use-o em um método drop, mas como você agora uma função que você usa lá
não usa internamente.

Isso para mim ainda não é grande coisa.

Se alguém usa drain_filter em um Drop impl com uma condição que pode entrar em pânico, o problema não é que eles escolheram usar drain_filter ; Da mesma forma, não importa se um método usa drain_filter internamente;

Desculpe, respondi muito cedo. Acho que entendi o que você quer dizer agora. Vou refletir sobre isso um pouco mais.

Certo, então seu argumento é que o código dentro de um impl drop que usa drain_filter pode quebrar misteriosamente se for executado durante o desenrolamento. (não se trata de drain_filter pânico, mas de outro código em pânico que faz com que drain_filter seja executado):

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 queda de repente se comportaria mal durante o desenrolamento.

Este é realmente um argumento convincente contra ter o DrainFilter detectando ingenuamente se o thread atual está em pânico.

A terminologia drain_filter é a que mais me atrai. Dado que já temos drain para remover todos os itens, selecionar quais itens remover seria um filter . Quando combinados, a nomeação parece muito consistente.

A correção para o problema de solidez do pânico duplo deixa o restante do Vec intacto no caso de um pânico no predicado. Os itens são deslocados para trás para preencher a lacuna, mas são deixados sozinhos (e descartados via Vec::drop durante o desenrolar ou manipulados pelo usuário se o pânico for detectado).

Descartar um vec::DrainFilter prematuramente continua a se comportar como se tivesse sido totalmente consumido (o que é o mesmo que vec::Drain ). Se o predicado entrar em pânico durante vec::Drain::drop , os itens restantes serão retrocedidos normalmente, mas nenhum outro item será descartado e o predicado não será chamado novamente. Essencialmente, ele se comporta da mesma forma, independentemente de um pânico no predicado ocorrer durante o consumo normal ou quando o vec::DrainFilter for descartado.

Supondo que a correção do furo de solidez esteja correta, o que mais está impedindo a estabilização desse recurso?

Vec::drain_filter pode ser estabilizado independentemente de LinkedList::drain_filter ?

O problema com a terminologia drain_filter , é que com drain_filter , existem realmente duas saídas: o valor de retorno e a coleção original, e não está muito claro de qual lado o "filtrado" itens continuam. Acho que até filtered_drain é um pouco mais claro.

não está muito claro de que lado os itens "filtrados" ficam

Vec::drain abre um precedente. Você especifica o intervalo dos itens que deseja _remover_. Vec::drain_filter funciona da mesma maneira. Você identifica os itens que deseja _remover_.

e não está muito claro de que lado os itens "filtrados" ficam

Eu sou da opinião de que isso já é verdade para Iterator::filter , então me resignei a ter que olhar para os documentos / escrever um teste sempre que uso isso. Eu não me importo o mesmo para drain_filter .


Eu gostaria que tivéssemos escolhido a terminologia select e reject de Ruby, mas esse navio já partiu há muito tempo.

Algum progresso nisso? O nome é a única coisa que ainda está no limbo?

Existe alguma coisa impedindo que isso seja estabilizado?

Parece que DrainFilter Drop vazará itens se algum de seus destruidores entrar em pânico no predicado . Esta é a causa raiz de https://github.com/rust-lang/rust/issues/52267. Temos certeza de que queremos estabilizar uma API como essa?

Não importa, isso foi corrigido por https://github.com/rust-lang/rust/pull/61224 aparentemente.

Vou cutucar um pouco esse problema de rastreamento também, adoraria ver esse recurso ficar estável :D Existem bloqueadores?

cc @Dylan-DPC

Alguma vez foi tomada uma decisão a favor ou contra que drain_filter tome um parâmetro RangeBounds , como drain ? Passar .. parece bastante fácil quando você deseja filtrar o vetor inteiro, então eu provavelmente seria a favor de adicioná-lo.

Acho que seria mais versátil:

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

Quase, a versão mais geral levaria um FnMut(T) -> Option<U> , como Iterator::{filter_map, find_map, map_while} . Não tenho ideia se vale a pena generalizar filter_map dessa maneira, mas pode valer a pena considerar.

Cheguei aqui porque estava procurando mais ou menos exatamente o método que o @jethrogb sugeriu acima:

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

A única diferença do que eu tinha em mente (que eu estava chamando update na minha cabeça) era que eu não tinha pensado em fazê-lo retornar um iterador de drenagem, mas isso parece uma melhoria clara, já que fornece uma interface única e razoavelmente direta que suporta a atualização de itens no local, a remoção de itens existentes e a entrega dos itens removidos ao chamador.

Quase, a versão mais geral levaria um FnMut(T) -> Option<U> , como Iterator::{filter_map, find_map, map_while} . Não tenho ideia se vale a pena generalizar filter_map dessa maneira, mas pode valer a pena considerar.

A função deve retornar Option<T> porque os valores que ela produz são armazenados em um Vec<T> .

@ johnw42 Não tenho certeza se sigo, o Some não seria imediatamente gerado pelo iterador?

Na verdade, acho que o valor de entrada dessa função ainda precisa ser &T ou &mut T em vez de T caso você não queira drená-lo. Ou talvez a função possa ser algo como FnMut(T) -> Result<U, T> . Mas não vejo por que o tipo de item não poderia ser outro tipo.

@timvermeulen Acho que estamos interpretando a proposta de forma diferente.

Do jeito que eu interpretei, o Some é armazenado de volta no Vec e None significa que o valor original é gerado pelo iterador. Isso permite que o encerramento atualize o valor no local ou o mova para fora do Vec . Ao escrever isso, percebi que minha versão realmente não adiciona nada porque você pode implementá-la em termos 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 outro lado, eu estava pensando que sua interpretação não é muito útil porque equivale a mapear o resultado de drain_filter , mas tentei escrevê-lo, e não é, pelo mesmo motivo que filter_map t equivalente a chamar filter seguido por map .

@johnw42 Ah, sim, pensei que você queria que None significasse que o valor deveria ficar no Vec .

Então parece que FnMut(T) -> Result<U, T> seria o mais geral, embora provavelmente não seja muito ergonômico. FnMut(&mut T) -> Option<U> não é realmente uma opção, porque isso não permitiria que você tomasse posse do T no caso geral. Acho que FnMut(T) -> Result<U, T> e FnMut(&mut T) -> bool são as únicas opções.

@timvermeulen Eu comecei a dizer algo mais cedo sobre uma solução "mais geral", e minha solução "mais geral" era diferente da sua, mas cheguei à mesma conclusão, que é que tentar tornar uma função muito geral resulta em algo que você realmente não gostaria de usar.

Embora talvez ainda haja algum valor em fazer um método muito geral sobre o qual usuários avançados possam construir abstrações mais agradáveis. Até onde eu sei, o ponto de drain e drain_filter não é que eles sejam APIs particularmente ergonômicas - eles não são - mas que eles suportam casos de uso que ocorrem em prática, e que não pode ser escrito de outra forma sem muitos movimentos redundantes (ou usando operações inseguras).

Com drain , você obtém as seguintes propriedades interessantes:

  • Qualquer seleção contígua de elementos pode ser removida.
  • Descartar os itens removidos é tão simples quanto descartar o iterador retornado.
  • Os itens removidos não precisam ser descartados; o chamador pode examinar cada um individualmente e escolher o que fazer com ele.
  • O conteúdo do Vec não precisa suportar Copy ou Clone .
  • Nenhuma memória para o Vec precisa ser alocada ou liberada.
  • Os valores restantes em Vec são movidos no máximo uma vez.

Com drain_filter , você ganha a capacidade de remover um conjunto arbitrário de itens do Vec em vez de apenas um intervalo contíguo. Uma vantagem menos óbvia é que, mesmo que um intervalo contíguo de itens seja removido, drain_filter ainda pode oferecer um aumento de desempenho se encontrar o intervalo para passar para drain envolver fazer uma passagem separada sobre o Vec para inspecionar seu conteúdo. Como o argumento para o encerramento é um &mut T , é possível até atualizar os itens restantes no Vec . Viva!

Aqui estão algumas outras coisas que você pode querer fazer com uma operação in-loco como drain_filter :

  1. Transforme os itens removidos antes de devolvê-los por meio do iterador.
  2. Interrompa a operação antecipadamente e relate um erro.
  3. Em vez de apenas remover o item ou deixá-lo no lugar (enquanto possivelmente o altera), adicione a capacidade de remover o item e substituí-lo por um novo valor (que pode ser um clone do valor original ou algo completamente diferente).
  4. Substitua o item removido por vários novos itens.
  5. Depois de fazer algo com o item atual, pule alguns itens a seguir, deixando-os no lugar.
  6. Depois de fazer algo com o item atual, remova alguns itens a seguir sem inspecioná-los primeiro.

E aqui está a minha análise de cada um:

  1. Isso não adiciona nada útil porque o chamador já pode transformar os itens conforme eles são retornados pelo iterador. Isso também anula o propósito do iterador, que é evitar a clonagem de valores entregando-os ao chamador somente depois que eles forem removidos do Vec .
  2. Ser capaz de abortar precocemente poderia melhorar a complexidade assintótica em alguns casos. Relatar um erro como parte da API não adiciona nada de novo porque você pode fazer a mesma coisa fazendo com que o encerramento altere uma variável capturada, e não está claro como fazer isso, porque o valor não será produzido até que o iterador tenha sido consumido.
  3. Isso, eu acho, adiciona alguma generalidade real.
  4. Isso é o que eu ia propor originalmente como a opção "pia da cozinha", mas decidi que não é útil, porque se um único item pode ser substituído por vários itens, é impossível manter a propriedade de que itens no Vec são movidos no máximo uma vez e pode ser necessário realocar o buffer. Se você precisa fazer algo assim, não é necessariamente mais eficiente do que apenas construir um novo Vec , e pode ser pior.
  5. Isso pode ser útil se o Vec estiver organizado de forma que você possa pular uma grande parte dos itens sem parar para inspecioná-los. Eu não o incluí no meu código de exemplo abaixo, mas ele pode ser suportado alterando o fechamento para retornar um usize adicional especificando quantos dos seguintes itens devem ser saltados antes de continuar.
  6. Isso parece complementar ao item 5, mas não é muito útil se você ainda precisar devolver os itens removidos por meio do iterador. Ainda pode ser uma otimização útil, no entanto, se os itens que você removeu não tiverem destruidor e você quiser apenas fazê-los desaparecer. Nesse caso, o usize acima pode ser substituído por Keep(usize) ou Drop(usize) (onde Keep(0) e Drop(0) são semanticamente equivalente).

Acho que podemos dar suporte aos casos de uso essenciais fazendo com que o encerramento retorne um enum com 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,
}

Uma última opção que gostaria de apresentar é livrar-se completamente do iterador, passar itens para o encerramento por valor e permitir que o chamador deixe um item inalterado substituindo-o por ele mesmo:

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

Eu gosto mais dessa abordagem porque é simples e suporta todos os mesmos casos de uso. A desvantagem potencial é que, mesmo que a maioria dos itens seja deixada no lugar, eles ainda precisam ser movidos para a estrutura de pilha do fechamento e, em seguida, movidos de volta quando o fechamento retornar. Seria de esperar que esses movimentos pudessem ser otimizados de forma confiável quando o encerramento apenas retornasse seu argumento, mas não tenho certeza se isso é algo com o qual devemos contar. Se outras pessoas gostarem o suficiente para incluí-lo, acho que update seria um bom nome para ele, porque se não me engano, ele pode ser usado para implementar qualquer atualização in-loco de passagem única de um conteúdo Vec .

(BTW, eu ignorei completamente as listas vinculadas acima porque esqueci completamente delas até ver o título desta edição. Se estamos falando de uma lista vinculada, ela altera a análise dos pontos 4-6, então acho que uma API seria apropriada para listas vinculadas.)

@johnw42 você já pode fazer 3. se tiver uma referência mutável, usando mem::replace ou mem::take .

@johnw42 @jplatte

(3) só faz sentido se permitirmos que o tipo de item do Iterator retornado seja diferente do tipo de item da coleção.
(3) é um caso especial, porque ambos retornam o elemento do Iterator e colocam um novo elemento de volta no Vec .

Bikeshedding: Eu meio que reverteria a função de Replace(T) e a substituiria por PushOut(T) , com o objetivo de "enviar" o valor interno de PushOut para o iterador, mantendo o item original (parâmetro) no Vec .

Stop provavelmente deve ter a capacidade de retornar um tipo Error (ou funcionar um pouco como try_fold ?).

Eu implementei minha função super_drain_by_value ontem à noite e aprendi várias coisas.

O item principal provavelmente deve ser que, pelo menos wrt Vec , tudo o que estamos falando aqui está na categoria "bom de ter" (em vez de adicionar um recurso fundamentalmente novo), porque Vec já basicamente fornece acesso direto de leitura e gravação a todos os seus campos por meio da API existente. Na versão estável, há uma pequena advertência de que você não pode observar o campo de ponteiro de um Vec vazio, mas o método instável into_raw_parts remove essa restrição. O que estamos realmente falando é expandir o conjunto de operações que podem ser executadas com eficiência por código seguro.

Em termos de geração de código, descobri que nos casos fáceis (por exemplo Vec<i32> ), movimentos redundantes dentro e fora do Vec não são um problema, e chamadas que equivalem a coisas simples como um no-op ou truncar o Vec são transformados em código muito simples para ser melhorado (zero e três instruções, respectivamente). A má notícia é que para casos mais difíceis, tanto minha proposta quanto o método drain_filter fazem muitas cópias desnecessárias, anulando amplamente o propósito dos métodos. Eu testei isso observando o código assembly gerado para um Vec<[u8; 1024]> e, em ambos os casos, cada iteração tem duas chamadas para memcpy que não são otimizadas. Mesmo uma chamada sem operação acaba copiando o buffer inteiro duas vezes!

Em termos de ergonomia, minha API, que parece muito boa à primeira vista, não é tão boa na prática; retornar um valor de enumeração do encerramento torna-se bastante detalhado em todos os casos, exceto os mais simples, e a variante que propus onde o encerramento retorna um par de valores de enumeração é ainda mais feia.

Eu também tentei estender DrainAction::Stop para carregar um R que é retornado de super_drain_by_value como Option<R> , e isso é ainda pior, porque no (presumivelmente típico) caso o valor retornado não seja necessário, o compilador não pode inferir R e você precisa anotar explicitamente o tipo de um valor que você nem está usando. Por esse motivo, não acho uma boa ideia oferecer suporte ao retorno de um valor do encerramento para o chamador de super_drain_by_value ; é aproximadamente análogo ao motivo pelo qual uma construção loop {} pode retornar um valor, mas qualquer outro tipo de loop é avaliado como () .

Em relação à generalidade, percebi que na verdade existem dois casos de rescisão prematura: um em que o restante do Vec é descartado e outro em que é deixado no lugar. Se o término prematuro não carrega um valor (como eu acho que não deveria), ele se torna semanticamente equivalente a retornar Keep(n) ou Drop(n) , onde n é o número de itens ainda não examinados. No entanto, acho que o término prematuro deve ser tratado como um caso separado, porque comparado ao uso de Keep / Drop , é mais fácil usar um caminho de código mais simples.

Para tornar a API um pouco mais amigável, acho que uma opção melhor seria fazer o encerramento retornar () e passar um objeto auxiliar (ao qual me referirei aqui como "atualizador") que pode ser usado para inspecionar cada elemento do Vec e controlar o que acontece com ele. Esses métodos podem ter nomes familiares como borrow , borrow_mut e take , com métodos extras como keep_next(n) ou drop_remainder() . Usando esse tipo de API, o fechamento é muito mais simples em casos simples e não mais complexo em casos complexos. Ao fazer com que a maioria dos métodos do atualizador receba self por valor, é fácil evitar que o chamador faça coisas como chamar take mais de uma vez ou dar instruções conflitantes sobre o que fazer nas iterações subsequentes.

Mas ainda podemos fazer melhor! Percebi esta manhã que, como tantas vezes acontece, este problema é análogo a um que foi definitivamente resolvido em linguagens funcionais, e podemos resolvê-lo com uma solução análoga. Estou falando de APIs "zipper", descritas pela primeira vez neste pequeno artigo com código de exemplo em OCaml e descritas aqui com código Haskell e links para outros artigos relevantes. Os zíperes fornecem uma maneira muito geral de percorrer uma estrutura de dados e atualizá-la "no local" usando quaisquer operações que essa estrutura de dados específica suporte. Outra maneira de pensar nisso é que um zíper é uma espécie de iterador turbinado com métodos extras para realizar operações em um tipo específico de estrutura de dados.

Em Haskell, você obtém a semântica "no lugar" fazendo do zíper uma mônada; em Rust, você pode fazer a mesma coisa usando tempos de vida fazendo com que o zíper mantenha uma referência mut para o Vec . Um zíper para um Vec é muito semelhante ao atualizador que descrevi acima, exceto que, em vez de passá-lo repetidamente para um fechamento, o Vec apenas fornece um método para criar um zíper em si mesmo e o zíper tem acesso exclusivo ao Vec enquanto ele existir. O chamador então se torna responsável por escrever um loop para percorrer a matriz, chamando um método em cada etapa para remover o item atual do Vec ou deixá-lo no lugar. A rescisão antecipada pode ser implementada chamando um método que consome o zíper. Como o loop está sob o controle do chamador, torna-se possível fazer coisas como processar mais de um item em cada iteração do loop ou manipular um número fixo de itens sem usar um loop.

Aqui está um exemplo muito artificial que mostra algumas das coisas que um zíper pode fazer:

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

Para comparação, aqui está mais ou menos a mesma função usando drain_filter , exceto que ela apenas finge parar mais cedo. É aproximadamente a mesma quantidade de código, mas IMHO é muito mais difícil de ler porque o significado do valor retornado pelo fechamento não é óbvio e está usando sinalizadores booleanos mutáveis ​​para transportar informações de uma iteração para a próxima, onde o zíper versão atinge a mesma coisa com o fluxo de controle. Como os itens removidos são sempre gerados pelo iterador, precisamos de uma etapa de filtro separada para remover valores negativos da saída, o que significa que precisamos verificar valores negativos em dois lugares em vez de um. Também é meio feio ter que acompanhar a posição em v ; a implementação de drain_filter tem essa informação, mas o chamador não tem acesso a ela.

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 Seu post anterior me lembrou do scanmut crate , especificamente o Remover struct , e o conceito de "zipper" que você mencionou parece muito semelhante! Isso parece muito mais ergonômico do que um método que fecha para quando você deseja controle total.

De qualquer forma, isso provavelmente não é muito relevante se drain_filter deve ser estabilizado, já que sempre podemos trocar os internos mais tarde. drain_filter si sempre será muito útil por ser conveniente. A única mudança que eu ainda gostaria de ver antes da estabilização é um parâmetro RangeBounds .

@timvermeulen Acho que faz sentido adicionar um parâmetro RangeBounds , mas manter a assinatura de fechamento atual ( F: FnMut(&mut T) -> bool ).
Você sempre pode pós-processar os elementos drenados com filter_map ou o que quiser.
(Para mim, é muito importante que o closure permita a mutação do elemento, porque retain não permite (ele foi estabilizado antes que esse erro fosse descoberto).)

Sim, esse parece ser o equilíbrio perfeito entre conveniência e utilidade.

@timvermeulen Sim, eu estava me afastando um pouco do tópico principal.

Uma coisa que notei que é relevante para o tópico original é que é meio difícil lembrar o que o valor de retorno do fechamento significa - ele está dizendo se deve manter o item ou removê-lo? Eu acho que seria útil para os documentos apontarem que v.drain_filter(p) é equivalente a v.iter().filter(p) com efeitos colaterais.

Com filter , usar um valor booleano ainda é menos que o ideal para clareza, mas é uma função muito conhecida, e IMHO é pelo menos um pouco intuitivo que o predicado responda à pergunta "devo manter isso?" em vez de "devo descartar isso?" Com drain_filter , a mesma lógica se aplica se você pensar sobre isso da perspectiva do iterador, mas se você pensar sobre isso da perspectiva da entrada Vec , a pergunta se torna "I NOT I NOT Mantenha isso?"

Quanto ao texto exato, proponho renomear o parâmetro filter para predicate (para corresponder a Iterator::filter ) e adicionar esta frase em algum lugar na descrição:

Para lembrar como o valor de retorno de predicate é usado, pode ser útil ter em mente que drain_filter é idêntico a Iterator::filter com o efeito colateral adicional de remover o valor selecionado itens de self .

@ johnw42 Sim, bom ponto. Acho que um nome como drain_where seria muito mais claro.

Se você vai entrar em nomenclatura bikeshedding; por favor, certifique-se de ter lido todos os comentários; mesmo os ocultos. Muitas variantes já foram propostas, por exemplo https://github.com/rust-lang/rust/issues/43244#issuecomment -331559537

Mas… tem que se chamar draintain() ! Nenhum outro nome é tão bonito!

Estou bastante interessado nesta questão e li todo o tópico, então posso tentar resumir o que todos disseram, na esperança de ajudar a estabilizar. Adicionei alguns dos meus próprios comentários ao longo do caminho, mas tentei mantê-los o mais neutros possível.

Nomeação

Aqui está um resumo não opinativo dos nomes que vi propostos:

  • drain_filter : O nome usado na implementação atual. Consistente com outros nomes como filter_map . Tem a vantagem de ser análogo a drain().filter() , mas com mais efeitos colaterais.
  • drain_where : Tem o benefício de indicar se true resulta em drenar _out_ ou filtrar _in_, o que pode ser difícil de lembrar com outros nomes. Não há precedente em std para o sufixo _where , mas há muitos precedentes para sufixos semelhantes.
  • Uma variação de drain().where() , já que where já é uma palavra-chave.
  • drain_retain : Consistente com retain , mas retain e drain têm interpretações opostas dos valores booleanos retornados pelo fechamento, o que pode ser confuso.
  • filtered_drain
  • drain_if
  • drain_when
  • remove_if

Parâmetros

Pode valer a pena adicionar um argumento de intervalo para consistência com drain .

Dois formatos de fechamento foram sugeridos, FnMut(&mut T) -> bool e FnMut(T) -> Result<T, U> . Este último é mais flexível, mas também mais desajeitado.

Reverter a condição booleana ( true significa "manter o Vec ") para ser consistente com retain foi discutido, mas não seria consistente com drain ( true significa "drenar do Vec ").

Desenrolar

Quando o fechamento do filtro entra em pânico, o iterador DrainFilter é descartado. O iterador então deve terminar de drenar o Vec , mas para isso ele deve chamar o fechamento do filtro novamente, arriscando um pânico duplo. Existem algumas soluções, mas todas são compromissos:

  • Não termine de drenar na queda. Isso é bastante contra-intuitivo quando usado com adaptadores como find ou all . Além disso, torna o idioma v.drain_filter(...); inútil, pois os iteradores são preguiçosos.

  • Sempre termine de drenar ao cair. Isso arrisca pânicos duplos (que resultam em abortos), mas torna o comportamento consistente.

  • Só termine de drenar na queda se não estiver desenrolando no momento. Isso corrige totalmente os pânicos duplos, mas torna o comportamento de drain_filter imprevisível: colocar DrainFilter em um destruidor pode _às vezes_ não fazer seu trabalho.

  • Só termine de drenar em queda se o fechamento do filtro não entrar em pânico. Este é o compromisso atual feito por drain_filter . Uma boa propriedade dessa abordagem é que entra em pânico no fechamento do filtro "curto-circuito", o que sem dúvida é bastante intuitivo.

Observe que a implementação atual é sólida e nunca vaza, desde que a estrutura DrainFilter seja descartada (embora possa causar uma interrupção). As implementações anteriores não eram seguras/livres de vazamentos.

Dreno em gota

DrainIter pode terminar de drenar o vetor de origem quando ele for descartado ou só pode drenar quando next for chamado (iteração lenta).

Argumentos a favor da drenagem na gota:

  • Consistente com o comportamento de drain .

  • Interage bem com outros adaptadores como all , any , find , etc...

  • Habilita o idioma vec.drain_filter(...); .

  • A funcionalidade preguiçosa pode ser habilitada explicitamente por meio de métodos de estilo drain_lazy ou um adaptador lazy() em DrainIter (e até mesmo em Drain , já que é compatível com versões anteriores para adicionar métodos).

Argumentos a favor da iteração preguiçosa:

  • Consistente com quase todos os outros iteradores.

  • A funcionalidade "drain-on-drop" pode ser habilitada explicitamente por meio de adaptadores em DrainIter , ou mesmo por meio de um adaptador geral Iterator::exhausting (consulte RFC #2370 ).

Eu posso ter perdido algumas coisas, mas pelo menos espero que ajude os recém-chegados ao percorrer o tópico.

@negamartin

A opção drain-on-drop não exigiria que o iterador retornasse uma referência ao item em vez do valor de propriedade? Acho que isso tornaria impossível usar drain_filter como um mecanismo para remover e apropriar-se de itens que correspondam a uma condição específica (que era meu caso de uso original).

Eu acho que não, já que o comportamento da implementação atual é precisamente drenado enquanto produz valores próprios. De qualquer forma, não vejo como a drenagem na gota exigiria o empréstimo de elementos, então acho que temos duas ideias diferentes sobre o que significa a drenagem na gota.

Só para ficar claro, quando digo drenar na gota, quero dizer apenas o comportamento quando o iterador não é totalmente consumido: todos os itens correspondentes ao fechamento devem ser drenados mesmo que o iterador não seja totalmente consumido? Ou apenas até o elemento que foi consumido, deixando o resto intocado?

Em particular, é a diferença 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]);

Apenas lançando uma ideia, mas outra API possível poderia ser algo como:

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

É menos flexível que as outras opções, mas evita o problema do que fazer quando DrainFilter é descartado.

Parece-me que tudo isso está me parecendo cada vez menos com retain_mut() ( retain() com uma referência mutável passada para o fechamento), que é o que se pretendia, em primeiro lugar, fornecer . Podemos fornecer retain_mut() por enquanto, além de trabalhar no projeto do dreno filtrado? Ou eu estou esquecendo de alguma coisa?

@BartMassey

que é o que se destinava em primeiro lugar a fornecer.

Eu não acho que seja o caso. Eu uso especificamente drain_filter para me apropriar de itens com base nos critérios de filtro. Drain e DrainFilter rendem o item onde a retenção não.

@negamartin

Só para ficar claro, quando digo drenar na gota, quero dizer apenas o comportamento quando o iterador não é totalmente consumido

Ah ok. Esse é o meu erro. Não entendi sua definição. Eu interpretei como "nada é removido do vec até cair", o que realmente não faz sentido.

Argumentos a favor da iteração preguiçosa

Acho que precisa ser consistente com drain . A RFC Iterator::exhausting não foi aceita e seria muito estranho se drain e drain_filter tivessem comportamentos de drenagem aparentemente opostos.

@negamartin

drain_filter: O nome usado na implementação atual. Consistente com outros nomes como filter_map. Tem a vantagem de ser análogo a drain().filter() , mas com mais efeitos colaterais.

Não é análogo (é por isso que precisamos retain_mut / drain_filter ):
drain().filter() drenaria até mesmo aqueles elementos para os quais o fechamento do filtro retorna false !

Acabei de notar uma pequena linha em um comentário da equipe lib em #RFC 2870 :

As possibilidades podem incluir "sobrecarregar" um método tornando-o genérico ou um padrão de construtor.

É compatível com versões anteriores tornar um método genérico se ele ainda aceitar o tipo concreto anterior? Se assim for, acredito que seria o melhor caminho a seguir.

(O padrão do construtor é um pouco não intuitivo com iteradores, pois os métodos nos iteradores geralmente são adaptadores, não modificadores de comportamento. Além disso, não há precedentes, por exemplo chunks e chunks_exact são dois métodos separados , não um combo chunks().exact() .)

Não, não com o design atual até onde eu sei como inferência de tipo que
funcionou antes agora pode falhar devido à ambiguidade de tipo. Genética com padrão
tipos para funções ajudariam, mas são super complicados de fazer certo.

Em sex, 12 de junho de 2020, 21:21 negamartin [email protected] escreveu:

Acabei de notar uma pequena linha em um comentário da equipe lib em #RFC 2870
https://github.com/rust-lang/rfcs/pull/2369 :

As possibilidades podem incluir "sobrecarregar" um método tornando-o genérico,
ou um padrão de construtor.

É compatível com versões anteriores tornar um método genérico se ele ainda aceitar
o tipo de concreto anterior? Se sim, acredito que seria a melhor forma
frente.

(O padrão construtor é um pouco não intuitivo com iteradores, uma vez que os métodos em
iteradores são geralmente adaptadores, não modificadores de comportamento. Além disso, existem
sem precedentes, por exemplo, chunks e chunks_exact são dois separados
métodos, não um combo chunks().exact().)


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-643444213 ,
ou cancelar
https://github.com/notifications/unsubscribe-auth/AB2HJELPWXNXJMX2ZDA6F63RWJ53FANCNFSM4DTDLGPA
.

É compatível com versões anteriores tornar um método genérico se ele ainda aceitar o tipo concreto anterior? Se assim for, acredito que seria o melhor caminho a seguir.

Não, pois quebra a inferência de tipo em alguns casos. Por exemplo foo.method(bar.into()) funcionará com um argumento de tipo concreto, mas não com um argumento genérico.

Acho que o drain_filter, conforme implementado agora, é muito útil. Poderia ser estabilizado como está? Se melhores abstrações forem descobertas no futuro, nada impede que elas sejam introduzidas também.

Que processo eu preciso iniciar para tentar adicionar retain_mut() , independente do que aconteça com drain_filter() ? Parece-me que os requisitos divergiram e que retain_mut() ainda seria útil ter, independentemente do que acontece com drain_filter() .

@BartMassey para novas APIs de bibliotecas instáveis, acho que fazer um PR com uma implementação deve ser bom. Há instruções em https://rustc-dev-guide.rust-lang.org/implementing_new_features.html para implementar o recurso e em https://rustc-dev-guide.rust-lang.org/getting-started.html #building -and-testing-stdcorealloctestproc_macroetc para testar suas alterações.

Eu tenho lutado com as diferenças de API entre HashMap e BTreeMap hoje e eu só queria compartilhar uma palavra de cautela que acho importante que várias coleções se esforcem para manter uma API coerente sempre que for sentido, algo que neste momento nem sempre é o caso.

Por exemplo, String, Vec, HashMap, HashSet, BinaryHeap e VecDeque têm um método retain , mas LinkedList e BTreeMap não. Acho especialmente estranho, pois retain parece um método mais natural para um LinkedList ou um Map do que para vetores em que a exclusão aleatória é uma operação muito cara.

E quando você cava um pouco mais fundo, é ainda mais desconcertante: o encerramento HashMap::retain recebe o valor em uma referência mutável, mas as outras coleções obtêm uma referência imutável (e String recebe um simples char ).

Agora vejo que novas APIs como drain_filter estão sendo adicionadas que 1/ parecem se sobrepor a retain e 2/ não estão estabilizadas para todas as coleções ao mesmo tempo:

  • HashMap::drain_filter está no repositório upstream, mas ainda não foi enviado com o std AFAIK de Rust (não aparece nos documentos)
  • BTreeMap::drain_filter , Vec::drain_filter , LinkedList::drain_filter estão no padrão de Rust, mas são bloqueados
  • VecDeque::drain_filter não parece existir, não aparece nos documentos
  • String::drain_filter também não existe

Não tenho uma opinião forte sobre a melhor maneira de implementar esses recursos ou se precisamos drain_filter , retain ou ambos, mas acredito fortemente que essas APIs devem permanecer consistentes em todas as coleções .

E talvez mais importante, métodos semelhantes de coleções diferentes devem ter a mesma semântica. Algo que as implementações atuais de retain violam a IMO.

Esta página foi útil?
0 / 5 - 0 avaliações