Rust: Tracking-Problem für `ops::Try` (`try_trait`-Funktion)

Erstellt am 31. Mai 2017  ·  99Kommentare  ·  Quelle: rust-lang/rust

Das Try Merkmal von https://github.com/rust-lang/rfcs/pull/1859; implementiert in PR https://github.com/rust-lang/rust/pull/42275.

Aus Gründen der Übersichtlichkeit von https://github.com/rust-lang/rust/issues/31436 abtrennen (gemäß https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)

  • [ ] Wenn Sie dies stabilisieren, können Sie Iterator::try_fold implementieren

    • [ ] Öffnen Sie als Teil der Stabilisierung PR #62606 erneut, um die Implementierung von try_fold für Iteratoren zu dokumentieren

    • [ ] Stellen Sie sicher, dass die Standardimplementierungen anderer Dinge den gewünschten langfristigen DAG haben, da eine spätere Änderung praktisch unmöglich ist. (Insbesondere wäre es schön, fold in Form von try_fold implementiert zu haben, damit beide nicht überschrieben werden müssen.)

A-error-handling B-RFC-implemented B-unstable C-tracking-issue Libs-Tracked T-lang T-libs

Hilfreichster Kommentar

Wie ist der aktuelle Status dieser Funktion?

Alle 99 Kommentare

Ein paar Fahrradschuppen:

  • Haben wir eine besondere Motivation, den zugehörigen Typ Error statt Err ? Wenn man es Err nennt, würde es mit Result : das andere heißt bereits Ok .

  • Haben wir eine besondere Motivation dafür, separate from_error und from_ok Methoden zu haben, anstelle einer einzigen from_result die symmetrischer mit into_result die die andere ist? die Hälfte der Eigenschaft?

( aktualisierte Version des Playground-Links aus dem RFC )

@glaebhörl

  • Error vs Err wurde im Zusammenhang mit TryFrom in https://github.com/rust-lang/rust/issues/33417#issuecomment -269108968 und https:// diskutiert.
  • Ich glaube, sie sind getrennt, weil sie unterschiedliche Verwendungszwecke haben, und ich gehe davon aus, dass es selten vorkommt, dass jemand tatsächlich ein Result , das er in ein T:Try . Ich bevorzuge Try::from_ok und Try::from_error als immer Try::from_result(Ok( und Try::from_result(Err( zu rufen, und ich bin glücklich, die beiden Methoden einfach zu implementieren, anstatt die Übereinstimmung auszuschreiben. Vielleicht liegt das daran, dass ich mir into_result nicht als Into<Result> , sondern als "War es bestanden oder nicht bestanden?", wobei der spezifische Typ Result als unwichtiges Implementierungsdetail ist. (Ich möchte jedoch nicht vorschlagen oder erneut öffnen, dass "es sollte einen neuen Typ für Produktwert vs. vorzeitige Rückgabe geben.) Und zur Dokumentation mag ich, dass from_error über ? spricht. throw ), während from_ok über das Erfolgs-Wrapping (#41414) spricht, anstatt beide in derselben Methode zu haben.

Ich bin mir nicht sicher, ob dies das richtige Forum für diesen Kommentar ist, bitte leite mich um, wenn dies nicht der Fall ist : https://github.com/rust-lang/rfcs/pull/1859 sein sollen und ich habe die Kommentarfrist verpasst; Hoppla!


Ich habe mich gefragt, ob es einen Grund gibt, das Merkmal Try aufzuteilen oder die Methode into_result entfernen; Für mich ist Try derzeit so etwas wie die Summe der Merkmale FromResult (enthält from_error und from_ok ) und IntoResult (enthält into_result ).

FromResult ermöglicht mit dem Operator ? FromResult ein sehr ergonomisches frühes Beenden, was meiner Meinung nach der Killer-Anwendungsfall für diese Funktion ist. Ich denke, dass IntoResult bereits sauber mit Methoden pro Anwendungsfall oder als Into<Result> implementiert werden kann; vermisse ich ein paar nützliche Beispiele?

Dem Try Merkmal RFC folgend, könnte expr? wie folgt entzuckern:

match expr { // Removed `Try::into_result()` here.
    Ok(v) => v,
    Err(e) => return Try::from_error(From::from(e)),
}

Die motivierenden Beispiele, die ich in Betracht gezogen habe, sind Future und Option .

Zukunft

Wir können FromResult natürlich für struct FutureResult: Future implementieren als:

impl<T, E> FromResult for FutureResult {
    type Ok = T;
    type Error = E;
    fn from_error(v: Self::Error) -> Self {
        future::err(v)
    }
    fn from_ok(v: Self::Ok) -> Self {
        future::ok(v)
    }
}

Angenommen, eine vernünftige Implementierung für ? , die von einer impl Future wertigen Funktion zurückgegeben wird, wenn sie auf eine Result::Err angewendet wird, kann ich schreiben:

fn async_stuff() -> impl Future<V, E> {
    let t = fetch_t();
    t.and_then(|t_val| {
        let u: Result<U, E> = calc(t_val);
        async_2(u?)
    })
}
fn fetch_t() -> impl Future<T, E> {}
fn calc(t: T) -> Result<U,E> {}

Genau das habe ich heute versucht umzusetzen und Try bringt es Try für Future zu implementieren, gibt es vielleicht keine kanonische Wahl für into_result ; Es könnte nützlich sein, einmal in Panik zu geraten, zu blockieren oder abzufragen, aber keine davon scheint universell nützlich zu sein. Wenn es keine into_result auf Try gäbe, kann ich wie oben einen frühen Exit implementieren, und wenn ich ein Future in ein Result umwandeln muss (und von dort zu any Try ) Ich kann es mit einer geeigneten Methode umwandeln (rufen Sie die wait Methode zum Blockieren auf, rufen Sie einige poll_once() -> Result<T,E> , etc.).

Möglichkeit

Option ist ein ähnlicher Fall. Wir implementieren from_ok , from_err natürlich wie im Try Trait RFC , und könnten Option<T> Result<T, Missing> einfach mit o.ok_or(Missing) in Result<T, Missing> umwandeln o.ok_or(Missing) oder eine bequeme Methode ok_or_missing .


Ich hoffe, das hilft!

Ich bin vielleicht sehr spät dran, aber dieses Wochenende ist mir aufgefallen, dass ? eine ziemlich natürliche Semantik hat, wenn Sie _Erfolg_ kurzschließen möchten.

fn fun() -> SearchResult<Socks> {
    search_drawer()?;
    search_wardrobe()
}

Aber in diesem Fall passt die Benennung der Methoden der Try Merkmale nicht.

Es würde jedoch die Bedeutung des Operators ? .

Beim Prototyping eines try_fold für Rayon wollte ich so etwas wie Try::is_error(&self) für die Consumer::full() Methoden. (Rayon hat das, weil Verbraucher im Grunde Iteratoren im Push-Stil sind, aber wir wollen trotzdem Fehler kurzschließen.) Ich musste stattdessen die Zwischenwerte als Result<T::Ok, T::Err> speichern, damit ich Result::is_err() aufrufen konnte.

Von dort aus wünschte ich mir auch ein Try::from_result um am Ende wieder zu T zu kommen, aber ein match Mapping auf T::from_ok und T::from_error ist nicht _zu_ schlecht.

Das Try Merkmal kann eine from_result Methode für die Ergonomie bieten, wenn from_ok und from_error erforderlich sind. Oder umgekehrt. Aus den angeführten Beispielen sehe ich einen Grund, beides anzubieten.

Bedeutet dies, dass dieses Problem seit der Zusammenführung von PR #42275 behoben wurde?

@skade Beachten Sie, dass ein Typ den kurzschließenden Typ definieren kann, wie es LoopState für einige Iterator Interna tut. Vielleicht wäre das natürlicher, wenn Try von "Fortfahren oder Brechen" sprechen würde, anstatt von Erfolg oder Misserfolg.

@cuviper Die Version, die ich am Ende brauchte, war entweder der Typ Ok oder der Typ Error -in-original. Ich hoffe, dass Destrukturieren und Wiederherstellen allgemein genug ist, um sich gut zu optimieren und eine Reihe spezieller Methoden für das Merkmal werden nicht benötigt.

@ErichDonGubler Dies ist ein Tracking-Problem, das erst behoben wird, wenn der entsprechende Code stabil ist.

Erfahrungsbericht:

Ich war ein wenig frustriert, als ich versuchte, diese Eigenschaft in die Praxis umzusetzen. Ich war jetzt mehrmals versucht, aus irgendeinem Grund meine eigene Variante auf Result zu definieren, aber jedes Mal habe ich am Ende nur Result , hauptsächlich wegen der Implementierung von Try war zu nervig. Ich bin mir aber nicht ganz sicher, ob das gut ist oder nicht!

Beispiel:

Im neuen Solver für die Chalk-VM wollte ich eine Aufzählung haben, die das Ergebnis der Lösung eines "Strangs" angibt. Dies hatte vier Möglichkeiten:

enum StrandFail<T> {
    Success(T),
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

Ich wollte, dass ? , wenn es auf diese Aufzählung angewendet wird, "Erfolg" auspackt, aber alle anderen Fehler nach oben propagiert. Um das Try Trait zu implementieren, hätte ich jedoch eine Art "Rest"-Typ definieren müssen, der nur die Fehlerfälle kapselt:

enum StrandFail {
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

Aber sobald ich diesen Typ habe, kann ich genauso gut StrandResult<T> einem Alias ​​machen:

type StrandResult<T> = Result<T, StrandFail>;

und das habe ich getan.

Das scheint nicht unbedingt schlimmer zu sein, als eine einzelne Aufzählung zu haben – aber es fühlt sich ein bisschen seltsam an. Wenn ich beispielsweise die Dokumentation für eine Funktion schreibe, "gruppiere" ich die Ergebnisse normalerweise nicht nach "ok" und "Fehler", sondern spreche von den verschiedenen Möglichkeiten als "gleich". Zum Beispiel:

    /// Invoked when a strand represents an **answer**. This means
    /// that the strand has no subgoals left. There are two possibilities:
    ///
    /// - the strand may represent an answer we have already found; in
    ///   that case, we can return `StrandFail::NoSolution`, as this
    ///   strand led nowhere of interest.
    /// - the strand may represent a new answer, in which case it is
    ///   added to the table and `Ok` is returned.

Beachten Sie, dass ich nicht gesagt habe: "Wir geben Err(StrandFail::NoSolution) . Das liegt daran, dass sich das Err einfach wie dieses nervige Artefakt anfühlt, das ich hinzufügen muss.

(Auf der anderen Seite würde die aktuelle Definition den Lesern helfen, das Verhalten von ? ohne die Try Impl. zu konsultieren.)

Ich denke, dieses Ergebnis ist nicht so überraschend: Das aktuelle Try Impl zwingt Sie dazu, ? für Dinge zu verwenden, die zu Result isomorph sind. Diese Einheitlichkeit kommt nicht von ungefähr, macht es aber ärgerlich, ? mit Dingen zu verwenden, die nicht im Grunde "nur Result " sind. (Übrigens ist es im Grunde das gleiche Problem, das zu NoneError – die Notwendigkeit, einen Typ künstlich zu definieren, um den "Fehler" eines Option darzustellen.)

Ich möchte auch anmerken, dass ich im Fall von StrandFail die From::from Konvertierung, die normale Ergebnisse erhalten, nicht besonders möchte, obwohl es mir nicht schadet.

Wird der zugehörige Try::Error Typ jemals dem Benutzer direkt zugänglich gemacht/verwendet? Oder wird es nur als Teil der Entzuckerung von ? ? Im letzteren Fall sehe ich kein wirkliches Problem darin, es einfach "strukturell" zu definieren - type Error = Option<Option<(Strand, Minimums)>> oder was auch immer. (Das strukturelle Äquivalent der "Fehlerhälfte" der enum Definition herausfinden zu müssen, ist nicht großartig, scheint aber weniger nervig zu sein, als die gesamte öffentlich zugängliche API neu aufzustellen.)

Ich folge auch nicht. Ich habe Try erfolgreich für einen Typ implementiert, der eine interne Fehlerdarstellung hatte, und es fühlte sich natürlich an, dass Ok und Error denselben Typ haben. Sie können die Implementierung hier sehen: https://github.com/kevincox/ecl/blob/8ca7ad2bc4775c5cfc8eb9f4309b2666e5163e02/src/lib.rs#L298 -L308

Tatsächlich scheint es ein ziemlich einfaches Muster zu geben, um diese Arbeit zu machen.

impl std::ops::Try for Value {
    type Ok = Self;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { v }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        if self.is_ok() { Ok(val) } else { Err(val) }
    }
}

Wenn Sie den Erfolg auspacken möchten, sollte so etwas funktionieren:

impl std::ops::Try for StrandFail<T> {
    type Ok = T;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { StrandFail::Success(v) }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        match self {
            StrandFail::Success(v) => Ok(v),
            other => Err(other),
        }
    }
}

Die Definition eines Strukturtyps ist möglich, fühlt sich aber ziemlich nervig an. Ich stimme zu, dass ich Self . Es fühlt sich für mich jedoch etwas verrückt an, dass Sie dann ? , um von StrandResult in Result<_, StrandResult> usw. umzuwandeln.

Toller Erfahrungsbericht, @nikomatsakis! Ich war auch mit Try (aus der anderen Richtung) unzufrieden.

TL/DR : Ich denke, FoldWhile hat das richtig gemacht, und wir sollten die Break -vs- Continue Interpretation von ? verdoppeln, anstatt darüber zu sprechen es in Bezug auf Fehler.

Länger:

Ich verwende Err für etwas, das näher an "Erfolg" als "Fehler" liegt, weil ? so praktisch ist.

Also, wenn nichts anderes, denke ich, dass die Beschreibung, die ich für Try falsch ist und nicht von einer "Erfolg/Fehler-Dichotomie" sprechen sollte, sondern eher von "Fehlern" abstrahieren.

Die andere Sache, über die ich nachgedacht habe, ist, dass wir einige leicht verrückte Impls für Try Betracht ziehen sollten. Zum Beispiel könnte Ordering: Try<Ok = (), Error = GreaterOrLess> in Kombination mit "Versuchsfunktionen" zulassen, dass cmp für struct Foo<T, U> { a: T, b: U } nur

fn cmp(&self, other: &self) -> Ordering try {
    self.a.cmp(&other.a)?;
    self.b.cmp(&other.b)?;
}

Ich weiß noch nicht, ob das die gute oder die böse Sorte ist :lacht: Es hat aber sicherlich eine gewisse Eleganz, denn wenn die Dinge anders sind, weiß man, dass sie anders sind. Und der Versuch, beiden Seiten "Erfolg" oder "Fehler" zuzuordnen, scheint nicht gut zu passen.

Ich stelle auch fest, dass dies ein weiterer Fall ist, in dem die "Fehlerkonvertierung" nicht hilfreich ist. Ich habe es auch nie in den obigen Beispielen für "Ich möchte? aber es ist kein Fehler" verwendet. Und es ist sicherlich bekannt, dass es Traurigkeit verursacht, also frage ich mich, ob es eine Sache ist, die auf das Ergebnis beschränkt werden sollte (oder möglicherweise zugunsten von .map_err(Into::into) , aber das ist wahrscheinlich nicht machbar).

Bearbeiten: Oh, und all das lässt mich fragen, ob die Antwort auf "Ich verwende weiterhin Ergebnis für meine Fehler, anstatt Try für einen eigenen Typ zu implementieren" "gut, das wird erwartet".

Bearbeiten 2: Dies ist nicht unähnlich https://github.com/rust-lang/rust/issues/42327#issuecomment -318923393 oben

Bearbeiten 3: Scheint, als ob eine Continue Variante auch in https://github.com/rust-lang/rfcs/pull/1859#issuecomment -273985250 vorgeschlagen wurde

Meine zwei Cent:

Ich mag den Vorschlag von @fluffysquirrels , das Merkmal in zwei Merkmale into_result oder Äquivalent als Teil der Entzuckerung beibehalten. Und ich denke, an diesem Punkt müssen wir, da sich die Verwendung von Option als Try stabilisiert hat.

Ich mag auch die Idee von hinweisen .

Um den spezifischen Code hier einzufügen, gefällt mir, wie sich das liest:

https://github.com/rust-lang/rust/blob/ab8b961677ac5c74762dcea955aa0ff4d7fe4915/src/libcore/iter/iterator.rs#L1738 -L1746

Es ist natürlich nicht ganz so schön wie eine gerade Schleife, aber mit "Methoden ausprobieren" wäre es knapp:

self.try_for_each(move |x| try { 
    if predicate(&x) { return LoopState::Break(x) } 
}).break_value() 

Zum Vergleich finde ich die Version "Fehlervokabular" wirklich irreführend:

self.try_for_each(move |x| { 
    if predicate(&x) { Err(x) } 
    else { Ok(()) } 
}).err() 

Können wir Display for NoneError implementieren? Es würde es der Fehlerkiste ermöglichen, automatisch From<NoneError> for failure::Error abzuleiten. Siehe https://github.com/rust-lang-nursery/failure/issues/61
Es sollte eine 3-Zeilen-Änderung sein, aber ich bin mir nicht sicher über den Prozess von RFCs und dergleichen.

@cowang4 Ich würde gerne versuchen, Try und des Deszuckers in etwas ändern würden, das NoneError nicht benötigt ...

@scottmcm Okay. Ich verstehe dein Argument. Ich möchte schließlich einen sauberen Weg, um das Muster der Rückgabe von Err zu verbessern, wenn eine Bibliotheksfunktion None zurückgibt. Vielleicht kennen Sie einen anderen als Try ? Beispiel:

fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    if let Some(filestem) = pb.file_stem() {
        if let Some(filestr) = filestem.to_str() {
            return Ok(MyStruct {
                filename: filestr.to_string()
            });
        }
     }
    Err(_)
}

Als ich diese experimentelle Funktion und die failure Kiste gefunden hatte, zog es mich natürlich zu:

use failure::Error;
fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    Ok({
        title: pb.file_stem?.to_str()?.to_string()
    })
}

Was _fast_ funktioniert, abgesehen vom Fehlen eines impl Display for NoneError wie ich bereits erwähnt habe.
Aber wenn dies nicht die Syntax ist, die wir verwenden möchten, könnte es vielleicht eine andere Funktion / ein anderes Makro geben, das das Muster vereinfacht:

if option.is_none() {
    return Err(_);
}

@cowang4 Ich glaube, das würde funktionieren, wenn Sie From<NoneError> für failure::Error implementieren würden, was Ihren eigenen Typ verwendet, der Display implementiert hat.

Es ist jedoch wahrscheinlich besser, opt.ok_or(_)? damit Sie den Fehler explizit angeben können, wenn die Option None ist. In Ihrem Beispiel möchten Sie beispielsweise einen anderen Fehler, wenn pb.file_stem None ist, als wenn to_str() None zurückgibt.

@tmccombs Ich habe versucht, meinen eigenen Fehlertyp zu erstellen, aber ich muss es falsch gemacht haben. Es war so:

#[macro_use] extern crate failure_derive;

#[derive(Fail, Debug)]
#[fail(display = "An error occurred.")]
struct SiteError;

impl From<std::option::NoneError> for SiteError {
    fn from(_err: std::option::NoneError) -> Self {
        SiteError
    }
}

fn build_piece(cur_dir: &PathBuf, piece: &PathBuf) -> Result<Piece, SiteError> {
    let title: String = piece
        .file_stem()?
        .to_str()?
        .to_string();
    Ok(Piece {
        title: title,
        url: piece
            .strip_prefix(cur_dir)?
            .to_str()
            .ok_or(err_msg("tostr"))?
            .to_string(),
    })
}

Und dann habe ich versucht, meinen Fehlertyp zu verwenden ...

error[E0277]: the trait bound `SiteError: std::convert::From<std::path::StripPrefixError>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
    | |___________________________________^ the trait `std::convert::From<std::path::StripPrefixError>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

error[E0277]: the trait bound `SiteError: std::convert::From<failure::Error>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
197 | |             .to_str()
198 | |             .ok_or(err_msg("tostr"))?
    | |_____________________________________^ the trait `std::convert::From<failure::Error>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

Okay, ich habe gerade festgestellt, dass es wissen möchte, wie man von anderen Fehlertypen in meinen Fehler konvertiert, den ich generisch schreiben kann:

impl<E: failure::Fail> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

Nö...

error[E0119]: conflicting implementations of trait `std::convert::From<SiteError>` for type `SiteError`:
   --> src/main.rs:183:1
    |
183 | impl<E: failure::Fail> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: conflicting implementation in crate `core`:
            - impl<T> std::convert::From<T> for T;

Okay, was ist mit std::error::Error ?

impl<E: std::error::Error> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

Das geht auch nicht. Teilweise, weil es mit meinen From<NoneError> Konflikt steht

error[E0119]: conflicting implementations of trait `std::convert::From<std::option::NoneError>` for type `SiteError`:
   --> src/main.rs:181:1
    |
175 | impl From<std::option::NoneError> for SiteError {
    | ----------------------------------------------- first implementation here
...
181 | impl<E: std::error::Error> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `SiteError`
    |
    = note: upstream crates may add new impl of trait `std::error::Error` for type `std::option::NoneError` in future versions

Was seltsam ist, weil ich dachte, dass NoneError std::error::Error nicht implementiert hat. Wenn ich mein nicht generisches impl From<NoneError> auskommentiere, erhalte ich:

error[E0277]: the trait bound `std::option::NoneError: std::error::Error` is not satisfied
   --> src/main.rs:189:25
    |
189 |       let title: String = piece
    |  _________________________^
190 | |         .file_stem()?
    | |_____________________^ the trait `std::error::Error` is not implemented for `std::option::NoneError`
    |
    = note: required because of the requirements on the impl of `std::convert::From<std::option::NoneError>` for `SiteError`
    = note: required by `std::convert::From::from`

Muss ich alle From s manuell schreiben? Ich dachte, dass die Fehlerkiste sie ableiten sollte?

Vielleicht sollte ich bei option.ok_or() bleiben

Muss ich alle Froms manuell schreiben. Ich dachte, dass die Fehlerkiste sie ableiten sollte?

Ich glaube nicht, dass die Fehlerkiste das tut. Aber ich könnte falsch liegen.

Ok, also habe ich die Fehlerkiste erneut untersucht, und wenn ich die Dokumentation und die verschiedenen Versionen richtig lese, ist es so konzipiert, dass immer failure::Error als Fehlertyp in Ihren Result s verwendet wird. siehe hier . Und es implementiert ein pauschales impl Fail Merkmal für die meisten Fehlertypen:

impl<E: StdError + Send + Sync + 'static> Fail for E {}

https://github.com/rust-lang-nursery/failure/blob/master/failure-1.X/src/lib.rs#L218

Und dann ein impl From damit es Try / ? andere Fehler (wie die von std) in den übergreifenden failure::Error .
rust impl<F: Fail> From<F> for ErrorImpl
https://github.com/rust-lang-nursery/failure/blob/d60e750fa0165e9c5779454f47a6ce5b3aa426a3/failure-1.X/src/error/error_impl.rs#L16

Da der für dieses Rostproblem relevante Fehler NoneError jedoch experimentell ist, kann er noch nicht automatisch konvertiert werden, da er das Merkmal Display nicht implementiert. Und das wollen wir nicht, denn das verwischt die Grenze zwischen Option s und Result s weiter. Es wird wahrscheinlich alles überarbeitet und irgendwann geklärt werden, aber für den Moment bleibe ich bei den entzuckerten Techniken, die ich gelernt habe.

Vielen Dank an alle für die Hilfe. Ich lerne langsam Rust! :Lächeln:

Ich bleibe bei den entzuckerten Techniken, die ich gelernt habe.

:+1: Ehrlich gesagt denke ich, dass .ok_or(...)? der Weg bleiben wird (oder natürlich .ok_or_else(|| ...)? ). Selbst wenn NoneError ein Display Impl _hätte_, was würde es sagen? "Irgendwas war nicht da"? Das ist kein großer Fehler...

Der Versuch, einem konkreten Vorschlag näher zu kommen...

Ich fange an, die TrySuccess Alternative zu mögen. Interessanterweise glaube ich, dass _niemand_, mich eingeschlossen, das ursprünglich gemocht hat -- es ist nicht einmal in der endgültigen Version des RFC. Aber zum Glück lebt es in der Geschichte weiter: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#using -an-associated-type-for-the-success -Wert

Für mich war der größte Einwand dagegen die berechtigte Beschwerde, dass ein ganzes Kernmerkmal nur für einen zugehörigen Typ ( trait TrySuccess { type Success; } ) übertrieben sei. Aber mit catch try Blöcke zurück (https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777) zum Ok-Wrapping (wie im RFC) , hat es plötzlich einen direkten und wichtigen Nutzen: Dies ist die Eigenschaft, die diese Umhüllung steuert. In Verbindung mit dem Ziel " ? sollte immer den gleichen Typ produzieren", das meiner Meinung nach allgemein erwünscht war, scheint die Eigenschaft ihr Gewicht besser zu halten. Strohmann:

trait TryContinue {
    type Continue;
    fn from_continue(_: Self::Continue) -> Self;
}

Dieser zugeordnete Typ, der vom Operator ? wird, muss auch eindeutig vorhanden sein. Das heißt, es trifft nicht auf den Ärger "muss eine Art 'Rest'-Typ definieren", den Niko artikulierte . Und () ist vernünftig, sogar üblich, vermeidet also NoneError -ähnliche Verrenkungen.

Bearbeiten: Für den Fahrradschuppen könnte "Rückgabe" ein gutes Wort sein, da dies passiert, wenn Sie return von einer try Methode (auch bekannt als ok-wrapping) verwenden. return ist auch der Monade Name des

Bearbeiten 2: Meine Gedanken zu den anderen Merkmalen/Methoden haben sich noch nicht stabilisiert, @tmccombs.

@scottmcm, nur um es klar zu

trait TryContinue {
  type Continue;
  fn from_continue(_: Self::Continue) -> Self;
}

trait Try<E>: TryContinue {
  fn try(self) -> Result<Self::Continue, E>
}

Und das Entschlacken von x? würde ungefähr so ​​aussehen:

x.try() match {
    Ok(c) => c,
   Err(e) => throw e // throw here is just a placeholder for either returning or breaking out of a try block
}

und try { ...; expr} würde zu etwas entzuckern:

{ 
    ...
    TryContinue::from_continue(expr);
}

@scottmcm Auch ich finde diese Variante viel ansprechender, wenn man über Ok-Wrapping

Weiter geht es mit dem nächsten Merkmal, ich denke, das für ? wird dieses (modulo massives Bikeshedding):

trait Try<Other: TryContinue = Self>: TryContinue + Sized {
    fn check(x: Other) -> ControlFlow<Other::Continue, Self>;
}

enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

Verschiedene Begründung:

  • Dies erfordert TryContinue für seinen Argumenttyp, damit der Typ eines x? Ausdrucks immer derselbe ist
  • Dies setzt den Typparameter auf Self für einfache Fälle wie try_fold , die denselben Typ untersuchen und zurückgeben, also sind nur T: Try Ordnung.
  • Dieses Merkmal erweitert TryContinue denn wenn ein als Rückgabetyp verwendeter Typ ? in seinem Körper zulässt, sollte er auch Ok-Wrapping unterstützen.
  • Die neue Aufzählung ist im Ergebnis isomorph, vermeidet jedoch, wie oben beschrieben, in Bezug auf "Fehler" zu sprechen, und bietet daher meiner Meinung nach eine sehr klare Bedeutung für jede der Varianten.
  • Das Argument für ? ist der generische Typ, so dass es wie TryContinue "ein Self aus etwas erzeugt"

Vollständige Proof-of-Concept-Demo, einschließlich Makros für try{} / ? und Impls für Option , Result und Ordering : https ://play.rust-lang.org/?gist=18663b73b6f35870d20fd172643a4f96&version=stable (danke @nikomatsakis für die Erstellung des Originals vor einem Jahr 🙂)

Nachteile:

  • Hat immer noch die Phantomtyp-Parameter in der Result Impl, die ich nicht wirklich mag

    • Aber vielleicht ist das ein guter Kompromiss, um keinen zusätzlichen LessOrGreater Typ in der Ordering Impl.

    • Und Phantomtypparameter in Impls sind sowieso weniger eklig als in Typen

  • Ergibt keine konsistente From Transformation

    • Aber das könnte gut sein, da es StrandResult sowieso egal war, für etwas wie SearchResult wäre es seltsam, da der Break-Pfad der Erfolgspfad ist, und es könnte am Ende helfen, Rückschlüsse zu ziehen die nested- ? Fällen sowieso.

  • Die Syntax von throw ist nicht so offensichtlich

    • Was die Erklärung von ? als Ok(x) => x, Err(r) => throw e.into() verliert, die mir wirklich gefallen hat

    • Könnte aber auch throw; sein, um None (über so etwas wie impl<T> Throw<()> for Option<T> ), was viel besser ist als throw NoneError;

    • Und das zu trennen wäre sowieso gut, denn throw LessOrGreater::Less wäre _wirklich_ albern gewesen.

Edit: Ping @glaebhoerl , dessen Meinung ich als großer Teilnehmer an RFC 1859 gerne dazu hätte.

Bearbeiten 2: Auch cc @colin-kiegel, für diese Aussage von https://github.com/rust-lang/rfcs/pull/1859#issuecomment -287402652:

Ich frage mich, ob der essentialistische Ansatz etwas von der Eleganz der Reduktionisten übernehmen könnte, ohne die oben genannten Ziele zu opfern.

Der Vorschlag gefällt mir sehr gut.

Es ist nicht so offensichtlich, wie die Throw-Syntax aussehen würde

Wenn man das Gepäck des Schlüsselworts throw aus anderen Sprachen ignoriert, ist "throw" insofern sinnvoll, als Sie den Wert in einen höheren Bereich "werfen". Python und scala verwenden auch Ausnahmen für den Kontrollfluss außer Fehlerfällen (obwohl Sie im Fall von scala normalerweise try/catch nicht direkt verwenden würden), daher gibt es einige Präzedenzfälle für die Verwendung von throw für erfolgreiche Pfade.

@tmccombs

Wenn man das Gepäck des Schlüsselworts throw aus anderen Sprachen ignoriert, ist "throw" insofern sinnvoll, als Sie den Wert in einen höheren Bereich "werfen".

Obwohl es mit ähnlichem Gepäck geliefert wird, vermute ich, dass "raise" besser passt:

Wurf (v): an einen Ort, eine Position, einen Zustand usw. wie durch Schleudern bringen oder gehen oder kommen lassen:

heben (v): um sich in eine höhere Position zu bewegen; hochheben; erheben

Es gibt möglicherweise eine Möglichkeit, "raise" mit der Logik von ? zu kombinieren, da "raise" auch "collect" bedeuten kann. Etwas wie: Ok(v) => v, Err(e) => raise From::from(e) , wobei raise das übereinstimmende Muster imitiert (zB bei einem Muster Err(e) es syntaktische Magie für ControlFlow::Break(Err(...)) ).

Ich vermute, "erhöhen" passt besser

guter Punkt

@scottmcm

Es ist nicht so offensichtlich, wie die Throw-Syntax aussehen würde

Gibt es einen Grund, warum wir from_err(value: Other) nicht haben können?

UPDATE: Vielleicht bin ich tatsächlich verwirrt über die Rolle von Other . Ich muss diese Eigenschaft noch ein bisschen studieren. =)

@nikomatsakis

Gibt es einen Grund, warum wir from_err(value: Other) nicht haben können?

Nun, Other ist ein ganzer ? -fähiger Typ (wie ein Result ), also würde ich nicht wollen, dass throw Ok(4) funktioniert. (Und es muss die gesamte Disjunktion sein, um zu vermeiden, dass die Einführung eines künstlichen Fehlertyps erzwungen wird.) Zum Beispiel denke ich, dass unser aktuelles Option-Result-Interop dem entspricht (und der Umkehrung):

impl<T, F, U> Try<Option<U>> for Result<T, F>
   where F: From<NoneError>
{
    fn check(x: Option<U>) -> ControlFlow<U, Self> {
        match x {
            Some(x) => ControlFlow::Continue(x),
            None => ControlFlow::Break(Err(From::from(NoneError))),
        }
    }
}

Meine aktuelle Neigung für throw wäre dafür:

trait TryBreak<T> : TryContinue { from_break(_: T) -> Self }

mit

throw $expr  ==>  break 'closest_catch TryBreak::from_break($expr)
  • Erweitert TryContinue sodass ok-wrapping auch für den Rückgabetyp der Methode verfügbar ist, in der es verwendet wird
  • Kein zugeordneter Typ, also könnten Sie beide, sagen wir, TryBreak<TryFromIntError> und TryBreak<io::Error> für einen Typ implementieren, wenn Sie wollten.

    • und impl<T> TryBreak<()> for Option<T> , um nur throw; impl<T> TryBreak<()> for Option<T> zu aktivieren, fühlt sich in einer Weise plausibel an, wie es ein zugehöriger Fehlertyp von () nicht tat.

    • impl<T> TryBreak<!> for T wäre auch nett, ist aber wahrscheinlich inkohärent.

(Außerdem: diese Eigenschafts- und Methodennamen sind schrecklich; bitte helft !)

Meine Gedanken zu den anderen hier angesprochenen Themen haben sich noch nicht in eine leicht artikulierbare Form gebracht, aber in Bezug auf die Typinferenzprobleme rund um die Entzuckerung von From::from() (ich kann mich nicht erinnern, ob dies in letzter Zeit auch woanders diskutiert wurde , oder nur hier?):

Das instinktive Gefühl (aus Haskell-Erfahrung), dass "auf diese Weise Ambiguitäten vom Typ der Inferenz liegen" war einer der (wenn auch nicht der Haupt-) Gründe, warum ich keine From Konvertierung als Teil des ursprünglichen RFC haben wollte. Jetzt, wo er in den Kuchen gebacken ist, frage ich mich jedoch, ob wir nicht versuchen könnten, diesen Kuchen zu haben und ihn auch zu essen, indem wir "nur" eine spezielle Standardregel dafür zum Typinferenzprozess hinzufügen?

Das heißt, ich denke: Immer wenn wir ein From::from() [optional: eines, das von der ? Entzuckerung eingefügt wurde, anstatt manuell geschrieben zu werden] und wir genau einen der beiden Typen kennen (Eingabe vs. Ausgabe), während der andere mehrdeutig ist, wird der andere standardmäßig mit dem anderen identisch sein. Mit anderen Worten, ich denke, wenn nicht klar ist, welches impl From verwendet werden soll, verwenden wir standardmäßig impl<T> From<T> for T . Das ist, glaube ich, im Grunde immer das, was man eigentlich will? Es ist vielleicht ein bisschen ad-hoc, aber wenn es funktioniert, scheinen die Vorteile die Kosten IMHO wert zu sein.

(Ich dachte auch, dass From schon ein langer Gegenstand ist, gerade wegen der ? Entzuckerung, aber das scheint es nicht zu sein? Auf jeden Fall ist es deswegen schon irgendwie besonders .)

in @scottmcms Vorschlag ist From::from() _nicht_ Teil der Entzuckerung, sondern in der Implementierung von Try für Result .

@tmccombs Ich habe keine Änderung seines Vorschlags vorgeschlagen.

Der try{} RFC (https://github.com/rust-lang/rfcs/pull/2388) diskutiert try Blöcke, bei denen einem das Ergebnis egal ist. Diese Alternative scheint dies recht gut zu handhaben, da sie die Fehlertypen auf Wunsch vollständig ignorieren kann

let IgnoreErrors = try {
    error()?;
    none()?;
};

Proof-of-Concept-Implementierung mit den gleichen Merkmalen wie zuvor: https://play.rust-lang.org/?gist=e0f6677632e0a9941ed1a67ca9ae9c98&version=stable

Ich denke, es gibt einige interessante Möglichkeiten, zumal eine benutzerdefinierte Implementierung beispielsweise nur Ergebnisse nehmen und E: Debug binden könnte, sodass alle auftretenden Fehler automatisch protokolliert werden. Oder es könnte eine Version speziell als Rückgabetyp für main in Verbindung mit Termination , die "einfach funktioniert", damit Sie ? ohne eine komplexe Typsignatur verwenden können (https ://github.com/rust-lang/rfcs/issues/2367).

Ich hatte ähnliche Probleme wie die von @nikomatsakis mit der vorhandenen Version des Merkmals Try . Zu den spezifischen Problemen siehe https://github.com/SergioBenitez/Rocket/issues/597#issuecomment -381533108.

Die von @scottmcm vorgeschlagenen

#[derive(Debug, Copy, Clone)]
enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

// Used by `try { }` expansions.
trait FromTry: Try {
    fn from_try(value: Self::Continue) -> Self;
}

// Used by `?`.
trait Try<T = Self>: Sized {
    type Continue;
    fn check(x: T) -> ControlFlow<Self::Continue, Self>;
}

Die wichtigste Änderung besteht darin, dass der mit Continue verknüpfte Typ auf dem Merkmal Try im Gegensatz zu FromTry (vorher TryContinue ). Neben der Vereinfachung der Definitionen hat dies den Vorteil, dass Try unabhängig von FromTry implementiert werden kann, was meiner Meinung nach häufiger vorkommt, und dass die Implementierung von FromTry einmal vereinfacht wird Try ist implementiert. (Hinweis: Wenn Try und FromTry implementiert werden sollen, können wir einfach die Methode from_try in Try )

Sehen Sie sich den kompletten Playground mit Implementierungen für Result und Option sowie Rockets Outcome auf diesem Playground an .

Danke für den Bericht, @SergioBenitez! Diese Implementierung entspricht der alternativen Version "Flip the type parameters" des ursprünglichen Merkmalsvorschlags in RFC 1859: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#unresolved -Fragen

Der größte Verlust ist die Eigenschaft, dass typeof(x?) nur von typeof(x) abhängt. Das Fehlen dieser Eigenschaft war eines der Bedenken beim Original ("Ich mache mir ein bisschen Sorgen um die Lesbarkeit von Code in dieser Richtung" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967) und ein Vorteil des endgültigen reduktionistischen Vorschlags ("Für jeden gegebenen Typ T kann ? genau eine Art von ok/error-Wert erzeugen" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310). Natürlich gab es auch Argumente, die besagte Eigenschaft sei unnötig oder zu restriktiv, aber in der abschließenden Zusammenfassung vor FCP war sie noch als Vorteil da (https://github.com/rust-lang/rfcs/pull/1859#issuecomment -295878466).

Neben der Vereinfachung der Definitionen hat dies den Vorteil, dass Try unabhängig von FromTry implementiert werden kann, was meiner Meinung nach häufiger vorkommt

Heutzutage ist from_ok sicherlich weniger verbreitet, da Try und do catch instabil sind. Und selbst wenn do catch stabil wäre, stimme ich zu, dass es insgesamt weniger als ? (da die meisten solcher Blöcke mehrere ? s enthalten).

Aus der Perspektive des Merkmals und seiner Funktionsweise denke ich jedoch, dass "einen 'Keep-Going'-Wert in den Trägertyp einpacken" ein absolut wesentlicher Bestandteil von ? . Heutzutage geht man dafür selten durch - nur Ok(...) oder Some(...) verwenden - aber es ist entscheidend für den generischen Gebrauch. Beispiel: try_fold :

https://github.com/rust-lang/rust/blob/8728c7a726f3e8854f5a80b474d1a8bacab10304/src/libcore/iter/iterator.rs#L1478 -L1482

Wenn eine Funktion einen Trägertyp zurückgibt, ist es wichtig, dass es eine Möglichkeit gibt, den interessierenden Wert in diesen Trägertyp einzufügen. Und vor allem denke ich nicht, dass dies eine Härte ist. Es hat eine sehr natürliche Definition für Ergebnis, Option, Ergebnis usw.

@Centril hat auch schon darauf hingewiesen, dass "einen Skalar in einen Träger wickeln" theoretisch auch ein einfacheres Konstrukt ist. Haskell nennt es zum Beispiel die Pointed Typklasse , obwohl ich glaube, dass wir es in Rust nicht so weit verallgemeinern wollen: try { 4 } zuzulassen ↦ vec![4] scheint mir übertrieben zu sein .

Ich stelle mir auch eine Zukunft vor, in der, wie async Funktionen vorgeschlagen werden, um den Blockwert in einen Future einzuschließen, wir try Funktionen haben könnten, die den Blockwert in einen fehlbaren Typ einschließen. Auch hier ist TryContinue der kritische Teil -- eine solche Funktion verwendet vielleicht nicht einmal ? , wenn wir ein throw Konstrukt haben.

Das alles ist also leider eher philosophisch als konkret. Lass mich wissen, ob es Sinn gemacht hat oder ob du das Gegenteil in einem der Teile denkst :slightly_smiling_face:

Bearbeiten : Entschuldigung, wenn Sie eine E-Mail mit einer frühen Version davon erhalten haben; Ich habe zu früh auf "Kommentar" geklickt...

Der größte Verlust ist die Eigenschaft, dass typeof(x?) nur von typeof(x) abhängt.

Ach ja, absolut. Danke für den Hinweis.

Natürlich gab es auch Argumente, die besagte Eigenschaft sei unnötig oder zu restriktiv, aber in der abschließenden Zusammenfassung vor FCP war sie immer noch als Vorteil da (rust-lang/rfcs#1859 (Kommentar)).

Gibt es konkrete Beispiele dafür, wo es zu restriktiv sein könnte?

Wenn eine Funktion einen Trägertyp zurückgibt, ist es wichtig, dass es eine Möglichkeit gibt, den interessierenden Wert in diesen Trägertyp einzufügen. Und vor allem denke ich nicht, dass dies eine Härte ist.

Ich denke, das ist eine faire Analyse. Ich stimme zu.

@SergioBenitez Von https://github.com/rust-lang/rfcs/pull/1859#issuecomment -279187967

Die Frage ist also, sind die vorgeschlagenen Beschränkungen ausreichend? Gibt es gute Verwendungsmöglichkeiten für den Fehlerfallkontext bei der Bestimmung des Erfolgstyps? Gibt es wahrscheinlich Missbrauch?

Aus meiner Erfahrung mit Futures kann ich sagen, dass es hier durchaus sinnvolle Fälle geben kann. Insbesondere der Umfragetyp, über den Sie sprechen, kann auf verschiedene Weise verarbeitet werden. Manchmal möchten wir auf die NotReady-Variante springen und im Wesentlichen einen Ergebniswert haben. Manchmal sind wir nur an der Ready-Variante interessiert und möchten auf eine der anderen Varianten springen (wie in Ihrer Skizze). Wenn wir zulassen, dass der Erfolgstyp teilweise durch den Fehlertyp bestimmt wird, ist es plausibler, beide Fälle zu unterstützen und im Wesentlichen den Kontext zu verwenden, um zu bestimmen, welche Art von Dekomposition gewünscht wird.

OTOH, ich mache mir ein bisschen Sorgen um die Lesbarkeit von Code in dieser Richtung. Das fühlt sich qualitativ anders an, als nur die Fehlerkomponente umzuwandeln – es bedeutet, dass die grundlegende Übereinstimmung mit dem? durchführen würde, hängt von Kontextinformationen ab.

Man könnte sich also eine Eigenschaft vorstellen, die _beide_ Typen zu Typparametern bewegt, wie

trait Try<T,E> {
    fn question(self) -> Either<T, E>;
}

Und verwenden Sie das, um alle zu aktivieren

let x: Result<_,_> = blah.poll()?; // early-return if NotReady
let x: Async<_> = blah.poll()?; // early-return if Error
let x: i32 = blah.poll()?; // early-return both NotReady and Errors

Aber ich denke, das ist definitiv eine schlechte Idee, da dies bedeutet, dass diese nicht funktionieren

println!("{}", z?);
z?.method();

Da es keinen Typkontext gibt, um zu sagen, was zu produzieren ist.

Die andere Version wäre, Dinge wie diese zu aktivieren:

fn foo() -> Result<(), Error> {
    // `x` is an Async<i32> because NotReady doesn't fit in Result
    let x = something_that_returns_poll()?;
}
fn bar() -> Poll<(), Error> {
    // `x` is just i32 because we're in a Poll-returning method
    let x = something_that_returns_poll()?;
}

Mein Instinkt dort ist, dass die Schlussfolgerung "aus dem ?" Da ist zu überraschend, und damit gehört das in den "zu cleveren" Eimer.

Kritisch denke ich nicht, dass es zu restriktiv ist, es nicht zu haben. my_result? in einer -> Poll Funktion braucht es nicht, da der Erfolgstyp der gleiche ist wie üblich (wichtig, damit synchroner Code in asynchronen Kontexten gleich funktioniert) und die Fehlervariante auch gut konvertiert wird . Die Verwendung von ? auf einem Poll in einer Methode, die Result zurückgibt, scheint sowieso ein Anti-Muster zu sein, nicht etwas, das üblich sein sollte, also verwenden Sie (hypothetische) dedizierte Methoden wie .wait(): T (um das Ergebnis zu blockieren) oder .ready(): Option<T> (um zu überprüfen, ob es fertig ist), um den Modus auszuwählen, ist wahrscheinlich sowieso besser.

Das ist "interessant" https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug

Ich mag diese Versuchsblöcke nicht, sie scheinen nicht sehr anfängerfreundlich zu sein.

Ich versuche, den aktuellen Stand des Vorschlags zusammenzustellen, der auf mehrere Kommentare verteilt zu sein scheint. Gibt es eine einzige Zusammenfassung der derzeit vorgeschlagenen Merkmale (die den zugehörigen Typ Error )?

Zu Beginn dieses Threads sehe ich einen Kommentar zur Aufteilung von Try in separate to/from-Fehlermerkmale – gibt es Pläne, diese Aufteilung zu implementieren?

Es wäre nützlich, eine transparente Konvertierung von Result<T, E> in einen beliebigen Typ Other<T2, E> bei Fragezeichen zu haben -- dies würde es ermöglichen, vorhandene IO-Funktionen mit einer schönen Syntax innerhalb einer Funktion mit einer spezialisierteren (z faul) Rückgabetyp.

pub fn async_handler() -> AsyncResult<()> {
    let mut file = File::create("foo.txt")?;
    AsyncResult::lazy(move || {
        file.write_all(b"Hello, world!")?;
        AsyncResult::Ok(())
    })
}

Semantisch fühlt sich das wie From::from(E) -> Other<T2, E> , aber die Verwendung von From ist derzeit auf die vorhandene Result -äquivalente Try Implementierung beschränkt.

Ich denke wirklich, dass NoneError ein separates Tracking-Problem haben sollte. Selbst wenn die Eigenschaft Try nie stabilisiert wird, sollte NoneError stabilisiert werden, weil es die Verwendung von ? auf Option viel ergonomischer macht. Berücksichtigen Sie dies bei Fehlern wie struct MyCustomSemanthicalError; oder Fehlern bei der Implementierung von Default . None könnte über From<NoneError> leicht in MyCustomSeemanthicalError werden.

Bei der Arbeit an https://github.com/rust-analyzer/rust-analyzer/ habe ich einen etwas anderen Papierschnitt als Unzulänglichkeiten im Operator ? kennengelernt, insbesondere wenn der Rückgabetyp Result<Option<T>, E> .

Für diesen Typ ist es sinnvoll, dass ? effektiv entzuckert, um:

match value? {
    None => return Ok(None),
    Some(it)=>it,
}

wobei value vom Typ Result<Option<V>, E> , oder:

value?

wobei value Result<V, E> . Ich glaube, dass dies möglich ist, wenn die Standardbibliotheken Try für Option<T> folgt implementieren, obwohl ich dies nicht explizit getestet habe und ich denke, dass es Spezialisierungsprobleme geben kann.

enum NoneError<E> {
    None,
    Err(E),
}

impl From<T> for NoneError<T> {
    fn from(v: T) -> NoneError<T> {
        NoneError:Err(v)
    }
}

impl<T, E> std::Ops::Try for Result<Option<T>, E> {
    type OK = T;
    type Error = NoneError<E>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            Ok(option) => {
                if let Some(inner) = option {
                    Ok(inner)
                } else {
                    Err(NoneError::None)
                }
            }
            Err(error) => {
                Err(NoneError::Err(error));
            }
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::Err(error) => Err(error),
            None => Some(None),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(Some(v))
    }
}

impl<T> std::Ops::Try for Option<T> {
    type OK = T;
    type Error = NoneError<!>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            None => Err(NoneError::None),
            Some(v) => Ok(v),
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::None => Some(None),
            _ => unreachable!("Value of type ! obtained"),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(v)
    }
}

Als er @matklad fragte, warum er keine benutzerdefinierte Aufzählung erstellen konnte, die Try implementierte, die in diesem Fall Cancellable heißen würde, wies er darauf hin, dass std::ops::Try instabil ist, also ist es kann sowieso nicht verwendet werden, da rust-analyzer (derzeit) auf stabilen Rost abzielt.

Repost von https://github.com/rust-lang/rust/issues/31436#issuecomment -441408288 weil ich dies kommentieren wollte, aber ich denke, das war das falsche Problem:

Im Wesentlichen habe ich Rückrufe in einem GUI-Framework - anstatt Option oder Result , müssen sie UpdateScreen , um dem Framework mitzuteilen, ob der Bildschirm muss aktualisiert werden oder nicht. Oft brauche ich überhaupt keine Protokollierung (es ist einfach nicht praktikabel, jeden kleinen Fehler einzuloggen) und gebe einfach ein UpdateScreen::DontRedraw wenn ein Fehler aufgetreten ist. Mit dem aktuellen ? Operator muss ich jedoch dies die ganze Zeit schreiben:

let thing = match fs::read(path) {
    Ok(o) => o,
    Err(_) => return UpdateScreen::DontRedraw,
};

Da ich mit dem Try-Operator nicht von Result::Err in UpdateScreen::DontRedraw umwandeln kann, wird dies sehr mühsam - oft habe ich einfache Suchvorgänge in Hash-Maps, die fehlschlagen können (was kein Fehler ist) ) - so oft habe ich in einem Rückruf 5 - 10 Verwendungen des Operators ? . Da das Obige sehr ausführlich zu schreiben ist, lautet meine aktuelle Lösung impl From<Result<T>> for UpdateScreen wie folgt und verwende dann eine innere Funktion im Rückruf wie folgt:

fn callback(data: &mut State) -> UpdateScreen {
     fn callback_inner(data: &mut State) -> Option<()> {
         let file_contents = fs::read_to_string(data.path).ok()?;
         data.loaded_file = Some(file_contents);
         Some(())
     }

    callback_inner(data).into()
}

Da der Callback als Funktionszeiger verwendet wird, kann ich kein -> impl Into<UpdateScreen> (aus irgendeinem Grund ist die Rückgabe eines impl für Funktionszeiger derzeit nicht zulässig). Die einzige Möglichkeit für mich, den Operator Try überhaupt zu verwenden, besteht darin, den Trick mit der inneren Funktion auszuführen. Es wäre schön, wenn ich einfach so etwas machen könnte:

impl<T> Try<Result<T>> for UpdateScreen {
    fn try(original: Result<T>) -> Try<T, UpdateScreen> {
        match original {
             Ok(o) => Try::DontReturn(o),
             Err(_) => Try::Return(UpdateScreen::DontRedraw),
        }
    }
}

fn callback(data: &mut State) -> UpdateScreen {
     // On any Result::Err, convert to an UpdateScreeen::DontRedraw and return
     let file_contents = fs::read_to_string(data.path)?;
     data.loaded_file = Some(file_contents);
     UpdateScreen::Redraw
}

Ich bin mir nicht sicher, ob dies mit dem aktuellen Vorschlag möglich wäre und wollte nur meinen Anwendungsfall zur Prüfung hinzufügen. Es wäre großartig, wenn ein benutzerdefinierter Try-Operator so etwas unterstützen könnte.

@joshtriplett Entschuldigung, dass es eine Weile https://github.com/rust-lang/rust/compare/master...scottmcm :try-trait-v2 zusammengestellt, um konkret zu sein. Ich hoffe, damit noch einiges ausprobieren zu können.

@scottmcm Haben Sie eine Erklärung auf höherer Ebene zu den Änderungen?

@scottmcm FWIW Ich habe deine Änderungen auch in Rayon ausprobiert:
https://github.com/rayon-rs/rayon/compare/master...cuviper :try-trait-v2
(immer noch private Kopien anstelle von instabilen Elementen verwenden)

Was ist also die Lösung, um die Option NoneError zu konvertieren? Es scheint, dass es nicht kompiliert wird, weil es die Try-Eigenschaft implementiert, es sei denn, Sie aktivieren die Verwendung dieser bestimmten (instabilen?) Funktion.

Sie können also grundsätzlich nicht die ? Betreiber mit Option soweit ich weiß?

@omarabid , der Operator ist stabil für die Verwendung mit Option oder Result , aber Sie können Try als generische Einschränkung verwenden, bis er stabil ist. Es ist völlig in Ordnung, ? auf einem Option in einer Funktion zu verwenden, die Option , da Sie NoneError überhaupt nicht involvieren müssen. Sie können auch Result wenn Sie Typen löschen:

use std::fmt::Debug;

pub struct Error(Box<dyn Debug>);

impl<T: Debug + 'static> From<T> for Error {
    fn from(error: T) -> Self {
        Error(Box::new(error))
    }
}

pub fn try_get<T>(slice: &[T], index: usize) -> Result<&T, Error> {
    Ok(slice.get(index)?)
}

( Spielplatz )

@scottmcm , Ihr Prototyp try-trait-v2 scheitert in diesem Beispiel!

Wenn wir nicht wollen, dass mein Beispiel kaputt geht, braucht try-trait-v2 etwas wie:

#[unstable(feature = "try_trait_v2", issue = "42327")]
impl<T, U, E: From<NoneError>> ops::Bubble<Result<U, E>> for Option<T> {
    fn bubble(self) -> ops::ControlFlow<T, Result<U, E>> {
        match self {
            Some(x) => ops::ControlFlow::Continue(x),
            None => ops::ControlFlow::Break(Err(E::from(NoneError))),
        }
    }
}

Wie ist der aktuelle Status dieser Funktion?

PR #62606 zum Dokumentieren der Implementierung von try_fold für Iteratoren sollte wieder geöffnet werden, sobald dies stabil wird.

Bearbeiten: Aktualisiert die Op mit einem Tracking-Item für diesen ~ scottmcm

Gibt es Pläne, das Merkmal Try durch eine der in diesem Thread vorgeschlagenen Alternativen zu ersetzen? Die von @scottmcm vorgeschlagene ? mit Option , und ich denke, das Merkmal Try sollte geändert werden, um die Semantik Result für Option zu erzwingen

Die Verwendung der Alternative von @scottmcm würde es uns ermöglichen, ? mit Option und NoneError loszuwerden. Ich stimme @nikomatsakis ( Kommentar ) zu, dass das Merkmal Try nicht die Notwendigkeit fördern sollte, "künstlich einen Typ zu definieren, um den 'Fehler' eines Option darzustellen".

pub struct Error(Box<dyn std::fmt::Debug>);
impl<T: std::fmt::Debug + 'static> From<T> for Error { fn from(error : T) -> Self { Error(Box::new(error)) } }
type Result<T> = std::result::Result<T, Error>;

Anfänger hier, ich war ein bisschen stur im Wollen ? um automatisch alle Fehler und Optionen zu löschen.
Nachdem ich viel zu viel Zeit damit verbracht habe, zu verstehen, warum andere wahrscheinliche Lösungen nicht implementiert werden können, habe ich festgestellt, dass @cuviper dem am nächsten kommt, was ich bekommen kann.
Einige Erklärungen hätten geholfen, aber zumindest habe ich mich mit einigen Einschränkungen der Rust-Metaprogrammierung vertraut gemacht.
Also habe ich versucht, es herauszufinden und konkret zu erklären.
Dieser Thread scheint die wahrscheinlichste Kreuzung zu sein, an der ich hoffentlich jedem wie mir helfen kann, der darüber stolpert.

  • Verwenden von Debug (gemeinsam für StdError und NoneError) anstelle von StdError :
    Ein allgemeiner From<T: StdError> for Error und ein spezialisierter From<NoneError> for Error Konflikt
    Weil es mehrdeutig wäre, wenn NoneError StdError implementiert (anscheinend erlaubt selbst der Standard keine Überschreibungen und erzwingt Exklusivität)
    Dies könnte durch eine "negative Grenze" gelöst werden, die nicht unterstützt wird, vielleicht weil es der einzige Anwendungsfall wäre? (über Spezialisierung)
    impl StdError for NoneError kann nur zusammen mit StdError oder NoneError selbst definiert werden, damit es über alle Downstreams hinweg konsistent ist.
  • Fehler kann kein Alias ​​sein:
    type Error = Box<Debug> bindet Debug, wodurch From<T:Debug> for Error Konflikt mit From<T> for T (reflexives From für Idempotenz)

Da Error Debug nicht implementieren kann, möchten Sie möglicherweise einen Helfer zum Auspacken in ein Ergebnis mit transitivem Debug :

fn from<T>(result : Result<T>) -> std::result::Result<T, Box<dyn std::fmt::Debug>> { match result { Ok(t) => Ok(t), Err(e) => Err(e.0) } }

Es kann nicht impl From<Result<T>> for StdResult<T> oder Into<StdResult> for Result<T> (kann keine Upstream-Eigenschaft für Upstream-Typ implizieren)

Sie können es beispielsweise verwenden, um eine Debug-Rückgabe für Termination zu erhalten:

fn main() -> std::result::Result<(), Box<dyn std::fmt::Debug>> { from(run()) }

Böse Idee: Vielleicht könnte der Operator ? irgendwie eine monadische Bindung darstellen, also

let x: i32 = something?;
rest of function body

wird

Monad::bind(something, fn(x) {
    rest of function body
})

Eine schreckliche Idee, aber es gefällt dem inneren Geek in mir.

@derekdreery Ich glaube nicht, dass das mit einem zwingenden Kontrollfluss wie return und continue gut funktionieren würde

Beachten Sie, dass die Semantik des Operators ? bereits stabil ist. Nur die eigentliche Eigenschaft Try ist instabil, und alle Änderungen müssen denselben stabilen Effekt für Option und Result beibehalten.

@KrishnaSannasi

@derekdreery Ich glaube nicht, dass das mit einem zwingenden Kontrollfluss wie Rückkehr und

Ich stimme dieser Aussage zu.

@cuviper

Denken Sie daran, dass die Semantik des ? Betreiber sind bereits stabil. Nur die eigentliche Try-Eigenschaft ist instabil, und alle Änderungen müssen denselben stabilen Effekt für Option und Ergebnis beibehalten.

Gilt das auch epochenübergreifend?

Allgemeiner gesagt, ich glaube nicht, dass es möglich ist, Konzepte wie .await , ?/ vorzeitige Rückkehr, Option::map, Result::map, Iterator::map in rust, aber zu verstehen, dass diese alle eine gewisse Struktur haben, hilft mir definitiv, ein besserer Programmierer zu sein.

Entschuldigung, dass ich OT bin.

Ich bin mir nicht sicher, ob es einer Epoche/Edition erlaubt wäre, die Entzuckerung von ? ändern, aber das würde sicherlich viele kisteübergreifende Bedenken wie Makros erschweren.

Meine Interpretation der Stabilitätsgarantien und Epochen-RFC ist, dass das Verhalten von ? (Zucker oder andere) geändert werden könnte, aber sein aktuelles Verhalten Result Typen Option / Result muss beibehalten werden das gleiche, weil das zu brechen würde weit mehr Abwanderung erzeugen, als wir jemals hoffen könnten zu rechtfertigen.

Was wäre, wenn Option irgendwie ein Typalias für Result ? Dh type Option<T> = Result<T, NoValue> , Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x) , Option::<T>::None = Result::<T, NoValue>::Err(NoValue) ? Das würde etwas Magie erfordern, um es zu implementieren, und in der aktuellen Sprachumgebung nicht realistisch, aber vielleicht lohnt es sich, es zu erkunden.

Wir können diese Änderung nicht vornehmen, weil es Eigenschaftsimpls gibt, die davon abhängen, dass Option Result unterschiedliche Typen sind.

Wenn wir das ignorieren, könnten wir noch einen Schritt weiter gehen und bool einem Alias ​​für Result<True, False> , wobei True und False Einheitentypen sind.

Wurde in Erwägung gezogen, eine Try Impl für die Einheit () hinzuzufügen? Bei Funktionen, die nichts zurückgeben, kann eine vorzeitige Rückgabe dennoch sinnvoll sein, wenn ein Fehler in einer nicht kritischen Funktion auftritt, beispielsweise bei einem Protokollierungs-Callback. Oder wurde eine Einheit ausgeschlossen, weil es vorgezogen wird, Fehler in keiner Situation stillschweigend zu ignorieren?

Oder wurde eine Einheit ausgeschlossen, weil es vorgezogen wird, Fehler in keiner Situation stillschweigend zu ignorieren?

Dies. Wenn Sie Fehler in einem unkritischen Kontext ignorieren möchten, sollten Sie unwrap oder eine seiner Varianten verwenden.

in der Lage zu sein, ? für etwas wie foo() -> () wäre in einem Kontext der bedingten Kompilierung sehr nützlich und sollte dringend in Betracht gezogen werden. Ich denke, das unterscheidet sich jedoch von Ihrer Frage.

Sie sollten unwrap oder eine seiner Varianten verwenden.

unwrap() verursacht Panik, daher würde ich nicht empfehlen, es in unkritischen Kontexten zu verwenden. Es wäre nicht angemessen in Situationen, in denen eine einfache Rückgabe gewünscht wird. Man könnte argumentieren, dass ? wegen der expliziten, sichtbaren Verwendung von ? im Quellcode nicht völlig "still" ist. Auf diese Weise sind ? und unwrap() für Unit-Funktionen analog, nur mit unterschiedlicher Rückgabesemantik. Der einzige Unterschied, den ich sehe, ist, dass unwrap() sichtbare Nebeneffekte erzeugt (das Programm abbrechen / einen Stacktrace ausgeben) und ? dies nicht tun würden.

Im Moment ist die Ergonomie der vorzeitigen Rückkehr in Einheitenfunktionen erheblich schlechter als bei denen, die Result oder Option . Vielleicht ist dieser Stand der Dinge ist wünschenswert , da die Benutzer einen Rückgabetyp verwenden sollten Result oder Option in diesen Fällen und dies bietet Anreiz für sie , ihre API zu ändern. In jedem Fall kann es gut sein, eine Diskussion des Rückgabetyps der Einheit in die PR oder die Dokumentation aufzunehmen.

in der Lage zu sein, ? für etwas wie foo() -> () wäre in einem Kontext der bedingten Kompilierung sehr nützlich und sollte dringend in Betracht gezogen werden. Ich denke, das unterscheidet sich jedoch von Ihrer Frage.

Wie würde das funktionieren? Einfach immer zu Ok(()) auswerten und ignoriert werden?

Können Sie auch ein Beispiel dafür geben, wo Sie dies verwenden möchten?

Ja, die Idee war, dass so etwas wie MyCollection::push je nach Kompilierzeit-Konfiguration entweder einen Result<()-, AllocError>-Rückgabewert oder einen ()-Rückgabewert haben könnte, wenn die Sammlung so konfiguriert ist, dass sie nur bei Fehlern in Panik gerät. Zwischencode, der die Sammlung verwendet, sollte sich darüber keine Gedanken machen. Wenn er also einfach _immer_ ? selbst wenn der Rückgabetyp () , wäre es praktisch.

Ist das nach fast 3 Jahren einer Lösung näher gekommen?

@Lokathor das würde nur funktionieren, wenn ein Rückgabetyp Result<Result<X,Y>,Z> oder ähnliches nicht möglich wäre. Aber es ist. Somit nicht möglich.

Ich verstehe nicht, warum ein geschichtetes Ergebnis Probleme verursacht. Könnten Sie bitte näher erläutern?

Für Querverweise wurde eine alternative Formulierung auf Interna von @dureuill vorgeschlagen :

https://internals.rust-lang.org/t/a-slightly-more-general-easier-to-implement-alternative-to-the-try-trait/12034

@Lokathor
Ok, ich habe mir genauer darüber Gedanken gemacht und denke, dass ich vielleicht eine ziemlich gute Erklärung gefunden habe.

Verwenden einer Anmerkung

Das Problem ist die Interpretation des Rückgabetyps oder seltsame Anmerkungen würden den Code überladen. Es wäre möglich, würde aber das Lesen des Codes erschweren. (Voraussetzung: #[debug_result] wendet Ihr gewünschtes Verhalten an und ändert eine Funktion in Panik im Freigabemodus, anstatt ein Result::Err(...) , und entpackt Result::Ok , aber dieser Teil ist knifflig)

#[debug_result]
fn f() -> Result<X, Y>;

#[debug_result]
fn f2() -> Result<Result<A, B>, Y>;

#[debug_result]
fn g() -> Result<X, Y> {
    // #[debug_result_spoiled]
    let w = f();
    // w could have type X or `Result<X,Y>` based on a #[cfg(debug_...)]
    // the following line would currently only work half of the time
    // we would modify the behavoir of `?` to a no-op if #[cfg(not(debug_...))]
    // and `w` was marked as `#[debug_result]`-spoiled
    let x = w?;

    // but it gets worse; what's with the following:
    let y = f2();
    let z = y?;
    // it has completely different sematics based on a #[cfg(debug_...)],
    // but would (currently?) print no warnings or errors at all,
    // and the type of z would be differently.

    Ok(z)
}

Dadurch würde es schwieriger, den Code zu lesen.
Wir können das Verhalten von ? einfach in einen No-Op ändern,
vor allem, wenn der Rückgabewert von #[debug_result] in einer temporären Variablen gespeichert und später mit dem ? Operator "try-propagated" wird. Es würde die Semantik von ? wirklich verwirrend machen, weil sie von vielen "Variablen" abhängen würde, die zum "Schreiben" nicht unbedingt bekannt sind oder durch einfaches Lesen des Quellcodes schwer zu erraten sind. #[debug_result] müsste Variablen, denen Funktionswerte zugewiesen sind, verderben, aber es funktioniert nicht, wenn eine Funktion mit #[debug_result] markiert ist und eine nicht, zB wäre folgendes ein Typfehler.

// f3 is not annotated
fn f3() -> Result<X, Y>;

// later inside of a function:
   // z2 needs to be both marked spoiled and non-spoiled -> type error
   let z2 = if a() {
       f3()
   } else {
       f()
   };
   // althrough the following would be possible:
   let z2_ = if a() {
       f3()?
   } else {
       f()?
   };

Alternative: Verwendung eines neuen Typs

Eine "sauberere" Lösung könnte ein DebugResult<T, E> Typ sein, der nur dann in Panik gerät, wenn ein bestimmtes Kompilier-Flag gesetzt ist und aus einem Fehler konstruiert wird, ansonsten aber Result<T, E> . Aber auch das wäre mit dem aktuellen Vorschlag möglich, denke ich.

Antwort auf den letzten Beitrag von

@tema3210 Ich weiß. Ich wollte nur darauf hinweisen, dass die Idee von @Lokathor in der Praxis im Allgemeinen nicht funktionieren würde. Das Einzige, was teilweise funktionieren könnte, ist folgendes, aber nur mit vielen Einschränkungen, die sich meiner Meinung nach auf Dauer nicht lohnen:

// the result of the fn *must* have Try<Ok=()>
// btw. the macro could be already implemented with a proc_macro today.
#[debug_result]
fn x() -> Result<(), XError> {
  /* ..{xbody}.. */
}

// e.g. evaluates roughly to:
//..begin eval
fn x_inner() -> Result<(), XError> {
  /* ..{xbody}.. */
}

#[cfg(panic_on_err)]
fn x() {
  let _: () = x_inner().unwrap();
}
#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

//..end eval

fn y() -> Result<(), YError> {
  /* ... */
  // #[debug_result] results can't be matched on and can't be assigned to a
  // variable, they only can be used together with `?`, which would create
  // an asymetry in the type system, but could otherwise work, althrough
  // usage would be extremely restricted.
  x()?;
  // e.g. the following would fail to compile because capturing the result
  // is not allowed
  if let Err(z) = x() {
    // ...
  }
}

@zserik Ist es möglich, dass es tatsächlich eine

#[cfg(panic_on_err)]
fn x() -> Result<(), !> {
  let _: () = x_inner().unwrap();
}

#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

Ok, gute Idee.

Ich bin mir nicht sicher, ob dies etwas ist, das vor der Stabilisierung berücksichtigt werden muss, aber ich bin daran interessiert, Fehlerrücklaufverfolgungen zu implementieren, und ich denke, es beinhaltet Änderungen am Merkmal Try oder zumindest an dessen bereitgestellten Impl für Result , damit es funktioniert. Irgendwann plane ich, dies in einen RFC zu verwandeln, aber ich möchte sicherstellen, dass das Try-Merkmal nicht so stabilisiert wird, dass es unmöglich ist, dies später hinzuzufügen, falls ich eine Weile brauche, um den RFC zu schreiben. Die Grundidee ist diese.

Sie haben eine Eigenschaft, die Sie verwenden, um Tracking-Informationen zu übergeben, die Spezialisierung und ein Standard-Impl für T verwenden, um die Abwärtskompatibilität aufrechtzuerhalten

pub trait Track {
    fn track(&mut self, location: &'static Location<'static>);
}

default impl<T> Track for T {
    fn track(&mut self, _: &'static Location<'static>) {}
}

Und dann ändern Sie die Try Implementierung für Result , um track_caller und übergeben diese Informationen an den inneren Typ,

    #[track_caller]
    fn from_error(mut v: Self::Error) -> Self {
        v.track(Location::caller());
        Self::Err(v)
    }

Und dann implementieren Sie für Typen, für die Sie Backtraces sammeln möchten, Track

#[derive(Debug, Default)]
pub struct ReturnTrace {
    frames: Vec<&'static Location<'static>>,
}

impl Track for ReturnTrace {
    fn track(&mut self, location: &'static Location<'static>) {
        self.frames.push(location);
    }
}

Die Nutzung sieht so aus

#![feature(try_blocks)]
use error_return_trace::{MyResult, ReturnTrace};

fn main() {
    let trace = match one() {
        MyResult::Ok(()) => unreachable!(),
        MyResult::Err(trace) => trace,
    };

    println!("{:?}", trace);
}

fn one() -> MyResult<(), ReturnTrace> {
    try { two()? }
}

fn two() -> MyResult<(), ReturnTrace> {
    MyResult::Err(ReturnTrace::default())?
}

Und die Ausgabe einer sehr beschissenen Version eines Backtrace sieht so aus

ReturnTrace { frames: [Location { file: "examples/usage.rs", line: 18, col: 42 }, Location { file: "examples/usage.rs", line: 14, col: 16 }] }

Und hier ist ein Proof of Concept davon in Aktion https://github.com/yaahc/error-return-traces

Ich dachte, dass nur ein Fehlertyp, den wir in Try Trait Implementer konvertieren können, unzureichend sein könnte, also könnten wir eine Schnittstelle wie diese bereitstellen:

trait Try{
     type Error;
     type Ok;
     fn into_result(self)->Result<Self::Ok,Self::Error>;
     fn from_ok(val: Self::Ok)->Self;
     fn from_error<T>(val: T)->Self;
}

Beachten Sie , kann es Compiler monomorfize from_error , Vermeidung von From::from Anruf, und man kann manuell Methode impl für verschiedene Fehlertypen liefern, was die Fähigkeit von ? Operator "auspacken" anders Arten von Fehlern.

 fn from_error<T>(val: T)->Self;

Wie geschrieben, müsste der Implementierer _jede_ Größe T ohne Einschränkungen akzeptieren. Wenn Sie dies benutzerdefiniert einschränken lassen wollten, müsste es ein Merkmalsparameter wie Try<T> . Dies ähnelt der Kombination TryBlock / Bubble<Other> , die @scottmcm in https://github.com/rust-lang/rust/issues/42327#issuecomment-457353299 hatte .

Wie geschrieben, müsste der Implementierer _jede_ Größe T ohne Einschränkungen akzeptieren. Wenn Sie dies benutzerdefiniert einschränken lassen wollten, müsste es ein Merkmalsparameter wie Try<T> . Dies ähnelt der Kombination TryBlock / Bubble<Other> , die @scottmcm in #42327 (Kommentar) hatte .

Ich meinte, dass die Verwendung so aussehen sollte:

trait Try{
     //same from above
}
struct Dummy {
    a: u8,
}
struct Err1();
struct Err2();
impl Try for Dummy {
    type Ok=();
    type Error=();

    fn into_result(self)->Result<Self::Ok,Self::Error>{
        std::result::Result::Ok(())
    }
    fn from_ok(val: Self::Ok)->Self{
        Self{a: 0u8}
    }
    fn from_error<T>(val: Err1)->Self where T == Err1{
        Self{a: 1u8}
    }
    fn from_error<T>(val: Err2)->Self where T == Err2{
        Self{a: 2u8}
    }
}

Sie müssten Try und TryFromError aufteilen. Das gefällt mir mehr als der ursprüngliche Vorschlag fwiw, aber ich denke, es bräuchte ein neues RFC.

(und ich denke immer noch, es hätte "propagieren" statt "versuchen" heißen sollen, aber ich schweife ab)

@ tema3210 Ich glaube, ich verstehe Ihre Absicht, aber das ist nicht gültig Rust.

@SoniEx2

Sie müssten Try und TryFromError aufteilen.

Genau, deshalb habe ich das Design TryBlock / Bubble<Other> . Wir können diese Namenswahl diskutieren, aber die Idee war, dass es bei einer vorzeitigen Rückkehr nicht immer um _Fehler_ an sich geht. Viele der internen Iterator Methoden verwenden beispielsweise einen benutzerdefinierten LoopState Typ. Für etwas wie find ist es kein Fehler, das Gesuchte zu finden, aber wir möchten die Iteration dort stoppen.

https://github.com/rust-lang/rust/blob/3360cc3a0ea33c84d0b0b1163107b1c1acbf2a69/src/libcore/iter/mod.rs#L375 -L378

Wir können diese Namenswahl diskutieren, aber die Idee war, dass es bei einer vorzeitigen Rückkehr nicht immer um Fehler an sich geht.

genau deshalb mag ich den Namen "try" nicht und würde den Namen "propagate" bevorzugen, weil er eine "frühe" Rückkehr "propagiert" :p

(Nicht sicher, ob das Sinn macht? Das letzte Mal, als ich dies zur Sprache brachte, machte das "Propagieren" nur aus irgendeinem Grund Sinn für mich und ich war nie in der Lage, es anderen zu erklären.)

Wird diese Eigenschaft hilfreich sein, wenn Sie versuchen, das Standardverhalten von ? zu überschreiben, um einen Log-Hook hinzuzufügen, um Debug-Informationen (wie Datei-/Zeilennummer) zu protokollieren?

Derzeit wird das Überschreiben von stdlib-Makros unterstützt, aber es scheint, dass der Operator ? nicht explizit in das Makro try! . Das ist bedauerlich.

@stevenroose Um dies ausschließlich dem Merkmal Try hinzuzufügen, wäre eine Änderung des Merkmals Try erforderlich, um Dateispeicherortinformationen über den Ort einzuschließen , an dem ? "vorgekommen" ist. .

@stevenroose Um dies ausschließlich dem Merkmal Try hinzuzufügen, wäre eine Änderung des Merkmals Try erforderlich, um Dateispeicherortinformationen über den Ort einzuschließen , an dem ? "vorgekommen" ist. .

Dies ist nicht wahr, #[track_caller] kann für Eigenschaftenimpls verwendet werden, auch wenn die Eigenschaftsdefinition die Anmerkung nicht enthält

@stevenroose , um Ihre Frage zu beantworten, ja, wenn Sie jedoch daran interessiert sind, alle ? Speicherorte zu drucken, durch die sich ein Fehler ausbreitet, sollten Sie sich den Kommentar zur Fehlerrückgabe-Trace von oben ansehen

https://github.com/rust-lang/rust/issues/42327#issuecomment -619218371

Ich bin mir nicht sicher, ob das schon jemand erwähnt hat, sollen wir impl Try for bool , vielleicht Try<Ok=(), Error=FalseError> ?
Damit wir so etwas machen können

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.is_valid()?; // bool
  x.transform()?; // Result
  true
}

Jetzt muss ich den Rückgabetyp Option<()> in den meisten Fällen verwenden, in denen ich denke, dass ? den Code viel klarer machen könnte.

Ich bin mir nicht sicher, ob das schon jemand erwähnt hat, sollen wir impl Try for bool , vielleicht Try<Ok=(), Error=FalseError> ?

Dadurch würde sich Try wie der && Operator auf bool s verhalten. Wie andere oben bereits erwähnt haben, gibt es auch Anwendungsfälle für das Kurzschließen bei Erfolg, in diesem Fall sollte sich Try wie || verhalten. Da die Operatoren && und || nicht mehr lange zu tippen sind, sehe ich auch keinen großen Vorteil in dieser Implementierung.

@calebsander danke für die freundliche Antwort.
Das trifft auf einige einfache Fälle zu, aber ich glaube nicht, dass dies oft der Fall ist, insbesondere könnten wir nie eine Zuweisungsanweisung wie let x = v.get_mut(key)? in einem Ausdruck haben.
Wenn && und || immer den Zweck erfüllen würden, könnten wir genauso gut mit .unwrap_or_else() , .and_then() für Option und Error Fälle.
Könnten Sie den fließenden Code in && und || ausdrücken?

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.not_valid().not()?; // bool
  for i in x.iter() {
    if i == 1 { return true }
    if i == 2 { return false }
    debug!("get {}" i);
  }
  let y = x.transform()?; // Result
  y == 1
}

Für eine Bedingung, dass true fehlgeschlagen bedeutet, während false Erfolg bedeutet, könnte !expr? verwirrend sein, aber wir könnten expr.not()? , um den Trick zu machen (Hinweis: für ops::Try , wir haben immer false für Error )

Das trifft auf einige einfache Fälle zu, aber ich glaube nicht, dass dies oft der Fall ist, insbesondere könnten wir nie eine Zuweisungsanweisung wie let x = v.get_mut(key)? in einem Ausdruck haben.

Nun, wenn ich Ihren Vorschlag nicht falsch verstehe, würde die einfache Implementierung von Try<Ok = (), Error = FalseError> für bool dies nicht zulassen. Sie müssten außerdem impl From<NoneError> for FalseError eingeben, damit der Operator ? None in false umwandeln kann. (Und wenn Sie ? auf ein Result<T, E> innerhalb einer Funktion anwenden möchten, die ? bool zurückgibt, müssten Sie impl From<E> for FalseError pauschale Implementierung wäre problematisch.) Sie könnten auch einfach some_option().ok_or(false)? und some_result().map_err(|_| false)? wenn Sie mit dem Rückgabewert Result<bool, bool> (den Sie mit .unwrap_or_else(|err| err) ).

Abgesehen von Problemen beim Konvertieren anderer Try Fehler in bool ist die Try Implementierung, die Sie für bool vorschlagen, nur der && Operator. Diese sind zum Beispiel gleichwertig

fn using_try() -> bool {
    some_bool()?;
    some_bool()?;
    some_bool()
}

und

fn using_and() -> bool {
    some_bool() &&
    some_bool() &&
    some_bool()
}

(Zugegeben, Kontrollflüsse wie if und loop die nicht- () sind weniger einfach zu übersetzen.)

Für eine Bedingung, dass true fehlgeschlagen bedeutet, während false Erfolg bedeutet, könnte !expr? verwirrend sein, aber wir könnten expr.not()? , um den Trick zu machen (Hinweis: für ops::Try , wir haben immer false für Error )

Mir ist nicht klar, dass false den Fall Error darstellen soll. (Zum Beispiel würde Iterator.all() false kurzschließen wollen, aber Iterator::any() würde stattdessen true .) umgekehrtes Kurzschlussverhalten, indem der an ? Wert invertiert wird (und auch der Rückgabewert der Funktion invertiert wird). Aber ich glaube nicht, dass das zu sehr lesbarem Code führt. Es könnte sinnvoller sein, separate struct s And(bool) und Or(bool) , die die beiden unterschiedlichen Verhaltensweisen implementieren.

Und ähnlich, wenn Sie sich bewerben möchten? zu einem Ergebnisinnerhalb einer Funktion, die bool zurückgibt, müssten Sie Fromfür FalseError. Ich halte eine solche pauschale Umsetzung für problematisch.

Nein, ich möchte nicht impl From<T> for FalseError , vielleicht könnten wir result.ok()?

Es ist mir nicht klar, dass false den Fehlerfall darstellen sollte.

Ich denke, es ist irgendwie natürlich, wenn wir bool::then , die true auf Some abbilden.

Verzeihen Sie mir, wenn ich es verpasst habe - warum NoneError impl std::error::Error ? Dies macht es für alle Funktionen nutzlos, die Box<dyn Error> oder ähnliches zurückgeben.

Verzeihen Sie mir, wenn ich es verpasst habe - warum NoneError impl std::error::Error ? Dies macht es für alle Funktionen nutzlos, die Box<dyn Error> oder ähnliches zurückgeben.

Ich bin hier kein Experte, aber ich sehe erhebliche Probleme beim Versuch, eine nützliche Fehlermeldung für "etwas war None und Sie haben erwartet, dass es Some " (was im Grunde das ist, was Sie sind) würde hier gewinnen - eine Diagnosemeldung). Meiner Erfahrung nach war es immer am sinnvollsten, den Option::ok_or_else Kombinator zu verwenden, um stattdessen einen anderen Fehlertyp zu verwenden, da ich als aufrufenden Code im Allgemeinen sowieso viel besseren Kontext angeben kann.

Ich stimme @ErichDonGubler zu. Es ist sehr ärgerlich, ? mit Option , aber das hat einen guten Grund, und als Sprachbenutzer denke ich, dass es im Interesse aller ist, Fehler richtig zu behandeln. Diese zusätzliche Arbeit, den Fehlerkontext an None binden, anstatt nur ? tun, ist wirklich, wirklich ärgerlich, aber es erzwingt eine korrekte Kommunikation von Fehlern, was sich lohnt.

Das einzige Mal, dass ich None als Fehler interpretieren würde, ist beim sehr groben Prototyping. Ich dachte, wenn wir so etwas wie einen Option::todo_err() Aufruf hinzufügen, würde das Ok des internen Werts zurückgeben, aber bei None in Panik geraten. Es ist sehr kontraintuitiv, bis Sie die Anforderungen des "Rough Prototyping"-Modus der Codeerstellung analysieren. Es ist Ok(my_option.unwrap()) sehr ähnlich, benötigt aber keinen Ok(...) Umbruch. Darüber hinaus gibt es explizit die "Todo"-Natur an, wodurch die Absicht kommuniziert wird, diesen Code aus der Produktionslogik zu entfernen und ihn durch eine richtige Fehlerbindung zu ersetzen.

wird aber bei None in Panik geraten

Ich bin der festen Überzeugung, dass wir dies einfach bei unwrap . Wenn wir todo_err hinzugefügt haben, sollte es einen tatsächlichen Fehlertyp zurückgeben, was die Frage aufwirft, welcher Fehlertyp zurückgegeben werden soll.

Außerdem vermute ich, dass fn todo_err(self) -> Result<Self, !> das offensichtliche Problem haben würde, ! zu benötigen, um sich zu stabilisieren, was ähm, eines Tages.

Mein Anwendungsfall ist in der Tat das grobe Prototyping, bei dem ich mich nicht allzu sehr um die Fehler kümmere. Leidet nicht das ganze NoneError unter diesen Problemen, die Sie aufgelistet haben? Wenn entschieden wird, dass es existieren sollte (was meiner Meinung nach eine gute Sache ist, zumindest für das Prototyping), sollte es meiner Meinung nach Error implizieren, da es so genannt wird.

Da dieses Impl dem Typ fehlt, habe ich einfach ein .ok_or("error msg") draufgeschlagen, was auch funktioniert, aber weniger praktisch ist.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen