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)
Iterator::try_fold
implementierenfold
in Form von try_fold
implementiert zu haben, damit beide nicht überschrieben werden müssen.)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. 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
.
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.).
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.
Als ich try_fold
benutzte, um position
(und eine Reihe anderer Iteratormethoden) Result
genauso war (mein Gehirn mochte find
das Ding ein Err
), das ich stattdessen mit Break
und Continue
Varianten erstellt habe .
Beim Schreiben von tree_fold1
in itertools kam ich zu einem seltsamen match
dass die innere Funktion immer Err
zurückgibt . Es gibt diese seltsame Trennung, dass None
ein "Fehler" von Iterator::next()
, aber gleichzeitig "Erfolg" von einem fold
, da du zum Ende kommen musst getan werden. Und es ist sehr praktisch, ?
(nun, try!
in diesem Code, da er auf Rust 1.12 kompiliert werden muss) zu verwenden, um die Endbedingung zu propagieren.
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:
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:
TryContinue
für seinen Argumenttyp, damit der Typ eines x?
Ausdrucks immer derselbe istSelf
für einfache Fälle wie try_fold
, die denselben Typ untersuchen und zurückgeben, also sind nur T: Try
Ordnung.TryContinue
denn wenn ein als Rückgabetyp verwendeter Typ ?
in seinem Körper zulässt, sollte er auch Ok-Wrapping unterstützen.?
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:
Result
Impl, die ich nicht wirklich magLessOrGreater
Typ in der Ordering
Impl.From
TransformationStrandResult
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.throw
ist nicht so offensichtlich?
als Ok(x) => x, Err(r) => throw e.into()
verliert, die mir wirklich gefallen hatthrow;
sein, um None
(über so etwas wie impl<T> Throw<()> for Option<T>
), was viel besser ist als throw NoneError;
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)
TryContinue
sodass ok-wrapping auch für den Rückgabetyp der Methode verfügbar ist, in der es verwendet wirdTryBreak<TryFromIntError>
und TryBreak<io::Error>
für einen Typ implementieren, wenn Sie wollten.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 vonFromTry
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
:
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.
From<T: StdError> for Error
und ein spezialisierter From<NoneError> for Error
Konflikttype 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 wiefoo() -> ()
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 :
@Lokathor
Ok, ich habe mir genauer darüber Gedanken gemacht und denke, dass ich vielleicht eine ziemlich gute Erklärung gefunden habe.
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()?
};
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 wieTry<T>
. Dies ähnelt der KombinationTryBlock
/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.
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 MerkmalsTry
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
, vielleichtTry<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ährendfalse
Erfolg bedeutet, könnte!expr?
verwirrend sein, aber wir könntenexpr.not()?
, um den Trick zu machen (Hinweis: fürops::Try
, wir haben immerfalse
fürError
)
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 Ergebnis
innerhalb einer Funktion, die bool zurückgibt, müssten Sie From fü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
implstd::error::Error
? Dies macht es für alle Funktionen nutzlos, dieBox<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.
Hilfreichster Kommentar
Wie ist der aktuelle Status dieser Funktion?