Repository navigation
Tracking Issue for Iterator::try_collect #94047
Description
Feature gate: #![feature(iterator_try_collect)]
This is a tracking issue for adding the try_collect() method to the Iterator trait, as originally discussed here. Iterator::try_collect() is a fallible variation of Iterator::collect() analogous to similar methods for reduce and fold, among others, that provides a simpler, generic way to collect iterators of Try elements into Try-wrapped types.
Difference from Iterator::collect
In terms of functionality, the main difference between try_collect() and collect() is that try_collect() allows you to fallibly collect iterators yielding types that implement Try but not FromIterator, which collect() can't do. Concretely this means you can try_collect() iterators yielding ControlFlow<_, i32> into ControlFlow<_, Vec<i32>>, which you can't do with collect().
It's also a &mut self method instead of collect's self, so that you can resume iteration after it early exits on an error. (Like how try_fold is &mut self while fold is just self.)
Another benefit of try_collect() is discoverability. Because the discoverability of the "collect-into-Result" appears to be lower than desired, "promoting" the technique into its own method seems like a good way to increase its discoverability and reach more users who would find it useful. (One of many examples of people just not being able to find the Option: FromIterator themselves: https://users.rust-lang.org/t/try-collect-as-iter-consuming-operation/20479?u=scottmcm.)
Finally, try_collect() presents a small ergonomics benefit in terms of type hints when collecting, namely that you only have to hint at the output type you're collecting into, not the whole Try type. For example, that means you can collect an iterator yielding Result<i32, SomeComplicatedError> with try_collect::<Vec<_>>(), as opposed to having to specify the whole Result with collect::<Result<Vec<_>, _>>().
Public API
trait Iterator {
type Item;
fn try_collect<B>(&mut self) -> ChangeOutputType<Self::Item, B>
where
Self: Sized,
<Self as Iterator>::Item: Try,
<<Self as Iterator>::Item as Try>::Residual: Residual<B>,
B: FromIterator<<Self::Item as Try>::Output>;
}Steps / History
- Implementation: Add a
try_collect()helper method toIterator#94041 - Final comment period (FCP)
- Stabilization PR
Unresolved Questions
- Should it have a more complicated signature to be able to return the partial results too? (@CAD97 https://internals.rust-lang.org/t/idea-fallible-iterator-mapping-with-try-map/15715/6?u=scottmcm )
- Should it take
selfrather than&mut self, to prevent users from accidentally continuing to use the iterator after atry_collect()failure? Note that you can still continue to use the iterator if you useby_ref()first, so it's not necessarily a functionality change. - Does the name
try_collect()conflict too much with the idea of collecting that's fallible in allocation? (i.e. collecting withVec::try_reserveor similar)
Activity
ChayimFriedman2 commentedon Feb 16, 2022
The name
try_collect()may become a problem if one day we would want acollect()-like method with fallible allocations (like e.g.Vec::try_reserve()).hellow554 commentedon Feb 16, 2022
Maybe I'm blind and I can't find it, but you can already to something like this:
What is the difference to
try_collect?ChayimFriedman2 commentedon Feb 16, 2022
IIRC the point was to ease the discovery of the method.
Xiretza commentedon Feb 16, 2022
Right, the name of this function made me think it was about collecting regular (non-
Try) values into things like arrays or fallibly allocatedVecs.a-lafrance commentedon Feb 16, 2022
@hellow554 the main differences are:
try_collect()helper is meant to make the technique more discoverable by "promoting" it into its own method.Trybut notFromIteratortypes: This is the main functional difference betweentry_collect()andcollect().collect()requires theTrytype involved to also implementFromIterator, which is fine for simple cases likeOptionandResult, but doesn't allow other "Try-not-FromIterator" types likeControlFlowto be fallibly collected. In contrast,try_collect()only requires that the output type being collected into implementsFromIterator, so I cantry_collect()an iterator yieldingControlFlow<_, i32>into aControlFlow<_, Vec<i32>>, which there's no way to do withcollect().collect()takes ownership of the iterator, so you can't continue to use it after a failure occurs when collecting.try_collect()mutably borrows the iterator, so even after a failure occurs, you can continue to perform iteration (e.g. you can calltry_collect()again).Trytype when usingtry_collect()like you do withcollect(). Concretely, if I have an iterator yieldingResult<i32, SomeComplicatedError>, I can calltry_collect::<Vec<_>>()to perform fallible collection, but I have to callcollect::<Result<Vec<_>, SomeComplicatedError>()to do the same thing.Perhaps I wasn't clear enough about the difference between
try_collect()andcollect()in my original description of the feature. If it would make things more clear, I'm happy to modify the feature description up above to get the points I just mentioned across.Yeah, I see how that might become a conflict; I guess there's two distinct ways that collecting can fail. If anyone has suggestions for method names that are clearer, I'd love to hear them.
TrolledWoods commentedon Feb 17, 2022
On the point about
collectconsuming the iterator, I think you could already do.by_ref().collect(). This might be useful to prevent mistakenly using the iterator again, theby_refmakes sure the programmer actually wanted to continue using the iterator. So would it be a benefit fortry_collectto also consume the iterator and then mention usingby_refin the docs if you actually want to resume using it?hellow554 commentedon Feb 17, 2022
@a-lafrance thanks for that explanation. That helps me understanding your intent. And yes, IMHO that should be in the original post as a description :)
@TrolledWoods That was my first thought too, why don't you just use
by_ref. Then I looked at the other three functions and they are each taking&mut self.I mean... it is what is is, but
selfwould have been better IMHO :) But now let's stick to that. I doesn't really matter, does it?ChayimFriedman2 commentedon Feb 17, 2022
@hellow554 If there is an argument for them to take
selfby reference, fine. But we should not repeat past mistakes in new APIs 😃a-lafrance commentedon Feb 17, 2022
Yeah, given that the
by_ref()technique exists I can definitely see the desire to takeselfrather than&mut self, so I'm definitely willing to consider that change. I guess since the PR is already landing we can leave it as-is for now and revisit it later, so I'll add it as an unresolved question for now.scottmcm commentedon Feb 18, 2022
Note that
by_refis a huge performance footgun, since it prevents using internal iteration, as it uses the&mut I where I: Iteratorimpl that, because it works fordyn Iterator, can't use any of the generics. The benchmarks incorecan be used to see the extra cost:rust/library/core/benches/iter.rs
Lines 150 to 158 in f838a42
It's absolutely by design that anything which can early-exit takes
&mut self. That's whyallandanytake&mut self, for example, whilecountandmaxand such don't. And becausealltakes&mut self, that meanstry_foldneeds to as well in order to meet its primary purpose (#45595) of being the one internal iteration method in terms of which every singleIteratormethod can be defined.There's nothing wrong with resuming iteration after getting an error from
try_collect. And sure, you can do that wrong and try to resume iteration after getting a success fromtry_collecton a non-fused iterator, but that's exactly the same mistake as resuming iteration after gettingNonefrom.next(), so that seems fine for the same reasons.a-lafrance commentedon Feb 18, 2022
Oh that's an interesting point @scottmcm. I was wondering why some methods took
selfwhile others took&mut selfso this clears it up for me. In that case I'm personally in favor of continuing to take&mut selfrather thanself.ChayimFriedman2 commentedon Feb 18, 2022
Can this situation be improved using specialization? I.e. specialize
impl<I: Sized> Iterator for &mut Ito use internal iteration where possible? If not, I think this definitely should be mentioned on the docs, since it is not at all obvious.TrolledWoods commentedon Feb 18, 2022
Thinking about it more there's also the argument that &mut self signifies that it's intended to sometimes resume the value, whereas if it's self by value, you should expect weird behaviour when resuming, so it may be a better idea, yeah. Surprising that by_ref is a performance footgun though, that feels like something that should be fixed asap
8 remaining items
rben01 commentedon Jul 29, 2022
I think the iterator methods prefixed with
try_can all be (and should continue to be) taken to be mean "this iterator method short-circuits with the first error encountered". Thetry_inVec::try_reserveis like thetry_inRc::try_newandarray::try_from— it simply means "this thing might not work out". Since the proposed method is/would be defined on iterators and not more general Rust objects/types, I thinktry_collectis the correct name for it. (One could say that the iterator-specific meaning of thetry_prefix "shadows" the language-wide meaning.)If collecting can fail due to a
TryReserveError— which AFAIK is the only way collecting into aFromIteratorcould fail — then I think the appropriate name for that method would be something likecollect_try_reserve. And if more general errors need to be handled then I think aTryFromIteratortrait would be appropriate.Xiretza commentedon Dec 12, 2022
It's not specific to reservation of
Vecs at all, there are many data structures that would benefit from a fallibleFromIteratorand such atry_collect()method: arrays (as previously mentioned) are an obvious example, but another one would be aGridtype that can be constructed from an iterator of iterators (the rows of the grid), and which would fail if one of the rows has a different length from the others.ilyvion commentedon May 5, 2023
I don't like the name
try_collectfor this; it doesn't fit the rest of the standard library API; i.e. it doesn't use aTryFromIteratortrait, but rather aFromIteratorover items that implementTry, which is a different idea.Based on its name, I would've expected
try_collectto behave similarly to howtry_into()can be used where it only succeeds where the source can be converted to the target; i.e. I'd expect to be able totry_collect()into a[T; 10], e.g., where it would only succeed if the iterator had exactly 10 items.This seems like an unnecessary generalization of the idea of being able to
collect()toResult/Option, which already exists today and similar implementations could be added to any new type that implementsTryin the future without "stealing" thetry_collectname for a different purpose than (at least I think) seems the most natural -- i.e. fallible collection, not collecting fallible items.It might seem a more clunky name, but this seems like it should be named
collect_tryrather thantry_collect, if it should exist at all.withoutboats commentedon Jun 29, 2023
(NOT A CONTRIBUTION)
It is my strongly held position that this API should be removed from the standard library. Its usefulness is far far less than the cognitive load it brings and therefore it does not carry its weight.
The biggest issue with this API is that its signature is completely unreasonable. Return
<<Self::Item as Try>::Residual as Residua<B>l>::TryType? Multiple associated type projections to perform type changing like this is not a pattern that should be accepted into std without extremely compelling motivation for the simple reason that is totally inscrutable to most users.And the motivation to add this API to std is not compelling at all. From the API docs and this thread I see:
Trybut notFromIteratorcan be collected into (the overwhelmingly most commonTrytypes, Result and Option, do implementFromIterator).Resultwithtry_collectwhereas you do withcollect.Remember that collect is just a convenient sugar over a for loop. For that final stated advantage, I would strongly prefer just writing a for loop that doesn't short circuit on the error case.
When contrasting these benefits with the signature of this function, I do not have any doubt that this is not a motivation that justifies a signature like this. When you consider other problems brought up in this thread - like that it takes a name that could plausibly useful for a fallibly allocating collect API, which would presumably have a simpler signature and a more compelling motivation - I am honestly shocked that this API has been added to std.
Jules-Bertholet commentedon Jun 29, 2023
The
Trytrait is still unstable, it may be that the final design won't be so inscrutable. I would suspend judgment untilTryis stabilized.Another potential issue is that a hypothetical faillibly-allocating collection function might want to be called
try_collect.WhyNotHugo commentedon Aug 30, 2023
What use cases are served by
Iterator::try_collectwhich aren't already covered byResult::from_iter?obaraelijah commentedon Jun 13, 2024
The try_collect method allows you to collect results from an iterator, where each item in the iterator is a Result. If an error occurs during the iteration, try_collect will return the error immediately. Which is useful in situations where you have a collection of results and you want to handle all the errors together. By using it you can avoid having to manually handle errors in the loop that processes the results.
epidemian commentedon Jul 9, 2024
@WhyNotHugo I think the docs on itertools's
try_collectsummarize it quite well:It's just a convenience alias :)
If
Iterator::collect()were able to infer that we want to collect into aResultand allow the use of?short-circuiting immediately after, there'd be no needtry_collecti think.But, for example, this doesn't compile:
(Imagine the
dataparameter being anything iterable, ands.parse()being any fallible operation.)We just want to collect numbers into a vector, and propagate any parsing errors up.
It doesn't work even if we change the return type of the function to explicitly be
Result<(), std::num::ParseIntError>, such that the?doesn't need to do any type conversions.Some alternatives that do compile are:
Specifying that we want to collect into a
Resultvia a turbofish type param:Which is kind of noisy. Why do we need to say we're collecting into a
Resultif we're immediately propagating it up via the?that follows that.collect()?Or also including the container type on the turbofish type parameter:
Which is as noisy as the previous option, but with the added drawback that in order to know the type of
numbersyou have to chase it all the way to the end of that expression.Using
Result::from_iter()instead ofcollect():Which is kind of unidiomatic (even the docs for
Result::from_iter()use.collect()instead)Separating the collection and the
Resultpropagation into two different lines:This one is not too bad TBH. Though i'd much rather prefer a readable one-liner :)
But none of these options are as readable as the first snippet IMO. And that's the appeal of
try_collect().Using itertools'
try_collect()instead ofcollect()on the first snippet compiles just fine:epidemian commentedon Jul 9, 2024
BTW, i understand there's a naming issue with
try_collect, which makes it look like a method with error handling for failed allocations, and i agree.If we want to bikeshed on possible method names, may i suggest either:
collect_ok()(which parallels itertools' extremely-usefulfold_ok()), orcollect_results()(which ironically parallels itertools'fold_options())I personally prefer the latter. It just looks vanilla and obvious :)
MeGaGiGaGon commentedon Aug 19, 2024
While the name is being bikeshed, here's my two cents:
I would prefer some other name than
try_collect. When I searched for "rust try_collect" I was looking for something to turn anIteratordirectly into a[T; N]without having to use theTryIntoimpl onVec.I think this intuition is because of how there is the infallible
Into/From, and the fallibleTryInto/TryFrom. Thus there is the infallibleFromIterator/Iterator::collectand the fallibleTryFromIterator/Iterator::try_collect. Of course,TryFromIteratordoesn't exist, and it would be fairly niche, but there has been some very mild interest previously expressed in havingTryFromIteratorrust-lang/rfcs#1839As for the alternative name, I don't have a strong preference. Either of epidemian's suggestions are fine.
Stargateur commentedon Aug 20, 2024
Since
try_collectuse theTrytrait it's not necessary aResultfor this reason I'm opposed to_resultor_okilyvion commentedon Aug 20, 2024
Which is why I suggested the name
collect_tryin the first place way back whenPzixel commentedon Jan 30, 2025
Tbh I don't see why it would be niche - collecting in arrays is a very much desired feature, one example being stack-alloced vecs and strings. There are opinionated libraries like "smallvec" that make are a bit non-transparent and non-configurable, but I very often find myself in a situation where I want to collect an iterator without making heap allocations.