-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tracking Issue for Result::flatten (result_flattening
)
#70142
Comments
Add Result<Result<T, E>, E>::flatten -> Result<T, E> This PR makes this possible (modulo type inference): ```rust assert_eq!(Ok(6), Ok(Ok(6)).flatten()); ``` Tracking issue: rust-lang#70142 <sub>largely cribbed directly from <https://github.com/rust-lang/rust/pull/60256></sub>
With the stabilization of #49146, this function can now have a trivial |
I think it would also be useful to have a |
It could also be impl<T, E1, E2> for Result<Result<T, E1>, E2>
where
E1: From<E2>,
{
fn flatten(self) -> Result<T, E1> {
self?
}
} But: which error type do you convert into the other? Keeping the inner error type seemed more logical to me, under the assumption that the inner thing is what you really want, and the outer result represents further processing. An example where that occurs is using |
In #70140 (comment) @CryZe brought up:
This seems to touch on the same point as @Diggsey's and @de-vri-es's comments on converting error types. And it's easy to see why; the The only argument against this so far has been #70140 (comment) which talks about monadic similarities between I think the implementation of @de-vri-es looks right. I intend to open a PR shortly to implement this |
I don't know that (Though personally I don't think the base One example for when you have two incompatible error types, and you have to convert into a third error type: // with `E1: From<E2>`
result.map(|res| res.map_err(anyhow::Error::from)).flatten_into()?;
// with `E2: From<E1>`
result.map_err(anyhow::Error::from).flatten_into()?; For maximum ergonomics, with improved defaults and impl<T, E1, E2> for Result<Result<T, E1>, E2> {
fn flatten_into<E = E1/E2>(self) -> Result<T, E> where E: From<E2>, E: From<E1> {
Ok(self??)
}
} which would allow converting two incompatible error types into a final error type that they are both compatible with fn foo() -> Result<Result<(), impl Error>, impl Error> { ... }
fn foobar() -> anyhow::Result<()> {
foo().flatten_into()?
} |
Oh yeah good one. I guess the one I had in mind was the one @de-vri-es mentioned with an async timeout method. For example in use async_std::{fs, future};
use std::time::Duration;
let dur = Duration::from_millis(100);
let s = fs::read_to_string("./my.db").timeout(dur).await??; The return type here is
We'd want to convert from I think you're asking an intriguing question: what if both errors could somehow converge into some third error type? You suggest using
|
FWIW, the specific case where I encountered this requires the full That said, I think there is value in both, as |
Agreed. The /edit: Also, you can't have defaults for generic type arguments for functions, right? So that won't be a solution (see also #36887). |
Yeah, also |
Hello everyone ! It seems I'm a bit late to the debate but the warning on the use of flatten was published in rust 1.48.0, aka one month ago. RecapI believe impl<V, E, F> Result<Result<V, F>, E> {
fn flatten(self) -> Result<V, E>
where
F: Into<E>,
{
self.flatten_with(|e| e.into())
}
fn flatten_with<O: FnOnce(F) -> E>(self, op: O) -> Result<V, E> {
match self {
Ok(Ok(v)) => Ok(v),
Ok(Err(f)) => Err(op(f)),
Err(e) => Err(e),
}
}
} ExplanationContextI work at Stockly, a startup doing mostly web development and to better manage errors we strongly differentiate two types of error:
Most of our functions return types like
We always use the same /// Wrapper around `failure::Error` with support for additional json context
#[derive(Deref, DerefMut)]
pub struct InternalError {
inner: Box<InternalErrorInner>,
}
pub struct InternalErrorInner {
pub err: failure::Error,
pub context: BTreeMap<String, serde_json::Value>,
#[cfg(feature = "sentry")]
pub patch_event: Option<Box<dyn Fn(&mut sentry_helpers::sentry::protocol::Event) + Send + Sync>>,
_private: (),
} Fails are often enums and always context specific. For example, working in e-commerce, when a consumer wants to cancel an order, the function cancelling it returns pub enum Fail {
NotFound,
AlreadyCancelled,
DelayExpired,
} Due to the frequency of using results of results, we've wrote a helper trait called I think the signature As to which error type should be converted, I think
For the use case of converting to a third error type, I believe most of the time you can replace I can write a RFC & implementation for Happy holidays ! Side NotesHere's our complete trait impl<V, E, F> Result2 for Result<Result<V, F>, E> {
type V = V;
type E = E;
type F = F;
fn and_then2<V2, O>(self, op: O) -> Result<Result<V2, Self::F>, Self::E>
where
O: FnOnce(Self::V) -> Result<Result<V2, Self::F>, Self::E>,
{
match self {
Ok(Ok(val)) => op(val),
Ok(Err(fail)) => Ok(Err(fail)),
Err(err) => Err(err),
}
}
fn flatten(self) -> Result<Self::V, Self::E>
where
Self::F: Into<Self::E>,
{
self.flatten_with(|e| e.into())
}
fn flatten_with<O: FnOnce(Self::F) -> Self::E>(self, op: O) -> Result<Self::V, Self::E> {
match self {
Ok(Ok(v)) => Ok(v),
Ok(Err(f)) => Err(op(f)),
Err(e) => Err(e),
}
}
fn map2<V2, O: FnOnce(Self::V) -> V2>(self, op: O) -> Result<Result<V2, F>, Self::E> {
match self {
Ok(r) => Ok(r.map(op)),
Err(e) => Err(e),
}
}
fn map_err2<F2, O: FnOnce(Self::F) -> F2>(self, op: O) -> Result<Result<Self::V, F2>, Self::E> {
match self {
Ok(r) => Ok(r.map_err(op)),
Err(e) => Err(e),
}
}
fn transpose(self) -> Result<Result<Self::V, Self::E>, Self::F> {
match self {
Ok(Ok(val)) => Ok(Ok(val)),
Ok(Err(fail)) => Err(fail),
Err(err) => Ok(Err(err)),
}
}
} |
Having just been asked if async_std::future::timeout(timeout, conn).await?? I wonder if This is, naturally a good spot for result flattening. My intuition says that This leaves a flatten on the table which is sorta like
I suspect that flatten-to-inner is more common from libraries and flatten-to-outer is more common in applications. So I wonder if we don't actually want two:
And uh, maybe it is worth taking Edit: and, which ever the case, |
I am strongly in favour of this position. I argue that the ambiguity of implicitly converting errors in My preference would be to only have |
The problem with that workaround is if you need to convert the inner error type. You end up with For that reason, although I agree that plain |
I have no qualms with additional methods. I realise my wording suggested I would be against methods that did implicit conversions, and while I might not personally use them, I'm not inclined to argue with their inclusion. |
I think flatten having an inner-error vs outer-error type preference will absolutely catch some people by surprise when they wanted the other one, and so I'm in favor of impl<T, E> Result<Result<T, E>, E> {
pub fn flatten(self) -> Result<T, E>;
}
impl<T, E1, E2> Result<Result<T, E1>, E2> {
pub fn flatten_into<E>(self) -> Result<T, E> where E1: Into<E>, E2: Into<E>;
} matching the |
If both errors need converting, Assuming If we ever get let flattened_result = try { Ok(r??) }; Note that with let future_with_flattened_result = async {
let dur = Duration::from_millis(100);
let s = fs::read_to_string("./my.db").timeout(dur).await??;
Ok(s)
}; And it is possible to (ab)-use closures for this: let flattened_result = (|| anyhow::Ok(r??))(); To aid with type-inference, Given these - relatively lightweight - alternatives, my vote would also go down for only adding |
That's only usable if you want to immediately return the error. If you just want to get back a |
On stable Rust, you can emulate |
I was looking for something like Flattening over the errors would be very useful! impl<T, E> Result<T, Result<T, E>> {
pub fn flatten(self) -> Result<T, E>;
} I guess inclusion in this issue would be too late? Should I open a follow-up? |
FWIW, it seems like |
@SuperFluffy your use case might be satisfied with |
Note that it's even simpler than that, thanks to #70941 : let flattened_result = try { r?? }; (Coincidentally exactly the same number of characters as |
@scottmcm I believe with // don't worry too much about these imports, they provide things like concurrency and timeouts
use async_time::prelude::*;
use async_concurrency::prelude::*;
use async_std::fs;
let fut1 = fs::read_to_string("some-file.txt")
.timeout(Duration::from_secs(2))
.flatten(); // flatten `io::Result<io::Result<String>>` to `io::Result<String>`
// This returns `io::Result<String>`
let fut2 = fs::read_to_string("other-file.txt");
// Because both futures now have the same signature, we can `race` them.
let s = (fut1, fut2).race().await?; without // don't worry too much about these imports, they provide things like concurrency and timeouts
use async_time::prelude::*;
use async_concurrency::prelude::*;
use async_std::fs;
// flatten `io::Result<io::Result<String>>` to `io::Result<String>`
let fut1 = try {
fs::read_to_string("some-file.txt")
.timeout(Duration::from_secs(2))??
};
// This returns `io::Result<String>`
let fut2 = fs::read_to_string("other-file.txt");
// Because both futures now have the same signature, we can `race` them.
let s = (fut1, fut2).race().await?; Imo both approaches compliment each other, and which is the better choice will depend on the situation. |
I just wrote flatten_into as a 5 sec hack when I discovered flatten and I ended up writing map_err in a nested map and my nice and clean function chain looked worse than without flatten. I independently wrote char for char the same function. I think this shows that flatten_into is an obvious step when adding flatten. |
@Nemo157 is it just docs and a stabilization PR blocking this? |
make unstable Result::flatten a const fn This method is still unstable (tracked at rust-lang#70142), but there's no reason I can see for it not to be const -- after all, `Option::flatten` is const. So let's make the `Result` one `const` as well, under the same feature gate. Cc rust-lang#70142
make unstable Result::flatten a const fn This method is still unstable (tracked at rust-lang#70142), but there's no reason I can see for it not to be const -- after all, `Option::flatten` is const. So let's make the `Result` one `const` as well, under the same feature gate. Cc rust-lang#70142
Rollup merge of rust-lang#130692 - RalfJung:result-flatten, r=Noratrieb make unstable Result::flatten a const fn This method is still unstable (tracked at rust-lang#70142), but there's no reason I can see for it not to be const -- after all, `Option::flatten` is const. So let's make the `Result` one `const` as well, under the same feature gate. Cc rust-lang#70142
I think
and
are a lot more readable than
and constructs like that. Imagine explaining |
This is a tracking issue for the
Result::flatten
API.The feature gate for the issue is
#![feature(result_flattening)]
.About tracking issues
Tracking issues are used to record the overall progress of implementation.
They are also uses as hubs connecting to other relevant issues, e.g., bugs or open design questions.
A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature.
Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.
Steps
Unresolved Questions
Implementation history
The text was updated successfully, but these errors were encountered: