Является ли try_fold предпочтительным способом остановить бесконечную итерацию или существуют более идиоматические альтернативы?

#rust #iteration

Вопрос:

Я ищу способы прервать итерацию бесконечного итератора. Я обнаружил, что try_fold это соответствует моей цели. Однако для этого необходимо сделать неловкую вещь-вернуться Err к успешному делу. Что я хочу понять, так это то, является ли это идиоматическим способом делать вещи. Единственный другой способ , который я могу придумать для этого, — использовать обычное for или что-то вроде find сохранения внешнего состояния (что кажется еще более странным!). Я знаю , что в клоджуре есть reduced , но я не смог найти эквивалент для ржавчины.

Вот минимальный жизнеспособный пример. Пример циклически обходит начальный Vec, суммируя каждый элемент по ходу, и останавливается на первой сумме, превышающей 10. Т. е. он возвращает 12, потому что 1 5 - 3 1 5 - 3 1 5 = 12 :

 fn main() {
    let seed = vec![1, 5, -3];

    let res = seed.iter().cycle().try_fold(0, |accum, value| {
        let next = accum   value;
        if next > 10 {
            Err(next)
        } else {
            Ok(next)
        }
    });
    if let Err(res) = res {
        println!("{:?}", res);
    } else {
        unreachable!();
    }
}
 

(ссылка на игровую площадку)

Часть, которая кажется мне странной if let Err(res) = res , — это то, что она является положительным условием и действительно единственным выходом из цикла (следовательно, почему другая ветвь недоступна).

Есть ли «лучший» способ?

Комментарии:

1. Я бы посоветовал просто не делать этого. Лучше поговорите о своей конкретной проблеме.

Ответ №1:

Просто, просто повторите обычную итерацию

 fn main() {
    let seed = vec![1, 5, -3];
    let mut accum = 0;
    for value in seed.iter().cycle() {
        accum  = value;
        if accum > 10 {
            break;
        }
    }
    println!("{:?}", accum)
}
 

Ответ №2:

Вы можете использовать scan и find :

 fn main() {
    let seed = vec![1, 5, -3];
    let res = seed
        .iter()
        .cycle()
        .scan(0, |accum, value| {
            *accum  = value;
            Some(*accum)
        })
        .find(|amp;x| x > 10)
        .unwrap();
    println!("{:?}", res);
}
 

На самом деле это немного короче , чем использование fold_while from itertools , которое выглядело бы так:

 use itertools::{FoldWhile::{Continue, Done}, Itertools};

fn main() {
    let seed = vec![1, 5, -3];
    let res = seed
        .iter()
        .cycle()
        .fold_while(0, |accum, value| {
            let next = accum   value;
            if next > 10 {
                Done(next)
            } else {
                Continue(next)
            }
        })
        .into_inner();
    println!("{:?}", res);
}
 

Комментарии:

1. О, я не знал об этом! Часть о том, чтобы иметь внутреннее состояние и подвергать внешнему итератору разные вещи, немного расплавляет мой мозг. Но я определенно изучу его дальше, чтобы немного лучше понять его семантику, спасибо!

Ответ №3:

Рассмотрите возможность использования itertools::fold_while, если вы не возражаете против использования хорошо зарекомендовавшей себя внешней библиотеки. Могут быть и другие полезные расширения, если вам нравится этот стиль программирования.

Комментарии:

1. Интересно, я не знал об итер-инструментах! fold_while действительно выглядит гораздо лучше подходящим для этой цели, чем мое странное решение try_fold . Спасибо!

2. Я также признаю, что, поскольку itertools не является частью stdlib, это, вероятно, не «идиоматично», и for решение, предложенное в другом ответе, было бы более распространенным способом сделать это. Я приму этот ответ, потому что он научил меня новой библиотеке, но принятие другого, вероятно, было бы столь же адекватным.

3. @jgpaiva В Rust использование внешних ящиков является нормой, и я бы даже сказал, что использование популярных ящиков идиоматично, если это делает ваш код чище.

Ответ №4:

На сегодняшний день нет очевидного ответа на ваш вопрос, что на самом деле очень удивительно. Как предположил @peter-hall, scan это идиоматический функциональный ответ на проблему, но scan известно , что Rust некрасив в использовании.

На мой взгляд try_fold , остается самый ржавый вариант. Вот мои комбинированные предложения, чтобы сделать петлевые складки более удовлетворительными (выберите одно или несколько изменений!) :

 let res = {
    use Result::{Err as Break, Ok as Next};
    seed.iter()
        .cycle()
        .try_fold(0, |accum, value| match accum   value {
            res if res > 10 => Break(res),
            next => Next(next),
        })
        .unwrap_err()
};
 

Использование a match помогает называть случаи лучше, чем if / else на мой взгляд. Псевдонимы Result типов также дают некоторую семантику, описывающую разрыв отступа.

Если вы не хотите использовать unwrap_err , кто-то предложил заменить его на

 let res = iter.try_fold(...).unwrap_or_else(|res| res);
 

Также можно использовать неопровержимый шаблон:

 let (Ok(res) | Err(res)) = iter.try_fold(...);
 

Если бы это было для моего собственного кода, я бы сделал все как можно проще и ванильнее:

 let res = seed
    .iter()
    .cycle()
    .try_fold(0, |accum, value| match accum   value {
        res if res > 10 => Err(res),
        acc => Ok(acc),
    })
    .unwrap_err();
 

Чтение .unwrap_err() достаточно ясно для меня, потому что это означает, что Err тип возврата, который мы ожидаем. Это мой лучший выстрел, ура!