Rust: Проблема отслеживания для Vec::drain_filter и LinkedList::drain_filter

Созданный на 15 июл. 2017  ·  119Комментарии  ·  Источник: 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,
    { ... }

Я уверен, что где-то есть проблема с этим, но я не могу ее найти. Какой-то ботаник подтолкнул меня к реализации. Приходит пиар.

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

Самый полезный комментарий

Есть ли что-то, что мешает этому стабилизироваться?

Все 119 Комментарий

Возможно, сюда не нужно включать кухонную раковину, но она _может_ иметь параметр диапазона, так что это похоже на надмножество стока. Есть ли в этом недостатки? Я предполагаю, что добавление проверки границ диапазона является недостатком, это еще одна вещь, которая может вызвать панику. А вот stack_filter(.., f) не может.

Есть ли шанс, что это стабилизируется в той или иной форме в ближайшем будущем?

Если компилятор достаточно умен, чтобы исключить проверки границ
в случае drain_filter(.., f) я бы выбрал это.

(И я почти уверен, что вы можете реализовать это таким образом,
что делает компилятор достаточно умным, в худшем случае
случае, если у вас может быть «специализация по функциям»,
в основном что-то вроде if Type::of::<R>() == Type::of::<RangeFull>() { dont;do;type;checks; return } )

Я знаю, что это в какой-то степени байкшеринг, но что послужило причиной назвать это drain_filter , а не drain_where ? Для меня первое означает, что весь Vec будет слит, но что мы также проводим filter по результатам (когда я впервые увидел это, я подумал: «как это не просто .drain(..).filter() ?"). Первый, с другой стороны, указывает на то, что мы сливаем только те элементы, для которых выполняется какое-либо условие.

Понятия не имею, но drain_where звучит намного лучше и интуитивно понятнее.
Есть ли еще шанс его изменить?

.remove_if также было предложено ранее

Я думаю, что drain_where объясняет это лучше всего. Как и дренаж, он возвращает значения, но не сливает/удаляет все значения, а только те, где заданное условие истинно.

remove_if звучит очень похоже на условную версию remove , которая просто удаляет один элемент по индексу, если условие истинно, например, letters.remove_if(3, |n| n < 10); удаляет букву в индексе 3, если он < 10 .

drain_filter , с другой стороны, немного неоднозначен, делает ли это drain , а затем filter более оптимизированным способом (например, filter_map) или если слить, чтобы итератор возвращался, сравнимый с итератор filter вернется,
и если да, то не следует ли его называть filtered_drain , поскольку фильтр логически используется раньше...

В стандартной библиотеке нет прецедентов использования _where или _if .

@Gankro есть ли где-нибудь прецедент использования _filter ? Я также не знаю, является ли это причиной для того, чтобы не использовать менее двусмысленную терминологию? В других местах стандартной библиотеки уже используются различные суффиксы, такие как _until и _while .

«Упомянутый эквивалентный» код в комментарии неверен ... вы должны вычесть один из i на сайте «ваш код здесь», иначе произойдет что-то плохое.

ИМО, проблема не в filter . Только что искавший это (и будучи новичком), drain кажется довольно нестандартным по сравнению с другими языками.

Опять же, только с точки зрения новичка, вещи, которые я бы искал, пытаясь найти что-то, что предлагает эта проблема, были бы delete (как в delete_if ), remove , filter или reject .

На самом деле я _искал_ filter , увидел drain_filter и _продолжал поиск_, не читая, потому что drain не представлял собой того простого, что я хотел сделать.

Кажется, что простая функция с именем filter или reject была бы гораздо более интуитивной.

В отдельной заметке я не чувствую, что это должно мутировать вектор, к которому он обращается. Это предотвращает цепочку. В идеальном сценарии хотелось бы иметь возможность сделать что-то вроде:

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

С текущей реализацией то, к чему он будет присоединяться, будет отклоненными значениями.

Я хотел бы видеть как accept , так и reject . Ни один из них не изменяет исходное значение.

Вы уже можете сделать цепочку только с filter . Весь смысл drain_filter состоит в том, чтобы изменить вектор.

@rpjohnst , поэтому я искал здесь , я где-то пропустил filter ?

Да, это член Iterator , а не Vec .

Drain — новая терминология, потому что она представляет собой четвертый вид владения в Rust, который применяется только к контейнерам, а также обычно является бессмысленным различием почти в любом другом языке (при отсутствии семантики перемещения нет необходимости объединять итерацию и удаление в одна «атомарная» операция).

Хотя дренаж_фильтр перемещает терминологию стока в пространство, о котором позаботятся другие языки (поскольку избегание обратного сдвига актуально для всех языков).

Я наткнулся на drain_filter в документах как результат Google для rust consume vec . Я знаю, что из-за неизменяемости по умолчанию в ржавчине фильтр не потребляет данные, просто не мог вспомнить, как к этому подойти, чтобы лучше управлять памятью.

drain_where — это хорошо, но пока пользователь знает, что делают drain и filter , я думаю, понятно, что метод истощает данные на основе предикатного фильтра.

Я все еще чувствую, что drain_filter означает, что он осушает (то есть опорожняет), а затем фильтрует. drain_where другой стороны,

Разве linked_list::DrainFilter не должен также реализовывать Drop , чтобы удалить все оставшиеся элементы, соответствующие предикату?

да

Почему именно удаление итератора заставляет его работать до конца? Я думаю, что это удивительное поведение для итератора, и при желании это можно было бы сделать явно. С другой стороны, обратное удаление только того количества элементов, которое вам нужно, невозможно, потому что mem::forget ing the iterator приводит к усилению утечки.

Я часто использую эту функцию, и мне всегда нужно помнить о возврате true для записей, которые я хочу слить, что кажется нелогичным по сравнению с retain() / retain_mut() .
На интуитивном логическом уровне было бы разумнее вернуть true для записей, которые я хочу сохранить, кто-нибудь еще так считает? (Особенно учитывая, что retain() уже так работает)
Почему бы не сделать это и не переименовать drain_filter() в retain_iter() или retain_drain() (или drain_retain() )?
Тогда это также будет более точно отражать retain() !

Вот почему я предложил переименовать его в
drain_where как тогда понятно что:

  1. Это форма стока, поэтому мы используем сток в названии.

  2. Используя where в сочетании с drain , становится ясно, что
    элементы _где_ предикат истинен, сливаются, т.е. удаляются и возвращаются

  3. Это больше синхронизируется с другими именами в std, обычно, если у вас есть
    функция, состоящая из двух предикатов, которую вы можете эмулировать (примерно), используя
    функции, представляющие каждый из предикатов в виде «и затем», например
    filter_map можно эмулировать (примерно) как filter an then map . Электрический ток
    имя указывает, что это drain and then filter , но это даже не близко к нему
    так как он вообще не делает полный сток.

Вс, 25 февраля 2018 г., 17:04 [email protected] написал:

Я часто использую эту функцию, и мне всегда нужно помнить
вернуть true для записей, которые я хочу слить, что кажется
нелогично по сравнению с continue_mut().
На начальном уровне было бы разумнее возвращать true для записей, которые я
хочу сохранить, кто-нибудь еще так считает?
Почему бы не сделать это и не переименовать сток_фильтр() в сохранить_фильтр()?


Вы получаете это, потому что подписаны на эту тему.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368320990 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AHR0kfwaNvz8YBwCE4BxDkeHgGxLvcWxks5tYYRxgaJpZM4OY1me
.

Но с drain_where() замыкание все равно должно возвращать true для элементов, которые должны быть удалены, что противоположно retain() , что делает его несогласованным.
Может retain_where ?
Но я думаю, что вы правы в том, что имеет смысл иметь «слив» в названии, поэтому я думаю, что drain_retain() имеет наибольший смысл: это похоже на drain() , но с сохранением элементов, из которых возвращается замыкание. true .

Хотя это не меняется, то, что вы должны вернуть true, ясно дает понять, что
вы должны сделать это.

Лично я бы использовал либо:

а. drain_where
б. retain_where
в. retain

Я бы не стал использовать drain_retain .
Слить и сохранить говорят об одном и том же процессе, но с противоположной стороны.
перспективы, слив говорит о том, что вы удаляете (и возвращаете) сохраняете
говорит о том, что вы храните (так, как это уже используется в std).

В настоящее время retaim уже реализовано в std с большим отличием
что он отбрасывает элементы, в то время как drain возвращает их, что делает
retain (к сожалению) не подходит для использования в имени (кроме случаев, когда вы хотите вызвать
это retain_and_return или подобное).

Другим моментом, говорящим в пользу утечки, является легкость миграции, т.е.
до drain_where так же просто, как запустить поиск по слову и заменить на
код, при изменении его для сохранения потребуется дополнительное отрицание
все используемые функции предикатов/фильтров.

Вс, 25 февраля 2018 г., 18:01 [email protected] написал:

Но с использованием функции drink_where() замыкание все равно должно возвращать значение true для
элементы, которые должны быть удалены, что является противоположностью функции сохранения(), которая
делает несовместимым..
Может быть, keep_where?
Но я думаю, что вы правы в том, что имеет смысл иметь "слив" в названии,
так что я думаю, что функция drop_retain() имеет смысл: она похожа на функцию дренажа(), но
сохраняя элементы, где замыкание возвращает true.


Вы получаете это, потому что подписаны на эту тему.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368325374 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AHR0kfG4oZHxGfpOSK8DjXW3_2O1Eo3Rks5tYZHxgaJpZM4OY1me
.

Но как часто вы будете переходить с drain() на drain_filter() ?
Во всех случаях до сих пор я мигрировал с retain() на drain_filter() , потому что в std нет retain_mut() и мне нужно изменить элемент! Итак, мне пришлось инвертировать возвращаемое значение закрытия.
Я думаю, что drain_retain() имеет смысл, потому что метод drain() безоговорочно удаляет все элементы в диапазоне, тогда как drain_retain() сохраняет элементы, где замыкание возвращает true , он объединяет эффекты методов drain() и retain() .

Извините, я имею в виду переход с drain_filter на drain_where .

Что у вас есть решение с использованием retain , а затем вам нужно использовать
drain_filter — это аспекты, которые я еще не рассматривал.

Вс, 25 февраля 2018 г., 19:12 [email protected] написал:

Но зачем вам переходить с дренажа() на дренаж_фильтр()?
Во всех случаях до сих пор я перешел с функции сохранения() на функцию дренажа_фильтра().
потому что в стандартном стандарте нет функции continue_mut(), и мне нужно изменить элемент!
Я думаю, что использование функции drink_retain() имеет смысл, потому что метод
безоговорочно все элементы в диапазоне, в то время как дренаж_сохраняет () сохраняет
элементы, где замыкание возвращает true.


Вы получаете это, потому что подписаны на эту тему.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368330896 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AHR0kSayIk_fbp5M0RsZW5pYs3hDICQIks5tYaJ0gaJpZM4OY1me
.

Ах да, но я думаю, что «цена» инвертирования замыканий в текущем коде, использующем drain_filter() , того стоит, чтобы получить согласованный и интуитивно понятный API в стандартном, а затем в стабильном.
Это всего лишь небольшая фиксированная стоимость (и облегченная тем фактом, что она будет сопровождаться переименованием функции, поэтому ошибка компилятора может сказать пользователю, что закрытие должно быть инвертировано, поэтому это не приведет к ошибке) , по сравнению со стоимостью стандартизации drain_filter() , а затем людям всегда приходится инвертировать замыкание при переходе с retain() на drain_filter() .. (помимо умственных затрат на запоминание сделать это, а также затраты на то, чтобы его было сложнее найти в документах, исходя из retain() и поиска «чего-то вроде retain() , но с передачей &mut в его закрытие, что вот почему я думаю, что в новом названии этой функции есть смысл «сохранить» в названии, чтобы люди могли найти ее при поиске в документах).

Некоторые анекдотические данные: в моем коде мне всегда нужен был только аспект retain_mut() drain_filter() (раньше они часто использовали retain() ), у меня никогда не было случаев использования, когда мне нужно было обработать слитые значения. Я думаю, что это также будет наиболее распространенным вариантом использования для других в будущем (поскольку retain() не передает &mut до закрытия, поэтому drain_filter() должен покрывать этот вариант использования , и это более распространенный вариант использования, чем необходимость обработки выгруженных значений).

Причина, по которой я против drain_retain , заключается в том, что имена в настоящее время используются в std wrt. коллекции:

  1. у вас есть имена функций, использующие предикаты, с которыми связаны концепции производства/потребления (относительно ржавчины, итераций). Например, drain , collect , fold , all , take , ...
  2. эти предикаты иногда имеют модификаторы, например, *_where , *_while
  3. у вас есть имена функций, использующие предикаты, которые имеют модифицирующие свойства ( map , filter , skip , ...)

    • здесь неясно, изменяется ли элемент или итерация ( map против filter / skip )

  4. имена функций, объединяющие несколько предикатов в цепочку с использованием модифицирующих свойств, например, filter_map

    • имея концепцию примерно apply modifier_1 and then apply modifier_2 , просто быстрее или гибче делать это за один шаг

Иногда у вас может быть:

  1. имена функций, объединяющие производящие/потребляющие предикаты с модифицирующими (например, drain_filter )

    • _но_ в большинстве случаев лучше/менее запутанно комбинировать их с модификаторами (например, drain_where )

Обычно у вас нет:

  1. два производящих/потребляющих предиката объединены в одно имя, т.е. у нас нет таких мыслей, как take_collect , так как это легко запутать

drain_retain действительно имеет смысл, но попадает в последнюю категорию, хотя вы, вероятно, можете догадаться, что он делает, в основном это remove and return all elements "somehow specified" and then keep all elements "somehow specified" discarding other elements .


С другой стороны, я не знаю, почему не должно быть retain_mut , возможно, открытие быстрого RFC, представляющего retain_mut как эффективный способ объединить modify + retain У меня есть подозрение, что это может быть быстрее
стабилизировалась потом эта функция. До тех пор вы могли бы подумать о написании трейта расширения, обеспечивающего
у вас есть retain_mut , используя iter_mut + логический массив (или битовый массив, или...), чтобы отслеживать, какие элементы
должны быть удалены. Или предоставьте свой собственный drain_retain , который внутренне использует drain_filer / drain_where
но оборачивает предикат в not |ele| !predicate(ele) .

@датинаб

  1. Здесь мы говорим о методе для коллекций, а не для Iterator. map, filter, filter_map, skip, take_while и т. д. — все это методы Iterator. Кстати, какие методы вы имеете в виду, которые используют *_where ?
    Таким образом, мы должны сравнить схему именования с методами, которые уже существуют в коллекциях, например, retain() , drain() . Нет никакой путаницы с методами Iterator, которые преобразуют один итератор в другой итератор.
  2. Насколько мне известно, консенсус состоял в том, что retain_mut() не будет добавлено в std, потому что drain_filter() уже будет добавлено, и людям было рекомендовано использовать это. Это возвращает нас к варианту использования перехода с retain() на drain_filter() , который очень распространен, поэтому он должен иметь похожее имя и API (закрытие, возвращающее true , означает сохранение записи )..

Я не знал, что retain_mut уже обсуждалось.

Речь идёт об общих схемах именования в std в основном по отношению. к
коллекций, которые включают в себя итераторы, поскольку они являются одним из основных
методы доступа к коллекциям в rust, особенно когда речь идет о
изменение более чем одной записи.

  • _where — это просто пример суффиксов для выражения слегка измененного
    функция. Суффиксы этого типа, которые в настоящее время используются в std, в основном
    _until , _while , _then , _else , _mut и _back .

Причина, по которой drain_retain сбивает с толку, заключается в том, что неясно,
на основе слива или сохранения, если он основан на сливе, возврат true удалит
значение, если оно сохранено, оно сохранит его. Использование _where делает это на
last ясно, что ожидается от переданной функции.

Пн, 26 февраля 2018 г., 00:25 [email protected] написал:

@датинаб https://github.com/dathinab

  1. Здесь мы говорим о методе для коллекций, а не для Iterator.
    map, filter, filter_map, skip, take_while и т. д. — все это методы Iterator.
    Кстати, какие методы вы имеете в виду, которые используют *_where?
    Поэтому мы должны сравнить схему именования с уже существующими методами.
    для коллекций, например, сохранить(), слить(). Нет путаницы с
    Методы итератора, которые преобразуют итератор в другой итератор.
  2. Насколько мне известно, консенсус заключался в том, что continue_mut() не будет добавляться в std.
    потому что дренаж_фильтр() уже будет добавлен, и люди были предупреждены
    использовать это. Что возвращает нас к варианту использования миграции из
    Сохранение () для дренажа_фильтра () очень распространено, поэтому он должен иметь
    похожее имя и API (закрытие, возвращающее true, означает сохранение записи).


Вы получаете это, потому что вас упомянули.

Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-368355110 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AHR0kfkRAZ5OtLFZ-SciAmjHDEXdgp-0ks5tYevegaJpZM4OY1me
.

Я много использую эту функцию, и мне всегда нужно помнить, что нужно возвращать true для записей, которые я хочу слить, что кажется нелогичным по сравнению с keep()/retain_mut().

FWIW, я думаю, что имя retain здесь противоречит здравому смыслу. Обычно я обнаруживаю, что хочу удалить определенные элементы из вектора, и с retain мне всегда приходится инвертировать эту логику.

Но retain() уже в стабильной версии, так что придется с этим жить.. Так что моя интуиция к этому привыкла..

@Boscop : так же как и drain , который является обратным retain , но также возвращает удаленные элементы и использование суффиксов, таких как _until , _while для создания доступные функции, которые являются лишь слегка измененной версией существующей функциональности.

Я имею в виду, что если бы я описал сток, это было бы что-то вроде:

_удалить и вернуть все элементы, указанные "каким-то образом", сохранить все остальные элементы_
где _"каким-то образом"_ означает _"нарезку"_ для всех типов коллекций, которые можно нарезать, и _"все"_ для остальных.

Описание обсуждаемой здесь функции _то же_, за исключением того, что
_"каким-то образом"_ есть _", где данный предикат возвращает true"_.

С другой стороны, описание, которое я бы дал:
_только сохранять (т.е. сохранять) элементы, в которых данный предикат возвращает истину, отбрасывать остальные_

(да, сохранить можно было бы использовать таким образом, чтобы остальные не отбрасывались, к сожалению, это не так)


Я действительно думаю, что было бы очень хорошо, если бы retain
передал &mut T в предикат и, возможно, вернул удаленные значения.
Потому что я думаю, что retain — более интуитивно понятная база имен.

Но независимо от этого я также думаю, что оба drain_filter / drain_retain неоптимальны
поскольку они не дают понять, должен ли предикат возвращать true/false, чтобы сохранить/исчерпать запись.
(drain указывает, что true удаляет его, поскольку он говорит об удалении элементов во время фильтрации
а переобучение говорит о том какие элементы оставить, наконец в ржавчине)


В конце концов, не так важно, какое из названий используется, было бы просто неплохо, если бы оно стабилизировалось.

Проведение опроса и/или предоставление возможности решить кому-то из языковой команды может быть лучшим способом продвинуться вперед?

Я думаю, что что-то вроде drain_where , drain_if или drain_when намного понятнее, чем drain_filter .

@tmccombs Из этих 3 я думаю, что drain_where имеет наибольший смысл. (Поскольку if подразумевает either do the whole thing (in this case draining) or not , а when является временным.)
По сравнению с drain_filter возвращаемое значение замыкания такое же, как и у drain_where ( true для удаления элемента), но этот факт становится более ясным/явным благодаря названию, поэтому оно устраняет риск случайной интерпретации значения возвращаемого значения закрытия неправильно.

Думаю, пришло время стабилизироваться. Резюме этой темы:

  • Следует ли добавить параметр R: RangeArgument ?
  • Следует ли инвертировать логическое значение? (Я думаю, что текущая логика имеет смысл: возврат true из обратного вызова приводит к включению этого элемента в итератор.)
  • Именование. (Мне нравится drain_where .)

@Ганкро , что ты думаешь?

Команда libs обсудила это, и пришли к соглашению, что на данный момент не следует стабилизировать более drain -подобные методы. (Существующий метод drain_filter может остаться в Nightly как нестабильный). (в отличие от использования итератора до конца).

Мы хотели бы увидеть эксперименты, чтобы попытаться обобщить на меньшей поверхности API различные комбинации слива:

  • Поддиапазон (через RangeArgument ака RangeBounds ) против всей коллекции (хотя последнее может быть достигнуто путем передачи .. , значения типа RangeFull ).
  • Слив всего (возможно, в пределах этого диапазона) против только элементов, которые соответствуют логическому предикату
  • Самоисчерпаемость при выпадении или нет (остальные элементы остаются в коллекции).

Возможности могут включать «перегрузку» метода, сделав его универсальным или шаблоном построителя.

Одним из ограничений является стабильность метода drain . Возможно, его можно обобщить, но только обратно совместимыми способами.

Мы хотели бы увидеть эксперименты, чтобы попытаться обобщить на меньшей поверхности API различные комбинации слива:

Как и где, по мнению команды, будут проводиться подобные эксперименты?

Как: придумать и предложить конкретный дизайн API, возможно, с реализацией проверки концепции (что можно сделать вне дерева, по крайней мере, через Vec::as_mut_ptr и Vec::set_len ). Где не имеет большого значения. Это может быть новый RFC или поток в https://internals.rust-lang.org/c/libs , и ссылка на него здесь.

Я немного поиграл с этим. В ближайшие дни открою тему по внутренностям.

Я думаю, что общий API, который работает следующим образом, имеет смысл:

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

Так что это API в стиле строителя: если .where(pred) не добавлено, он безоговорочно истощит весь диапазон.
Это охватывает возможности текущего метода .drain(a..b) , а также .drain_filter(pred) .

Если имя drain нельзя использовать, потому что оно уже используется, это должно быть похожее имя, например drain_iter .

Метод where не должен называться *_filter , чтобы избежать путаницы с фильтрацией результирующего итератора, особенно когда where и filter используются в такой комбинации:

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

Здесь он будет использовать pred1 , чтобы решить, что будет слито (и передано в итераторе), а pred2 используется для фильтрации результирующего итератора.
Любые элементы, которые pred1 возвращают true for, но pred2 возвращают false for, будут по-прежнему вытягиваться из v , но не будут возвращены этот комбинированный итератор.

Что вы думаете о таком подходе к API в стиле конструктора?

На секунду я забыл, что where нельзя использовать в качестве имени функции, потому что это уже ключевое слово :/

А drain уже стабилизировано, так что это имя тоже нельзя использовать..

Тогда я думаю, что второй лучший общий вариант — сохранить текущий drain и переименовать drain_filter в drain_where , чтобы избежать путаницы с .drain(..).filter() .

(Как сказал Джонху выше:)

что послужило причиной назвать этот дренажный_фильтр, а не дренажный_где? Для меня первое означает, что весь Vec будет слит, но мы также запускаем фильтр по результатам (когда я впервые увидел это, я подумал: «Как это не просто .drain(..).filter() ?"). Первый, с другой стороны, указывает на то, что мы сливаем только те элементы, для которых выполняется какое-либо условие.

Я открыл тему о внутренностях .
TLDR заключается в том, что я думаю, что несамоистощение — это большая банка червей, чем ожидалось в общем случае, и что мы должны стабилизировать drain_filter раньше, чем позже, с параметром RangeBounds . Если только у кого-то нет хорошей идеи для решения изложенных там проблем.

Редактировать: я загрузил свой экспериментальный код: эксперименты по сливу
Есть также стенды для слива и очистки и некоторые тесты, но не ожидайте чистого кода.

Совсем пропустил эту тему. У меня был старый импл, который я немного подправил и скопировал, чтобы отразить несколько опций, описанных в этой теме. Одна приятная вещь в реализации, которая, я думаю, не вызовет споров, заключается в том, что она реализует DoubleEndedIterator . Посмотреть его здесь .

@Emerentius, но тогда мы должны хотя бы переименовать drain_filter в drain_where , чтобы указать, что замыкание должно вернуть true для удаления элемента!

@Boscop Оба подразумевают одинаковую «полярность» true => yield. Лично мне все равно, называется ли это drain_filter или drain_where .

@Popog Можете ли вы обобщить различия, плюсы и минусы? В идеале над внутренней резьбой. Я думаю, что функциональность DoubleEndedIterator может быть добавлена ​​с обратной совместимостью с нулевыми или низкими накладными расходами (но я не проверял это).

Как насчет drain_or_retain ? Это грамматически значимое действие, и оно сигнализирует о том, что оно выполняет одно или другое действие.

@askeksa Но это не проясняет, означает ли возврат true из закрытия «слить» или «сохранить».
Я думаю, что с таким именем, как drain_where , совершенно ясно, что возврат true истощает его, и всем должно быть ясно, что элементы, которые не истощаются, сохраняются.

Было бы неплохо, если бы был какой-то способ ограничить/остановить/отменить/прервать слив. Например, если бы я хотел слить первые N четных чисел, было бы неплохо иметь возможность просто сделать vec.drain_filter(|x| *x % 2 == 0).take(N).collect() (или что-то подобное).

Как это реализовано в настоящее время, метод DrainFilter drop всегда будет выполнять очистку до завершения; его нельзя прервать (по крайней мере, я не придумал никакого трюка, который мог бы это сделать).

Если вам нужно такое поведение, вы должны просто закрыть некоторое состояние, которое отслеживает, сколько вы видели, и начать возвращать false. Запуск до завершения при сбросе необходим, чтобы адаптеры вели себя разумно.

Я только что заметил, что способ реализации drain_filter в настоящее время небезопасен, но
на самом деле угроза безопасности wrt. расслабиться + возобновить безопасность. Кроме того, это легко вызывает аборт, как
из которых поведение метода в std действительно не должно быть. И пока писал это заметилчто его текущая реализация небезопасна

Я знаю, что Vec по умолчанию не является безопасным, но поведение drain_filer , когда
предикат panic вполне удивителен, потому что:

  1. он продолжит вызывать закрытие, которое запаниковало при падении
    если закрытие снова вызовет панику, это вызовет посадку на борт, и в то время как некоторые люди
    как и все паники, чтобы быть на борту другой работы с паттернами ядра ошибок и для них
    закончить с бортом довольно плохо
  2. если не будет правильно продолжать слив потенциально одно значение
    и содержащие одно уже отброшенное значение, что потенциально может привести к использованию после бесплатного

Пример такого поведения здесь:
play.rust-lang.org

В то время как пункт 2. должен быть решаемым, я думаю, что первый пункт сам по себе должен
привести к пересмотру поведения DrainFilter для выполнения до завершения
при падении причины для изменения этого включают:

  • итераторы ленивы в ржавчине, выполнение итератора при удалении - довольно неожиданное поведение
    исходя из того, что обычно ожидается
  • предикат, переданный в drain_filter , может вызвать панику при некоторых обстоятельствах (например, блокировка
    отравился) и в этом случае, скорее всего, снова запаниковать при вызове во время дроп-лидинга
    на двойную панику и, следовательно, на борт, что довольно плохо для любого, кто использует ядро ​​​​ошибки
    шаблоны или, наконец, желание завершить работу контролируемым образом, это нормально, если вы все равно используете panic=aboard
  • если у вас есть побочные эффекты в предикате и вы не запустите DrainFilter до завершения, вы можете получить
    удивительные ошибки, когда он затем запускается до завершения при падении (но вы, возможно, сделали
    другой думает между сливом его до точки и падением)
  • вы не можете отказаться от этого поведения без изменения переданного ему предиката, который вы
    может быть не в состоянии обойтись без его упаковки, с другой стороны, вы всегда можете подписаться на запуск
    его до завершения, просто запустив итератор до завершения (да, этот последний аргумент немного
    ручная работа)

Аргументы в пользу запуска до завершения включают:

  • drain_filter аналогична ratain , которая является функцией, поэтому люди могут быть удивлены, когда они
    "просто" сбросить DrainFilter вместо того, чтобы запустить его до конца

    • этому аргументу много раз противостояли в других RFC, и поэтому #[unused_must_use]

      существующих, которые в некоторых ситуациях уже рекомендуют использовать .for_each(drop) , что по иронии судьбы

      случается, что DrainFilter делает при сбросе

  • drain_filter часто используется только из-за побочного эффекта, так что это слишком многословно.

    • используя его таким образом, он примерно равен retain



      • но сохраните использование &T , сток_фильтр использовал &mut T



  • другие??
  • [РЕДАКТИРОВАТЬ, ДОБАВЛЕНО ПОЗЖЕ, THX @tmccombs ]: незавершение при сбросе может быть очень запутанным в сочетании с такими адаптерами, как find , all , any , что я довольно веская причина чтобы сохранить текущее поведение.

Это может быть только я, или я пропустил какой-то момент, но изменил поведение Drop и
добавление #[unused_must_use] кажется предпочтительным?

Если .for_each(drop) слишком длинный, мы могли бы вместо этого рассмотреть возможность добавления RFC для итераторов, предназначенных для
есть побочный эффект, добавляющий к итератору такой метод, как complete() (или drain() , но это
это совсем другой разговор)

другие??

Я не могу найти исходную аргументацию, но я помню, что также была проблема с адаптерами, работающими с DrainFilter , которые не выполняются до конца.

См. также https://github.com/rust-lang/rust/issues/43244#issuecomment -394405057

Хороший момент, например, find приведет к сливу только до тех пор, пока он не достигнет первого
совпадают, подобные all , any делают короткое замыкание, что может сбивать с толку
относительно осушать.

Хм, может, мне стоит изменить свое мнение. Через это может быть общая проблема
с итераторами, имеющими побочные эффекты, и, возможно, нам следует рассмотреть общее решение
(независимо от этой проблемы с отслеживанием), как адаптер .allways_complete() .

Лично я не нашел никаких причин безопасности, по которым слив должен выполняться до конца, но, как я писал здесь парой сообщений выше, побочные эффекты на next() взаимодействуют субоптимальным образом с такими адаптерами, как take_while , peekable и skip_while .

Это также поднимает те же проблемы, что и мой RFC о несамоисчерпаемом стоке и связанном с ним самоисчерпающем адаптере iter RFC .

Это правда, что drain_filter может легко вызвать прерывание, но можете ли вы показать пример, где это нарушает безопасность?

Да, я уже сделал: play.rust-lang.org

Что это:

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

}

Но это внутреннее мышление реализации, которое пошло не так.
В основном открытый вопрос заключается в том, как обрабатывать panic предикатной функции:

  1. пропустить элемент, на котором он запаниковал, слить его и увеличить счетчик del

    • требует некоторой формы обнаружения паники

  2. не продвигайте idx перед вызовом предиката

    • но это означает, что при сбросе он снова будет вызываться с тем же предикатом

Другой вопрос заключается в том, стоит ли запускать функции, которые можно рассматривать как пользовательский ввод API при удалении.
в общем, но тогда это единственный способ не заставить find , any и т. д. вести себя запутанно.

Возможно, соображение могло бы быть чем-то вроде:

  1. установить флаг при вводе next , снять его перед возвратом из next
  2. при сбросе, если флаг все еще установлен, мы знаем, что запаниковали, и, следовательно, происходит утечка
    оставшиеся предметы ИЛИ удалить все оставшиеся предметы

    1. может быть довольно большая утечка с неожиданными побочными эффектами, если вы, например, утечка дуги

    2. может быть очень удивительным, если у вас есть Arc и Weak's

Возможно, есть лучшее решение.
Каким бы он ни был, он должен быть задокументирован в rustdoc после его реализации.

@датинаб

Ага, я уже сделал

Утечка нежелательна, но хороша, и ее здесь трудно избежать, но двойного падения точно нет. Хороший улов! Хотели бы вы сообщить об отдельной проблеме, связанной с этой проблемой безопасности?

Выполняет ли drain_filter перераспределение каждый раз, когда удаляет элемент из коллекции? Или он перераспределяет только один раз и работает как std::remove и std::erase (в паре) в C++? Я бы предпочел такое поведение из-за ровно одного распределения: мы просто помещаем наши элементы в конец коллекции, а затем удаляем ее, уменьшая ее до нужного размера.

Кроме того, почему нет try_drain_filter ? Что возвращает тип Option и значение None , если мы должны остановиться? У меня очень большая коллекция и мне бессмысленно продолжать, когда я уже получил то, что мне было нужно.

В прошлый раз, когда я изучал код, он сделал что-то вроде: создал "пробел"
при перемещении элементов и перемещении элемента, который не слит, в
начало разрыва, когда он находит его. При этом каждый элемент, который должен быть
перемещается (вне или на новое место в массиве) перемещается только один раз.
Также как, например, remove , он не перераспределяет. Освобожденная часть просто становится
часть неиспользуемой мощности.

Пт, 10 авг 2018, 07:11 Виктор Полевой[email protected] написал:

Выполняет ли дренажный фильтр перераспределение каждый раз, когда он удаляет элемент из
коллекция? Или он делает перераспределение только один раз и работает как std::remove
и std::erase (в паре) на С++? Я бы предпочел такое поведение из-за
ровно одно выделение: мы просто помещаем наши элементы в конец коллекции
а затем удаляет уменьшить его до нужного размера.

Кроме того, почему нет try_drain_filter? Который возвращает тип опции и
Нет значения, если мы должны остановиться? У меня очень большая коллекция, и это
бессмысленно продолжать для меня, когда я уже получил то, что мне нужно.


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-411977001 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AHR0kdOZm4bj6iR9Hj831Qh72d36BxQSks5uPRYNgaJpZM4OY1me
.

@rustonaut спасибо. Что вы думаете о try_drain_filter ? :)

PS Только что посмотрел код, вроде работает так, как мы хотели.

Он продвигается поэлементно при итерации, поэтому обычно вы можете ожидать
чтобы остановить итерацию при удалении, но это было сочтено слишком
сбивает с толку, поэтому он фактически стекает до конца при падении.
(Что резко увеличивает вероятность двойной паники и прочего
как это).

Так что маловероятно, что вы получите пробную версию, которая ведет себя как вы.
ожидать.

Справедливости ради ранняя остановка слива, когда итерация действительно может сбивать с толку
в некоторых ситуациях, например, thing.drain_where(|x| x.is_malformed()).any(|x| x.is_dangerus()) не удалит все искаженные, а только до тех пор, пока один из
найдено, что также опасно. (Текущая реализация истощает все деформированные
продолжая сливать по капле).

Пт, 10 авг 2018, 10:52 Виктор Полевой[email protected] написал:

@rustonaut https://github.com/rustonaut спасибо. Каково твое мнение
о try_drain_filter? :)


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-412020490 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AHR0kcEMHCayqvhI6D4LK4ITG2x5di-9ks5uPUnpgaJpZM4OY1me
.

Я думаю, что это было бы более универсальным:

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

Привет, я искал функциональность drain_filter для HashMap , но ее не существует, и меня попросили открыть проблему, когда я нашел эту. Это должно быть в отдельной теме?

Что-то в настоящее время блокирует это от стабилизации? Это все еще небезопасно для раскручивания, как сообщалось выше?

Это кажется довольно маленькой функцией, и она находится в подвешенном состоянии уже более года.

Я думаю, что это было бы более универсальным:

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

Я не думаю, что это лучше, чем композиция из drain_filter и map .

Это все еще небезопасно для раскручивания, как сообщалось выше?

По-видимому, существует сложный выбор между отказом от слива всех совпадающих элементов, если итерация останавливается раньше, или потенциальной паникой во время раскрутки, если фильтрация и слив до конца выполняются при сбросе DrainFilter .
Я думаю, что эта функция изобилует проблемами в любом случае, и поэтому ее не следует стабилизировать.

Есть ли какая-то особая проблема с тем, что он ведет себя по-разному при раскрутке?

Возможности:

  • Он может выполняться до завершения в обычном режиме, но оставлять совпадающие элементы при раскрутке (так что предполагается, что все оставшиеся элементы не совпадают).
  • Он может нормально выполняться до завершения, но обрезать вектор после последней записанной позиции при раскрутке (так что предполагается, что все оставшиеся элементы совпадают).
  • Он может выполняться до завершения нормально, но усекать вектор до длины 0 при раскрутке.

Самый понятный контраргумент, который я могу придумать, состоит в том, что код drop зависит от инварианта, обычно предоставляемого drain_filter (что в конце элементы в векторе будут точно такими же, какие не выполнил условие) может быть произвольно удален от кода (скорее всего, нормального, безопасного кода), который использует drain_filter .

Однако предположим, что такой случай был. Этот код будет содержать ошибки независимо от того, как пользователь его напишет. Например, если они напишут императивный цикл, который пошел в обратном порядке и заменил элементы, удаленные, тогда, если их условие может вызвать панику, а их реализация удаления сильно зависит от условия фильтра, являющегося ложным, в коде все еще есть ошибка. Наличие такой функции, как drop_filter , документация которой может привлечь внимание к этому пограничному случаю, кажется улучшением по сравнению с ней.

Кроме того, спасибо, я нашел этот пример игровой площадки , опубликованный ранее в ветке, который демонстрирует, что текущая реализация по-прежнему дважды отбрасывает элементы. (так что его определенно нельзя стабилизировать как есть!)

Может быть, стоит открыть отдельную тему по поводу ошибки со звуком? Тогда это может быть помечено как I-нездоровое.

Насколько я знаю, вы не можете отмечать или так же нездоровы, как двойная паника _это звук_
просто очень неудобно как то обрывается. Также, насколько я помню,
возможность двойной паники - это не ошибка, а поведение неявно, но
сознательно выбран.

Варианты в основном:

  1. Не бегите до конца при падении.
  2. Выполнить до завершения, но потенциально прервать из-за двойной паники
  3. Отбрасывайте все «непроверенные» элементы при сбросе во время паники.
  4. Не завершайте при падении во время паники.

Проблемы:

  1. => Неожиданное поведение во многих случаях использования.
  2. => Неожиданное прерывание, если предикат может вызвать панику, особенно если вы его используете
    чтобы "просто" удалить элементы, т.е. вы не используете возвращаемый итератор.
  3. => Неожиданная разница между падением и выходом из паники. Просто
    считайте, что кто-то _использует_Drain_filter в функции сброса.
  4. => См. 3.

Или, другими словами, 1. приводит к путанице в обычных случаях использования, 2. может привести к
прервать, если предикат может паниковать 3.,4. сделай так, чтобы ты не мог на самом деле
используйте его в методе сброса, но как вы теперь используете функцию, которую вы там используете
не использует его внутри.

В результате этого варианта 3.,4. не идут. Проблемы с вариантом 2.
реже, чем в 1. поэтому был выбран 2.

ИМХО было бы лучше иметь API стока+дрена_фильтра, которые не запускаются
до завершения при сбросе + общий комбинатор итераторов, который работает до
завершение при удалении + метод, который завершает итератор, но просто удаляет все
оставшиеся предметы. Проблема в том, что сток уже стабилен, итератор
комбинатор добавляет накладные расходы, поскольку ему необходимо объединить внутренний итератор и слить
может быть не самое подходящее имя.

В понедельник, 20 мая 2019 г., 09:28 Ральф Юнг, уведомления@github.com написал:

Может быть, стоит открыть отдельную тему по поводу ошибки со звуком? Это может
тогда пометить как I-нездоровый.


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/43244?email_source=notifications&email_token=AB2HJEL7FS6AA2A2KF5U2S3PWJHK7A5CNFSM4DTDLGPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVueX5RWA#issue#issue
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AB2HJEMHQFRTCH6RCQQ64DTPWJHK7ANCNFSM4DTDLGPA
.

Однако двойные капли ненадежны.

Создано # 60977 для проблемы с надежностью.

Спасибо, я чувствую себя глупо из-за того, что прочитал двойное падение как двойную панику :man_facepalming: .

  1. => Неожиданная разница между падением и выходом из паники. Просто
    считайте, что кто-то _использует_Drain_filter в функции сброса.

3.,4. сделай так, чтобы ты не мог на самом деле
используйте его в методе сброса, но как вы теперь используете функцию, которую вы там используете
не использует его внутри.

Это для меня все еще не имеет большого значения.

Если кто-то использует drain_filter в реализации Drop с условием, вызывающим панику, проблема не в том, что он решил использовать drain_filter ; Точно так же не имеет значения, использует ли метод внутри себя drain_filter ;

Извините, я ответил слишком рано. Кажется, теперь я понимаю, что вы имеете в виду. Я еще немного обдумаю это.

Итак, ваш аргумент заключается в том, что код внутри реализации drop , который использует drain_filter , может таинственным образом сломаться, если он запустится во время раскрутки. (речь идет не о панике drain_filter , а о панике другого кода, который вызывает запуск 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
        ...
    }
}

Этот drop impl мог внезапно начать работать неправильно во время раскрутки.

Это действительно убедительный аргумент против того, чтобы DrainFilter наивно обнаруживал, что текущий поток паникует.

Терминология drain_filter больше всего подходит для меня. Учитывая, что у нас уже есть drain для удаления всех элементов, выбор элементов для удаления будет filter . В сочетании названия кажутся очень последовательными.

Исправление проблемы с надежностью двойной паники оставляет оставшуюся часть Vec нетронутой в случае паники в предикате. Элементы сдвигаются назад, чтобы заполнить пробел, но в остальном остаются в покое (и отбрасываются через Vec::drop во время раскрутки или иным образом обрабатываются пользователем, если паника обнаружена).

Преждевременное удаление vec::DrainFilter продолжает вести себя так же, как если бы оно было полностью израсходовано (что аналогично vec::Drain ). Если предикат вызывает панику во время vec::Drain::drop , оставшиеся элементы смещаются назад, как обычно, но дальнейшие элементы не отбрасываются, и предикат больше не вызывается. По сути, он ведет себя одинаково независимо от того, происходит ли паника в предикате при обычном потреблении или при сбросе vec::DrainFilter .

Если предположить, что исправление дыры в надежности правильное, что еще сдерживает стабилизацию этой функции?

Можно ли стабилизировать Vec::drain_filter независимо от LinkedList::drain_filter ?

Проблема с терминологией drain_filter заключается в том, что с drain_filter на самом деле есть два вывода: возвращаемое значение и исходная коллекция, и на самом деле неясно, с какой стороны "отфильтровано" предметы продолжаются. Я думаю, что даже filtered_drain немного понятнее.

не совсем понятно, с какой стороны идут "отфильтрованные" элементы

Vec::drain создает прецедент. Вы указываете диапазон элементов, которые хотите _удалить_. Vec::drain_filter работает так же. Вы определяете элементы, которые хотите _удалить_.

и не совсем понятно, с какой стороны идут "отфильтрованные" элементы

Я считаю, что это уже верно для Iterator::filter , поэтому я смирился с тем, что мне нужно смотреть документы / писать тест всякий раз, когда я это использую. Я не против того же для drain_filter .


Я бы хотел, чтобы мы выбрали терминологию select и reject Ruby, но этот корабль уже давно уплыл.

Есть ли прогресс в этом? Является ли имя единственной вещью, которая все еще находится в подвешенном состоянии?

Есть ли что-то, что мешает этому стабилизироваться?

Похоже, что DrainFilter #$ импл Drop вызовет утечку элементов, если какой-либо из их деструкторов паникует предикат паники. Это основная причина https://github.com/rust-lang/rust/issues/52267. Мы уверены, что хотим стабилизировать такой API?

Неважно, это было исправлено https://github.com/rust-lang/rust/pull/61224 , по-видимому.

Я тоже немного поковыряюсь в этой проблеме с отслеживанием, хотелось бы, чтобы эта функция работала стабильно :D Есть ли какие-нибудь блокировщики?

копия @Dylan-DPC

Было ли когда-либо принято решение в пользу или против того, чтобы drain_filter принимал параметр RangeBounds , как это делает drain ? Передача .. кажется достаточно простой, если вы хотите отфильтровать весь вектор, поэтому я, вероятно, поддержу его добавление.

Я думаю, что это было бы более универсальным:

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

Почти, более общая версия потребует FnMut(T) -> Option<U> , например, Iterator::{filter_map, find_map, map_while} . Я понятия не имею, стоит ли обобщать filter_map таким образом, но, возможно, стоит подумать.

Я прибыл сюда, потому что искал более или менее именно метод, предложенный @jethrogb выше:

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

Единственное отличие от того, что я имел в виду (которое я называл update в своей голове), заключалось в том, что я не подумал заставить его возвращать итератор слива, но это кажется явным улучшением, поскольку оно обеспечивает единый, достаточно простой интерфейс, поддерживающий обновление элементов на месте, удаление существующих элементов и доставку удаленных элементов вызывающей стороне.

Почти, более общая версия будет принимать FnMut(T) -> Option<U> , как это делает Iterator::{filter_map, find_map, map_while} . Я понятия не имею, стоит ли обобщать filter_map таким образом, но, возможно, стоит подумать.

Функция должна возвращать Option<T> , потому что полученные ею значения хранятся в Vec<T> .

@ johnw42 johnw42 Я не уверен, что понял, не будет ли итератор немедленно выдавать значение Some ?

На самом деле, я предполагаю, что входное значение этой функции по-прежнему должно быть &T или &mut T вместо T , если вы не хотите его сливать. Или, может быть, функция может быть чем-то вроде FnMut(T) -> Result<U, T> . Но я не понимаю, почему тип элемента не может быть другим типом.

@timvermeulen Я думаю, что мы интерпретируем это предложение по-разному.

Как я это интерпретировал, значение Some сохраняется обратно в Vec , а None означает, что исходное значение выдается итератором. Это позволяет замыканию либо обновить значение на месте, либо переместить его из Vec . При написании этого я понял, что моя версия на самом деле ничего не добавляет, потому что вы можете реализовать ее с точки зрения 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,
    })
}

И наоборот, я думал, что ваша интерпретация не очень полезна, потому что она эквивалентна отображению результата drain_filter , но я попытался написать это, и это не так, по той же причине filter_map не t эквивалентно вызову filter с последующим map .

@ johnw42 А, да, я думал, вы хотели, чтобы None означало, что значение должно оставаться в Vec .

Так что кажется, что FnMut(T) -> Result<U, T> будет наиболее общим, хотя это, вероятно, не очень эргономично. FnMut(&mut T) -> Option<U> на самом деле не вариант, потому что это не позволит вам стать владельцем T в общем случае. Я думаю, что FnMut(T) -> Result<U, T> и FnMut(&mut T) -> bool — единственные варианты.

@timvermeulen Я начал говорить что-то ранее о «самом общем» решении, и мое «самое общее» решение отличалось от вашего, но я пришел к тому же выводу, что попытка сделать функцию слишком общей приводит к тому, что вы на самом деле не хотел бы использовать.

Хотя, возможно, есть некоторая ценность в создании очень общего метода, на основе которого опытные пользователи могут создавать более удобные абстракции. Насколько я могу судить, смысл drain и drain_filter не в том, что они особенно эргономичны — это не так, — а в том, что они поддерживают варианты использования, возникающие в практики, и которые нельзя написать по-другому без множества избыточных ходов (или с использованием небезопасных операций).

С drain вы получаете следующие приятные свойства:

  • Любой непрерывный выбор элементов может быть удален.
  • Отбросить удаленные элементы так же просто, как отказаться от возвращенного итератора.
  • Удаленные элементы не нужно отбрасывать; вызывающий абонент может изучить каждый из них по отдельности и выбрать, что с ним делать.
  • Содержимое Vec не обязательно должно поддерживать Copy или Clone .
  • Не нужно выделять или освобождать память для самого Vec .
  • Значения, оставленные в Vec , перемещаются не более одного раза.

С помощью drain_filter вы получаете возможность удалять произвольный набор элементов из Vec , а не только непрерывный диапазон. Менее очевидным преимуществом является то, что даже если удалить непрерывный диапазон элементов, drain_filter все равно может повысить производительность, если поиск диапазона для перехода к drain потребует отдельного прохода по Vec #$11$#$. &mut T , можно даже обновить элементы, оставшиеся в Vec . Ура!

Вот некоторые другие вещи, которые вы, возможно, захотите сделать с операцией на месте, такой как drain_filter :

  1. Преобразуйте удаленные элементы, прежде чем возвращать их через итератор.
  2. Досрочно прервите операцию и сообщите об ошибке.
  3. Вместо того, чтобы просто удалить элемент или оставить его на месте (возможно, изменив его), добавьте возможность удалить элемент и заменить его новым значением (которое может быть клоном исходного значения или чем-то совершенно другим).
  4. Замените удаленный элемент несколькими новыми элементами.
  5. Сделав что-то с текущим элементом, пропустите некоторое количество следующих элементов, оставив их на месте.
  6. Сделав что-то с текущим элементом, удалите некоторое количество следующих элементов, не проверяя их сначала.

И вот мой анализ каждого:

  1. Это не добавляет ничего полезного, потому что вызывающий объект уже может преобразовывать элементы по мере того, как они возвращаются итератором. Это также противоречит цели итератора, которая состоит в том, чтобы избежать клонирования значений, доставляя их вызывающей стороне только после того, как они были удалены из Vec .
  2. Возможность преждевременного прерывания потенциально может улучшить асимптотическую сложность в некоторых случаях. Сообщение об ошибке как часть API не добавляет ничего нового, потому что вы можете сделать то же самое, заставив замыкание изменить захваченную переменную, и неясно, как это сделать, потому что значение не будет создано до тех пор, пока итератор не был съеден.
  3. Это, я думаю, добавляет некоторую реальную общность.
  4. Это то, что я изначально собирался предложить как вариант «кухонной раковины», но решил, что это бесполезно, потому что, если один элемент можно заменить несколькими элементами, невозможно сохранить свойство, которое элементы в Vec перемещаются не более одного раза, и может потребоваться перераспределение буфера. Если вам нужно сделать что-то подобное, это не обязательно эффективнее, чем просто построить новый Vec , а может быть и хуже.
  5. Это может быть полезно, если Vec организовано таким образом, что вы можете просто пропустить большую часть элементов, не останавливаясь для их проверки. Я не включил его в свой пример кода ниже, но его можно поддержать, изменив замыкание, чтобы оно возвращало дополнительные usize , указывающие, сколько из следующих элементов нужно перепрыгнуть, прежде чем продолжить.
  6. Это кажется дополнением к пункту 5, но не очень полезно, если вам все еще нужно вернуть удаленные элементы через итератор. Тем не менее, это может оказаться полезной оптимизацией, если у удаляемых вами элементов нет деструктора, и вы просто хотите, чтобы они исчезли. В этом случае usize выше можно заменить на Keep(usize) или Drop(usize) (где Keep(0) и Drop(0) семантически эквивалент).

Я думаю, что мы можем поддерживать основные варианты использования, если замыкание возвращает перечисление с 4 вариантами:

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

Последний вариант, который я хотел бы представить, — полностью избавиться от итератора, передать элементы в замыкание по значению и позволить вызывающей стороне оставить элемент без изменений, заменив его самим собой:

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

Мне больше всего нравится этот подход, потому что он прост и поддерживает все те же варианты использования. Потенциальный недостаток заключается в том, что даже если большинство элементов останется на месте, их все равно нужно будет переместить в фрейм стека замыкания, а затем вернуть обратно, когда замыкание вернется. Можно было бы надеяться, что эти ходы можно надежно оптимизировать, когда замыкание просто возвращает свой аргумент, но я не уверен, стоит ли нам на это рассчитывать. Если другим людям он понравится достаточно, чтобы включить его, я думаю, что update было бы хорошим названием для него, потому что, если я не ошибаюсь, его можно использовать для реализации любого однопроходного обновления на месте Содержимое Vec .

(Кстати, я полностью проигнорировал связанные списки выше, потому что я совершенно забыл о них, пока не посмотрел заголовок этого выпуска. Если мы говорим о связанном списке, это меняет анализ пунктов 4-6, поэтому я думаю, что другое API подходит для связанных списков.)

@johnw42 johnw42 вы уже можете сделать 3. если у вас есть изменяемая ссылка, используя mem::replace или mem::take .

@johnw42 @jplatte

(3) действительно имеет смысл только в том случае, если мы позволяем типу элемента возвращаемого Iterator отличаться от типа элемента коллекции.
(3) является особым случаем, потому что вы одновременно возвращаете элемент из Iterator и помещаете новый элемент обратно в Vec .

Bikeshedding: я бы как бы перевернул функцию Replace(T) и заменил ее на PushOut(T) с целью «отправить» внутреннее значение PushOut в итератор, сохраняя при этом исходный элемент (параметр) в Vec .

Вероятно, Stop должен иметь возможность возвращать тип Error (или работать как try_fold ?).

Вчера вечером я реализовал свою функцию super_drain_by_value и узнал несколько вещей.

Заголовок, вероятно, должен состоять в том, что, по крайней мере, в отношении Vec все , о чем мы здесь говорим, относится к категории «приятно иметь» (в отличие от добавления принципиально новой возможности), потому что Vec уже обеспечивает прямой доступ для чтения и записи ко всем своим полям через существующий API. В стабильной версии есть небольшая оговорка: вы не можете наблюдать поле указателя пустого Vec , но нестабильный метод into_raw_parts снимает это ограничение. На самом деле мы говорим о расширении набора операций, которые можно эффективно выполнять с помощью безопасного кода.

С точки зрения генерации кода, я обнаружил, что в простых случаях (например, Vec<i32> ) избыточные перемещения внутрь и наружу Vec не являются проблемой, и вызовы сводятся к таким простым вещам, как отсутствие операций или усечение Vec превращаются в код, который слишком прост для улучшения (ноль и три инструкции соответственно). Плохая новость заключается в том, что в более сложных случаях и мое предложение, и метод drain_filter делают много ненужного копирования, в значительной степени нарушая цель методов. Я проверил это, просмотрев ассемблерный код, созданный для Vec<[u8; 1024]> , и в обоих случаях каждая итерация имеет два вызова memcpy , которые не были оптимизированы. Даже бездействующий вызов приводит к двойному копированию всего буфера!

С точки зрения эргономики мой API, который на первый взгляд выглядит довольно хорошо, на практике не так хорош; возврат значения перечисления из замыкания становится довольно многословным во всех случаях, кроме простейших, а предложенный мной вариант, когда замыкание возвращает пару значений перечисления, еще более уродлив.

Я также попытался расширить DrainAction::Stop , чтобы передать значение R , возвращаемое из super_drain_by_value как Option<R> , и это еще хуже, потому что в (предположительно типичном) случае, когда возвращаемое значение не требуется, компилятор не может вывести R , и вы должны явно аннотировать тип значения, которое вы даже не используете. По этой причине я не считаю хорошей идеей поддержку возврата значения из замыкания вызывающему объекту super_drain_by_value ; это примерно аналогично тому, почему конструкция loop {} может возвращать значение, но любой другой тип цикла оценивается как () .

Что касается общности, я понял, что на самом деле есть два случая преждевременного завершения: в одном оставшаяся часть Vec отбрасывается, а в другом она остается на месте. Если преждевременное завершение не имеет значения (а я думаю, что и не должно), оно становится семантически эквивалентным возврату Keep(n) или Drop(n) , где n — это количество еще не проверенные предметы. Однако я считаю, что преждевременное завершение следует рассматривать как отдельный случай, потому что по сравнению с использованием Keep / Drop проще использовать более простой путь кода.

Чтобы сделать API немного более дружелюбным, я думаю, что лучшим вариантом было бы заставить замыкание возвращать () и передавать ему вспомогательный объект (который я буду называть здесь «обновителем»), который можно используется для проверки каждого элемента Vec и контроля того, что с ним происходит. Эти методы могут иметь знакомые имена, такие как borrow , borrow_mut и take , с дополнительными методами, такими как keep_next(n) или drop_remainder() . Используя этот тип API, закрытие становится намного проще в простых случаях и не сложнее в сложных случаях. Благодаря тому, что большинство методов средства обновления принимают self по значению, легко помешать вызывающей стороне делать такие вещи, как вызов take более одного раза или давать противоречивые инструкции о том, что делать в последующих итерациях.

Но мы все еще можем сделать лучше! Сегодня утром я понял, что, как это часто бывает, эта проблема аналогична той, которая была окончательно решена в функциональных языках, и мы можем решить ее с помощью аналогичного решения. Я говорю об API-интерфейсах «застежки-молнии», впервые описанных в этой короткой статье с примером кода на OCaml и описанных здесь с кодом Haskell и ссылками на другие соответствующие документы. Застежки-молнии предоставляют очень общий способ обхода структуры данных и обновления ее «на месте», используя любые операции, которые поддерживает эта конкретная структура данных. Другой способ думать об этом состоит в том, что застежка-молния — это своего рода итератор с турбонаддувом с дополнительными методами для выполнения операций над определенным типом структуры данных.

В Haskell вы получаете семантику «на месте», превращая молнию в монаду; в Rust вы можете сделать то же самое, используя время жизни, если застежка-молния содержит ссылку mut на Vec . Застежка-молния для Vec очень похожа на средство обновления, которое я описал выше, за исключением того, что вместо того, чтобы повторно передавать ее в замыкание, Vec просто предоставляет метод для создания застежки-молнии на самом себе, и молния имеет эксклюзивный доступ к Vec , пока она существует. Затем вызывающая сторона несет ответственность за написание цикла для обхода массива, вызывая метод на каждом этапе либо для удаления текущего элемента из Vec , либо для того, чтобы оставить его на месте. Досрочное завершение можно реализовать, вызвав метод, использующий застежку-молнию. Поскольку цикл находится под контролем вызывающей стороны, становится возможным выполнять такие действия, как обработка более одного элемента в каждой итерации цикла или обработка фиксированного числа элементов без использования цикла вообще.

Вот очень надуманный пример, показывающий, на что способна молния:

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

Для сравнения, вот более или менее та же функция, использующая drain_filter , за исключением того, что она только делает вид, что останавливается раньше. Это примерно такой же объем кода, но ИМХО его намного сложнее читать, потому что значение значения, возвращаемого замыканием, неочевидно, и он использует изменяемые логические флаги для переноса информации от одной итерации к другой, где застежка-молния version достигает того же самого с потоком управления. Поскольку удаленные элементы всегда возвращаются итератором, нам нужен отдельный шаг фильтра для удаления отрицательных значений из вывода, что означает, что нам нужно проверять отрицательные значения в двух местах вместо одного. Это также немного уродливо, что он должен отслеживать позицию в v ; реализация drain_filter имеет эту информацию, но вызывающая сторона не имеет к ней доступа.

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 Ваш предыдущий пост напомнил мне о ящике scanmut , в частности о структуре Remover , и упомянутая вами концепция «застежки-молнии» кажется очень похожей! Это кажется намного более эргономичным, чем метод, который требует закрытия, когда вам нужен полный контроль.

В любом случае, это, вероятно, не очень важно для стабилизации drain_filter , поскольку мы всегда можем заменить внутренние компоненты позже. drain_filter сам по себе всегда будет очень полезен из-за того, насколько он удобен. Единственное изменение, которое я все еще хотел бы видеть перед стабилизацией, — это параметр RangeBounds .

@timvermeulen Я думаю, имеет смысл добавить параметр RangeBounds , но сохранить текущую подпись закрытия ( F: FnMut(&mut T) -> bool ).
Вы всегда можете выполнить постобработку осушенных элементов с помощью filter_map или чего угодно.
(Для меня очень важно, что замыкание позволяет мутировать элемент, потому что retain этого не позволяет (оно было стабилизировано до того, как была обнаружена эта ошибка).)

Да, это идеальный баланс между удобством и полезностью.

@timvermeulen Да, я довольно далеко отклонился от основной темы.

Я заметил одну вещь, имеющую отношение к исходной теме: довольно сложно вспомнить, что означает возвращаемое значение замыкания — говорит ли оно о том, следует ли сохранить элемент или удалить его? Я думаю, что в документах было бы полезно указать, что v.drain_filter(p) эквивалентно v.iter().filter(p) с побочными эффектами.

С filter использование логического значения по-прежнему далеко от идеального для ясности, но это очень известная функция, и IMHO, по крайней мере, несколько интуитивно понятно, что предикат отвечает на вопрос «должен ли я сохранить это?» а не "должен ли я отказаться от этого?" С drain_filter та же логика применяется, если вы думаете об этом с точки зрения итератора, но если вы думаете об этом с точки зрения ввода Vec , вопрос становится «следует ли НЕ держи это?"

Что касается точной формулировки, то предлагаю переименовать параметр filter в predicate (чтобы соответствовало Iterator::filter ) и добавить где-нибудь в описание следующее предложение:

Чтобы запомнить, как используется возвращаемое значение predicate , полезно иметь в виду, что drain_filter идентично Iterator::filter с дополнительным побочным эффектом удаления выбранного предметы от self .

@ johnw42 Да, хорошая мысль. Я думаю, имя вроде drain_where было бы намного яснее.

Если вы собираетесь назвать байкшединг; пожалуйста , убедитесь, что вы прочитали все комментарии; даже скрытые. Уже было предложено много вариантов, например https://github.com/rust-lang/rust/issues/43244#issuecomment -331559537.

Но… он должен называться draintain() ! Нет другого такого красивого имени!

Меня очень интересует этот вопрос, и я прочитал всю ветку, поэтому я мог бы также попытаться обобщить то, что все сказали, в надежде помочь стабилизировать ситуацию. По пути я добавил несколько собственных комментариев, но постарался сделать их как можно более нейтральными.

Именование

Вот непредвзятое резюме имен, которые я видел предложенными:

  • drain_filter : имя, используемое в текущей реализации. Соответствует другим именам, таким как filter_map . Преимущество в том, что он аналогичен drain().filter() , но с большим количеством побочных эффектов.
  • drain_where : полезно указать, приводит ли true к сливу _out_ или фильтрации _in_, что может быть трудно запомнить с другими именами. В std нет прецедента для суффикса _where , но есть много прецедентов для подобных суффиксов.
  • Вариант drain().where() , поскольку where уже является ключевым словом.
  • drain_retain : соответствует retain , но retain и drain имеют противоположные интерпретации логических значений, возвращаемых замыканием, что может сбивать с толку.
  • filtered_drain
  • drain_if
  • drain_when
  • remove_if

Параметры

Возможно, стоит добавить аргумент диапазона для согласованности с drain .

Было предложено два формата закрытия: FnMut(&mut T) -> bool и FnMut(T) -> Result<T, U> . Последний более гибкий, но и более неуклюжий.

Обсуждалось обращение логического условия ( true означает «держать в Vec ») для согласования с retain , но тогда оно не соответствовало бы drain ( true означает "слить из Vec ").

Раскручивание

При аварийном закрытии фильтра итератор DrainFilter отбрасывается. Затем итератор должен закончить опустошение Vec , но для этого он должен снова вызвать закрытие фильтра, рискуя получить двойную панику. Есть несколько решений, но все они компромиссы:

  • Не заканчивайте слив на падении. Это довольно нелогично при использовании таких адаптеров, как find или all . Кроме того, это делает идиому v.drain_filter(...); бесполезной, поскольку итераторы ленивы.

  • Всегда заканчивайте слив при падении. Это рискует двойной паникой (которая приводит к прерыванию), но делает поведение последовательным.

  • Завершайте слив только при падении, если в настоящее время не разматываете. Это полностью устраняет двойную панику, но делает поведение drain_filter непредсказуемым: удаление DrainFilter в деструкторе может _иногда_ не выполнять свою работу.

  • Заканчивайте слив при падении только в том случае, если закрытие фильтра не захлопнулось. Это текущий компромисс, сделанный drain_filter . Приятным свойством этого подхода является то, что паника при закрытии фильтра «короткое замыкание», что, возможно, вполне интуитивно понятно.

Обратите внимание, что текущая реализация надежна и никогда не дает утечек, пока структура DrainFilter удалена (хотя это может привести к аварийному завершению). Однако предыдущие реализации не были безопасными и защищенными от утечек.

Слив по капле

DrainIter может либо завершить очистку исходного вектора при его удалении, либо только при вызове next (ленивая итерация).

Аргументы в пользу слива по капле:

  • В соответствии с поведением drain .

  • Хорошо взаимодействует с другими адаптерами, такими как all , any , find и т. д.

  • Включает идиому vec.drain_filter(...); .

  • Ленивая функциональность может быть явно включена с помощью методов в стиле drain_lazy или адаптера lazy() на DrainIter (и даже на Drain , так как он обратно совместим с добавить методы).

Аргументы в пользу ленивой итерации:

  • Совместим почти со всеми другими итераторами.

  • Функциональность «слива при сбросе» может быть явно включена через адаптеры на DrainIter или даже через общий адаптер Iterator::exhausting (см. RFC #2370 ).

Возможно, я что-то пропустил, но, по крайней мере, надеюсь, что это поможет новичкам при беглом просмотре ветки.

@негамартин

Разве опция drain-on-drop не требует, чтобы итератор возвращал ссылку на элемент вместо собственного значения? Я думаю, что это сделало бы невозможным использование drain_filter в качестве механизма для удаления и получения права собственности на элементы, соответствующие определенному условию (что было моим первоначальным вариантом использования).

Я так не думаю, поскольку поведение текущей реализации точно такое же, как слив-при-падении с получением собственных значений. В любом случае, я не понимаю, как метод «слива-на-капля» потребует заимствования элементов, поэтому я думаю, что у нас есть два разных взгляда на то, что означает «слив-на-кап».

Просто для ясности: когда я говорю «слив при сбросе», я имею в виду только поведение, когда итератор не полностью используется: должны ли все элементы, соответствующие замыканию, сбрасываться, даже если итератор не полностью используется? Или только до того элемента, который был израсходован, а остальные остались нетронутыми?

В частности, это разница между:

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]);

Просто выбрасываю идею, но другим возможным API может быть что-то вроде:

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

Он менее гибкий, чем другие варианты, но позволяет избежать проблемы, связанной с тем, что делать, когда DrainFilter отбрасывается.

Мне кажется, что все это все меньше и меньше похоже retain_mut() ( retain() с изменяемой ссылкой, переданной в замыкание), что и было в первую очередь предназначено для обеспечения . Можем ли мы сейчас предоставить retain_mut() в дополнение к работе над дизайном дренажной системы с фильтром? Или я что-то упускаю?

@БартМэсси

это то, что он был в первую очередь предназначен для обеспечения.

Я не думаю, что это так. Я специально использую drain_filter , чтобы стать владельцем элементов на основе критериев фильтрации. Drain и DrainFilter дают элемент, а as - нет.

@негамартин

Просто для ясности: когда я говорю «слив при сбросе», я имею в виду только поведение, когда итератор не полностью используется.

Ах хорошо. Это моя ошибка. Я неправильно понял ваше определение. Я интерпретировал это как «ничего не удаляется из vec до тех пор, пока не будет удалено», что на самом деле не имеет никакого смысла.

Аргументы в пользу ленивой итерации

Я думаю, что это должно соответствовать drain . RFC Iterator::exhausting не был принят, и было бы очень странно, если бы drain и drain_filter имели, казалось бы, противоположное поведение слива.

@негамартин

сток_фильтр: имя, используемое в текущей реализации. Соответствует другим именам, таким как filter_map. Преимущество в том, что он аналогичен drain().filter() , но с большим количеством побочных эффектов.

Это не аналог (поэтому нам нужны retain_mut / drain_filter ):
drain().filter() истощает даже те элементы, для которых закрытие фильтра возвращает false !

Я только что заметил небольшую строчку в комментарии команды lib в #RFC 2870 :

Возможности могут включать «перегрузку» метода, сделав его универсальным или шаблоном построителя.

Можно ли сделать метод универсальным, если он по-прежнему принимает предыдущий конкретный тип? Если да, то я считаю, что это был бы лучший путь вперед.

(Шаблон построителя немного неинтуитивен с итераторами, поскольку методы на итераторах обычно являются адаптерами, а не изменяют поведение. Кроме того, нет прецедентов, например, chunks и chunks_exact — это два отдельных метода. , а не комбо chunks().exact() .)

Нет, не с текущим дизайном, насколько я знаю, как вывод типа, который
работало раньше, теперь может дать сбой из-за неоднозначности типа. Генетика по умолчанию
типы для функций могли бы помочь, но их очень сложно сделать правильно.

В пт, 12 июня 2020 г., 21:21 [email protected] написал:

Я только что заметил небольшую строчку в комментарии команды lib в #RFC 2870.
https://github.com/rust-lang/rfcs/pull/2369 :

Возможности могут включать «перегрузку» метода, сделав его универсальным,
или шаблон строителя.

Является ли обратно совместимым сделать метод универсальным, если он все еще принимает
предыдущий тип бетона? Если да, то я считаю, что это был бы лучший способ
вперед.

(Шаблон построителя немного неинтуитивен с итераторами, так как методы на
итераторы обычно являются адаптерами, а не средствами изменения поведения. Кроме того, есть
нет прецедентов, например, chunks и chunks_exact - это два отдельных
методы, а не комбинация chunks().exact().)


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/43244#issuecomment-643444213 ,
или отписаться
https://github.com/notifications/unsubscribe-auth/AB2HJELPWXNXJMX2ZDA6F63RWJ53FANCNFSM4DTDLGPA
.

Можно ли сделать метод универсальным, если он по-прежнему принимает предыдущий конкретный тип? Если да, то я считаю, что это был бы лучший путь вперед.

Нет, так как в некоторых случаях это нарушает вывод типа. Например, foo.method(bar.into()) будет работать с аргументом конкретного типа, но не с общим аргументом.

Я думаю, что drop_filter в том виде, в каком он реализован сейчас, очень полезен. Можно ли его стабилизировать как есть? Если в будущем будут обнаружены более совершенные абстракции, ничто не мешает их внедрению.

Какой процесс мне нужно инициировать, чтобы попытаться добавить retain_mut() , независимо от того, что произойдет с drain_filter() ? Мне кажется, что требования разошлись, и что retain_mut() все равно было бы полезно иметь вне зависимости от того, что происходит с drain_filter() .

@BartMassey для новых нестабильных API-интерфейсов библиотек, я думаю, что создание PR с реализацией должно быть в порядке. На https://rustc-dev-guide.rust-lang.org/implementing_new_features.html есть инструкции по реализации этой функции и на https://rustc-dev-guide.rust-lang.org/getting-started.html .

Сегодня я боролся с различиями API между HashMap и BTreeMap , и я просто хотел поделиться предостережением, которое я считаю важным, чтобы различные коллекции стремились поддерживать согласованный API всякий раз, когда вносятся изменения. смысле, что-то, что на данный момент не всегда так.

Например, String, Vec, HashMap, HashSet, BinaryHeap и VecDeque имеют метод retain , а LinkedList и BTreeMap — нет. Я нахожу это особенно странным, поскольку retain кажется более естественным методом для LinkedList или Map, чем для векторов, где случайное удаление является очень дорогой операцией.

И когда вы копаете немного глубже, это еще больше сбивает с толку: замыкание HashMap::retain получает значение в изменяемой ссылке, но другие коллекции получают неизменяемую ссылку (а String получает простой char ).

Теперь я вижу, что добавляются новые API, такие как drain_filter , которые 1/ кажется перекрываются с retain и 2/ не стабилизируются для всех коллекций одновременно:

  • HashMap::drain_filter находится в исходном репозитории, но еще не поставляется с Rust std AFAIK (он не отображается в документации)
  • BTreeMap::drain_filter , Vec::drain_filter , LinkedList::drain_filter находятся в стандартной версии Rust, но функция ограничена
  • VecDeque::drain_filter вообще не существует, его нет в документах
  • String::drain_filter тоже не существует

У меня нет четкого мнения о том, как лучше всего реализовать эти функции, или о том, нужны ли нам drain_filter , retain или и то, и другое, но я очень твердо верю, что эти API должны оставаться едиными для всех коллекций. .

И, что еще более важно, похожие методы разных коллекций должны иметь одинаковую семантику. Что-то, что текущие реализации retain нарушают IMO.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги