Як зупинити ітерацію та повернути помилку, коли Iterator :: map повертає Result :: Err?


84

У мене є функція, яка повертає Result:

fn find(id: &Id) -> Result<Item, ItemError> {
    // ...
}

Потім інший використовує його так:

let parent_items: Vec<Item> = parent_ids.iter()
    .map(|id| find(id).unwrap())
    .collect();

Як я розглядаю випадки збою всередині будь-якої з mapітерацій?

Я знаю, що міг би використовувати, flat_mapі в цьому випадку результати помилок будуть ігноруватися :

let parent_items: Vec<Item> = parent_ids.iter()
    .flat_map(|id| find(id).into_iter())
    .collect();

ResultІтератор має 0 або 1 елемент, залежно від стану успіху, і flat_mapфільтрує його, якщо 0.

Однак я не хочу ігнорувати помилки, я хочу замість цього зробити весь блок коду просто зупиненим і повернути нову помилку (на основі помилки, яка виникла на карті, або просто переслати наявну помилку).

Як мені найкраще впоратися з цим у Rust?

Відповіді:


115

Result інвентарFromIterator , так що ви можете переміщати Resultзовні і ітератори подбає про решту (включаючи зупинки ітерації , якщо знайдена помилка).

#[derive(Debug)]
struct Item;
type Id = String;

fn find(id: &Id) -> Result<Item, String> {
    Err(format!("Not found: {:?}", id))
}

fn main() {
    let s = |s: &str| s.to_string();
    let ids = vec![s("1"), s("2"), s("3")];

    let items: Result<Vec<_>, _> = ids.iter().map(find).collect();
    println!("Result: {:?}", items);
}

Дитячий майданчик


8
+1 Це чудово! (Приклад з моєї відповіді перенесено до цього: is.gd/E26iv9 )
Догберт

1
@KaiSellgren Так, ви можете застосувати цей самий фокус. Ключ знаходиться у підписі типу collect, який є поліморфним щодо типу повернення, який повинен бути реалізований FromIterator. Я не знаю, що ви маєте на увазі під "чи можна застосовувати це ширше". Іржа підтримує поліморфні типи повернення ... Так, так? (Див. RngІ Defaultознаки для отримання додаткових прикладів поліморфізму зворотного типу.)
BurntSushi5,

3
У методі from_iterвикликається @KaiSellgren collect.
BurntSushi5,

1
Використання collect()вимагає, щоб ітератор був скінченним, правильно? Якщо так, то як слід поводитися з подібним, але нескінченним ітератором?
U007D

1
Що ви зробите у випадку кількох map()s? Якщо перший map()повертає a Result, то наступний map()також повинен прийняти і a Result, що може дратувати. Чи є спосіб досягти цього з середини map()ланцюга? Коротко просто робити .map(...).collect<Result<Vec<_>, _>>()?.into_iter().map(...), звичайно.
Good Night Nerd Pride

6

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

Як уже зазначалося, forможе використовуватися для емуляції зупинки при помилці, але це іноді неелегантно, як коли ви хочете зателефонувати max()або іншим споживаючим способом. В інших ситуаціях це майже неможливо, як, наприклад, коли метод споживання знаходиться в іншому ящику, наприклад, itertoolsабо Rayon 1 .

Споживач ітератора: try_for_each

Коли ви контролюєте, як ітератор споживається, ви можете просто використовувати, try_for_eachщоб зупинити першу помилку. Він поверне результат, який, Okякщо помилки не було, а в Errіншому випадку містить значення помилки:

use std::{io, fs};

fn main() -> io::Result<()> {
    fs::read_dir("/")?
        .take_while(Result::is_ok)
        .map(Result::unwrap)
        .try_for_each(|e| -> io::Result<()> {
            println!("{}", e.path().display());
            Ok(())
        })?;
    // ...
    Ok(())
}

Якщо вам потрібно підтримувати стан між викликами закриття, ви можете також використовувати try_fold. Обидва методи реалізовані ParallelIterator, тому ви можете використовувати їх із Rayon.

Цей підхід вимагає контролювати споживання ітератора. Якщо це робиться за допомогою коду, який не знаходиться під вашим контролем - наприклад, якщо ви передаєте ітератор itertools::merge()або тому подібне, вам знадобиться адаптер.

Адаптер ітератора: scan

Перша спроба зупинити помилку полягає у використанні take_while:

use std::{io, fs};

fn main() -> io::Result<()> {
    fs::read_dir("/")?
        .take_while(Result::is_ok)
        .map(Result::unwrap)
        .for_each(|e| println!("{}", e.path().display()));
    // ...
    Ok(())
}

Це працює, але ми не отримуємо жодних ознак того, що сталася помилка, ітерація просто мовчки зупиняється. Крім того, для цього потрібна неприваблива річ, через map(Result::unwrap)яку здається, що програма буде панікувати через помилку, що насправді не так, оскільки ми зупиняємось на помилці.

Обидві проблеми можна виправити, переключившись з take_whileна scan, більш потужний комбінатор, який не тільки підтримує зупинку ітерації, але передає свої елементи, що належать зворотному виклику, дозволяючи закриттю витягнути помилку абоненту:

fn main() -> io::Result<()> {
    let mut err = Ok(());
    fs::read_dir("/")?
        .scan(&mut err, |err, res| match res {
            Ok(o) => Some(o),
            Err(e) => {
                **err = Err(e);
                None
            }
        })
        .for_each(|e| println!("{}", e.path().display()));
    err?;
    // ...
    Ok(())
}

Якщо це потрібно в кількох місцях, закриття можна абстрагувати до функції утиліти:

fn until_err<T, E>(err: &mut &mut Result<(), E>, item: Result<T, E>) -> Option<T> {
    match item {
        Ok(item) => Some(item),
        Err(e) => {
            **err = Err(e);
            None
        }
    }
}

... в цьому випадку ми можемо викликати це як .scan(&mut err, until_err)( дитячий майданчик ).

Ці приклади тривіально виснажують ітератор for_each(), але його можна зв'язати довільними маніпуляціями, включаючи район par_bridge(). Використовуючи scan()його, можна навіть переносити collect()елементи в контейнер і мати доступ до елементів, побачених до помилки, що іноді є корисним і недоступним під час збирання в Result<Container, Error>.


1 Потрібно використовувати `par_bridge ()` при використанні Rayon для паралельної обробки потокових даних:
fn process(input: impl BufRead + Send) -> std::Result<Output, Error> {
    let mut err = Ok(());
    let output = lines
        .input()
        .scan(&mut err, until_err)
        .par_bridge()
        .map(|line| ... executed in parallel ... )
        .reduce(|item| ... also executed in parallel ...);
    err?;
    ...
    Ok(output)
}

Знову ж таки, еквівалентний ефект неможливо тривіально досягти, збираючи в Result.



як коли ви хочете sum()[...] Okоб'єкти - це вже реалізовано в стандартній бібліотеці, використовуючи ту саму техніку, що і process_resultsметод в itertools.
Шепмастер

@Shepmaster, про якого я не знав process_results(), дякую. Його перевагою є те, що вона не вимагає окремої змінної помилки. Його мінуси полягають у тому, що вона доступна лише як функція верхнього рівня, яка викликає вас (можлива проблема при ітерації кількох речей паралельно), і що для неї потрібен зовнішній ящик. Код у цій відповіді досить короткий, працює зі stdlib та бере участь у ланцюжку ітераторів.
користувач4815162342

1

Ця відповідь стосується версії Rust до 1.0 і необхідні функції були видалені

Для цього можна використовувати std::result::foldфункцію. Він перестає повторюватися після зустрічі з першим Err.

Приклад програми, яку я щойно написав:

fn main() {
  println!("{}", go([1, 2, 3]));
  println!("{}", go([1, -2, 3]));
}

fn go(v: &[int]) -> Result<Vec<int>, String> {
    std::result::fold(
        v.iter().map(|&n| is_positive(n)),
        vec![],
        |mut v, e| {
            v.push(e);
            v
        })
}

fn is_positive(n: int) -> Result<int, String> {
    if n > 0 {
        Ok(n)
    } else {
        Err(format!("{} is not positive!", n))
    }
}

Вихід:

Ok([1, 2, 3])
Err(-2 is not positive!)

Демо

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.