Idea: Fallible iterator mapping with `try_map`

Fairly new Rust user here–love the language, excited to have a chance to contribute!

I've come across a use case where I'm looking for a fallible version of the Map iterator, much like the version specified for arrays in #79711. I was originally thinking about a solution independent of anything related to Try, like:

fn try_map<F, T, E>(self, transform: F) -> Result<TryMap<Self, F>, E>
    where F: FnMut(Self::Item) -> Result<T, E>

Basically just an adapted version of the existing map that's "all-or-nothing" fallible: it either succeeds or fails in aggregate.

Would love to get opinions on either implementing the above "no-try" solution generically for iterators similar to map, or extending the solution proposed in the array-related tracking issue above. If it sounds like something worth pursuing, I'd love to give it a shot.

The fundamental complication here is the expectation that iterators are lazy. .map(...) just returns a Map<...> without actually running anything. There's no way to both be lazy and know immediately that something down the road is going to be an Err.

More discussion of this problem here:

3 Likes

Oh yeah, I see what you're saying. Fortunately, looking at the post you linked, collect() seems to accomplish what I'm looking for.

In any case, thanks for the info!

Would adding a rustdoc alias for try_map to collect potentially be helpful? Or perhaps to map, and add an example that uses collect::<Result<_, _>>.

Overall, discoverability of collect-to-Result still seems lower than ideal, especially for how useful it is.

Perhaps it's actually worth adding try_collect just as a helper for this. I believe Itertools has one, actually. Additionally because (especially when used with ?) you can often have to hint the the types involved, you'd be able to write try_collect::<Vec<_>, MyError> instead of collect::<Result<Vec<_>, MyError>> or collect::<MyResult<Vec<_>>>.

(Well, at least if it's only for Result and not any impl Try, which removes the hinting benefit. Though maybe makes it work with Try + !Collect types? I'm unsure how Tryv2 and Collect would interact for non-Result types.)

3 Likes

Yeah, I've wanted this for a long time for discoverability.

Before Friday? Poorly.

But Make `array::{try_from_fn, try_map}` and `Iterator::try_find` generic over `Try` by scottmcm · Pull Request #91286 · rust-lang/rust · GitHub was just merged to fix Decide on generic-ness of functions like `Iterator::try_find` and `array::try_map` · Issue #85115 · rust-lang/rust · GitHub, which gives a way forward for this.

So like try_reduce just did, I think it could just be .try_collect::<Vec<_>>(), where the method would be fn try_collect<B>(&mut self) -> ChangeOutputType<Self::Item, B> -- and thus if the iterator's item is Option<T> the method would return Option<Vec<T>>, for an item type of Result<T, E> it'd return Result<Vec<T>, E>, and so on.

(The &mut self also helps distinguish this from collect -- you could resume using the iterator after the error, if you wanted.)

Want to make a PR? :slightly_smiling_face:

2 Likes

I very much might. (And I very much might not. If anyone else wants to beat me to the punch, please do, and stick a link in this thread.)

First, though, I need to experiment a bit with another Result-ish adapter. Specifically, Collect for Result as-is has the limitation that any yielded items before the error are just unretrievably dropped.

I want to at least see if it's possible to write a TResult<B, Item> where Item: Try, B: FromIterator<B::Output> that stores Result<B, (B, Item::Residual)> but implements Try<Output=B, Residual=Item::Residual>. Basically, ? as-if it were ChangeOutputType<Item, B>, but allow access to the output elements prior to the residual.

I don't know if we want to actually do it that way, but I'd like to have it for comparison against the simpler one.

Signatures

I did this without rustc to check my work so this is just to sketch the signatures slightly more formally:

fn try_collect<B>(&mut self) ->
    ChangeOutputType<Self::Item, B>
where
    ChangeOutputType<Self::Item, B>: FromIterator<Self::Item>,
;

fn try_collect<B>(&mut self) ->
    TryResult<Self::Item, B>
where
    B: FromIterator<<Self::Item as Try>::Output>,
;

enum TryResult<T: Try, B> {
    inner: Result<B, (B, T::Residual)>
}

impl<T: Try, B: FromIterator<T::Output>>
Try for TryResult<T, B> {
    type Output = B;
    type Residual = T::Residual;

    fn from_output(output: T::Output) -> Self {
        Self { inner: Ok(output) }
    }

    fn branch(self) -> ControlFlow<B, T::Residual> {
        match self.inner {
            Ok(output) => ControlFlow::Continue(output),
            Err((_, residual)) => ControlFlow::Break(residual),
        }
    }
}

impl<T: Try, B: FromIterator<T::Output>>
FromResidual for TryResult<T, B> {
    fn from_residual(residual: T::Residual) -> Self {
        Self { inner: Err((iter::empty().collect(), residual)) }
    }
}

A while ago I had a similar idea, which may be relevant? Never got to actually submit the RFC, because I did not have the time for it :neutral_face: