Wie stoppe ich die Iteration und gebe einen Fehler zurück, wenn Iterator :: map ein Ergebnis :: Err zurückgibt?

76

Ich habe eine Funktion, die Folgendes zurückgibt Result:

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

Dann ein anderer, der es so benutzt:

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

Wie gehe ich mit dem Fehlerfall in einer der mapIterationen um?

Ich weiß, ich könnte verwenden flat_mapund in diesem Fall würden die Fehlerergebnisse ignoriert :

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

ResultDer Iterator hat je nach Erfolgsstatus entweder 0 oder 1 Elemente und flat_mapfiltert ihn heraus, wenn er 0 ist.

Ich möchte jedoch keine Fehler ignorieren , sondern den gesamten Codeblock stoppen und einen neuen Fehler zurückgeben (basierend auf dem Fehler, der in der Karte aufgetreten ist, oder den vorhandenen Fehler einfach weiterleiten).

Wie gehe ich in Rust am besten damit um?

Kai Sellgren
quelle

Antworten:

109

Result implementiertFromIterator , so dass Sie die ResultAußenseite verschieben können und Iteratoren sich um den Rest kümmern (einschließlich des Stoppens der Iteration, wenn ein Fehler gefunden wird).

#[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);
}

Spielplatz

BurntSushi5
quelle
7
+1 Das ist großartig! (Das Beispiel aus meiner Antwort portiert auf diese: is.gd/E26iv9 )
Dogbert
1
@KaiSellgren Ja, Sie können denselben Trick anwenden. Der Schlüssel befindet sich in der Typensignatur von collect, die für den Rückgabetyp, der implementiert werden muss, polymorph ist FromIterator. Ich weiß nicht, was Sie unter "kann es breiter angewendet werden" verstehen. Rust unterstützt polymorphe Rückgabetypen ... Also ja? ( Weitere Beispiele für Polymorphismus vom Rückgabetyp finden Sie unter Rngund DefaultMerkmale.)
BurntSushi5
3
@KaiSellgren from_iterwird in der collectMethode aufgerufen .
BurntSushi5
1
Für die Verwendung von collect()muss der Iterator endlich sein, richtig? Wenn ja, wie würde ein ähnlicher, aber unendlicher Iterator behandelt?
U007D
1
Was würden Sie bei mehreren map()s tun ? Wenn der erste a map()zurückgibt Result, muss der folgende map()auch a akzeptieren, Resultwas ärgerlich sein kann. Gibt es eine Möglichkeit, dasselbe von der Mitte der map()Kette aus zu erreichen? .map(...).collect<Result<Vec<_>, _>>()?.into_iter().map(...)Natürlich nicht nur zu tun .
Gute Nacht Nerd Pride
2

Die akzeptierte Antwort zeigt, wie Sie beim Sammeln auf Fehler verzichten können , und das ist in Ordnung, da das OP dies angefordert hat. Wenn Sie eine Verarbeitung benötigen, die auch mit großen oder unendlich fehlbaren Iteratoren funktioniert, lesen Sie weiter.

Wie bereits erwähnt, forkann es verwendet werden, um Stop-on-Error zu emulieren, aber das ist manchmal unelegant, wie wenn Sie aufrufen möchten max()oder eine andere konsumierende Methode. In anderen Situationen ist es fast unmöglich, als wenn die aufwändige Methode ist in einer anderen Kiste, wie itertoolsoder Rayon 1 .

Iterator Verbraucher: try_for_each

Wenn Sie steuern, wie der Iterator verwendet wird, können Sie nur den try_for_eachersten Fehler stoppen. Es wird ein Ergebnis zurückgegeben, das, Okwenn kein Fehler aufgetreten ist, Errandernfalls den Fehlerwert enthält:

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(())
}

Wenn Sie den Status zwischen den Aufrufen des Abschlusses beibehalten müssen, können Sie auch verwenden try_fold. Beide Methoden werden von implementiert ParallelIterator, sodass Sie sie mit Rayon verwenden können.

Für diesen Ansatz müssen Sie steuern, wie der Iterator verwendet wird. Wenn dies durch Code erfolgt, der nicht unter Ihrer Kontrolle steht - wenn Sie beispielsweise den Iterator an itertools::merge()oder einen ähnlichen Code übergeben , benötigen Sie einen Adapter.

Iteratoradapter: scan

Der erste Versuch, bei einem Fehler anzuhalten, besteht darin, Folgendes zu verwenden 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(())
}

Dies funktioniert, aber wir erhalten keinen Hinweis darauf, dass ein Fehler aufgetreten ist. Die Iteration wird nur stillschweigend gestoppt. Es erfordert auch das Unansehnliche, map(Result::unwrap)was den Anschein erweckt, als würde das Programm bei einem Fehler in Panik geraten, was in der Tat nicht der Fall ist, wenn wir bei einem Fehler aufhören.

Beide Probleme können durch Umschalten von take_whileauf behoben werden. Dies scanist ein leistungsfähigerer Kombinator, der nicht nur das Stoppen der Iteration unterstützt, sondern auch die Elemente zurückruft, die dem Rückruf gehören, sodass der Abschluss den Fehler an den Aufrufer extrahieren kann:

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(())
}

Bei Bedarf an mehreren Stellen kann der Verschluss in eine Utility-Funktion abstrahiert werden:

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

... in diesem Fall können wir es als .scan(&mut err, until_err)( Spielplatz ) aufrufen .

Diese Beispiele erschöpfen den Iterator trivial for_each(), aber man kann ihn mit willkürlichen Manipulationen, einschließlich der von Rayon, verketten par_bridge(). Die Verwendung scan()ist sogar möglich, um collect()die Elemente in einen Container zu verschieben und auf die Elemente zuzugreifen, die vor dem Fehler angezeigt wurden Result<Container, Error>. Dies ist manchmal nützlich und beim Sammeln in nicht verfügbar .


1 Wenn Sie Rayon verwenden, um Streaming-Daten parallel zu verarbeiten, müssen Sie par_bridge () verwenden:
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)
}

Auch hier kann ein gleichwertiger Effekt nicht trivial durch Sammeln in erzielt werden Result.

user4815162342
quelle
wie wenn Sie sum()die OkElemente [...] möchten - das ist bereits in der Standardbibliothek implementiert und verwendet dieselbe Technik wie die process_resultsMethode in itertools.
Shepmaster
@ Shepmaster Ich wusste nichts davon process_results(), danke. Der Vorteil ist, dass keine separate Fehlervariable erforderlich ist. Die Nachteile sind, dass es nur als Funktion der obersten Ebene verfügbar ist, die Sie aufruft (mögliches Problem, wenn mehrere Dinge parallel durchlaufen werden), und dass eine externe Kiste erforderlich ist. Der Code in dieser Antwort ist relativ kurz, funktioniert mit stdlib und nimmt an der Verkettung von Iteratoren teil.
user4815162342
1

Diese Antwort bezieht sich auf eine Version vor 1.0 von Rust und die erforderlichen Funktionen wurden entfernt

Sie können hierfür die std::result::foldFunktion verwenden. Es hört auf zu iterieren, nachdem es auf das erste gestoßen ist Err.

Ein Beispielprogramm, das ich gerade geschrieben habe:

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

Ausgabe:

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

Demo

Dogbert
quelle