(Result<T,E>, Result<T,E>, ...) -> Result<(T, T, ...), E>

What would be a not too painful way of turning a homogeneous tuple of Result<T,E> into a single Result containing a tuple of Oks, or the first Err? That is to say

  • (Ok(a), Ok(b), Ok(3)) -> Ok((a, b, c))
  • (Ok(a), Err(b), Ok(c)) -> Err(b)
  • (Ok(a), Err(b), Err(c)) -> Err(b)

In other words, I want Haskell's sequence (traverse id) on a (Traversable) tuple of Eithers.

(The tuple will always have 3 elements.)

Except that tuples in Haskell aren’t traversable (in that way).


With try blocks, I’d suggest try { (x1?, x2?, x3?) }. I guess, the equivalent with and_then is x1.and_then(|x1| x2.and_then(|x2| x3.map(|x3| (x1, x2, x3)))).

Edit: In the meantime (without try blocks yet) you can also use a closure: (|| Ok((x1?, x2?, x3?)))().

7 Likes

Or without try blocks, but with a suitable early-return context,

Ok((tuple.0?, tuple.1?, tuple.2?))

E.g. since the error types are all the same, you could have

// There's definitely a better name
fn flatten<A, B, C, E>(tuple: (Result<A, E>, Result<B, E>, Result<C, E>)) -> Result<(A, B, C), E> {
    Ok((tuple.0?, tuple.1?, tuple.2?))
}

(It could be more generic for different error types too, but then you'd almost surely have to specify the end error type that could be made From::from every different error type.)

Or since you said homogeneous, even

// There's definitely a better name
fn flatten<O, E>(tuple: (Result<O, E>, Result<O, E>, Result<O, E>)) -> Result<(O, O, O), E> {
    Ok((tuple.0?, tuple.1?, tuple.2?))
}
2 Likes

Yes, I should simply have said 'list' instead of trying to maintain some link to the original tuple.

Actually, if it were a list (i.e. Vec) there is some higher-level way of doing it in Rust, isn't there? Don't the implementations of *Iter* for Result (and, presumably, Option) do exactly this?

The closure was actually my first attempt. When that didn't work I thought I'd ask. Still not sure what I did wrong. When I try to use it in the real-life context, I get all sorts of errors. But the equivalent named function with explicit type annotations gets me there.

Yes, for Vec you can do vec.into_iter().collect() and utilize this impl.

3 Likes

That's generally because ? has multiple extension points for Result, so inference gets confused. (This will be one of the advantages of try{}, actually, assuming the change I'm working on goes in.)

If you specifically annotate the closure, like (|| -> Result<_, TheActualErrorType> { Ok((x1?, x2?, x3?)) })(), then it'll probably work. (Because the difference that matters about the "equivalent named function" is that you had to write out the return type on it.)

Yes, I guessed it would be something like that. By the time you write explicit annotations on the closure, it becomes cleaner, more manageable, etc. as as a function, so I'm OK with that.

Or even

fn flatten<O, E>((a, b, c): (Result<O, E>, Result<O, E>, Result<O, E>)) -> Result<(O, O, O), E> {
    Ok((a?, b?, c?))
}

using pattern matching rather than indexing into the tuple.

Yup, that's exactly what I ended up doing.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.