or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Do you want to remove this version name and description?
Syncing
---
iter!
Pre-RFCThis document proposes to work towards stabilizing a limited version of generators under a
core::iter::iter!
macro. Stabilizing generators in this form would allow us to provide value to users today and learn from real world usage of iterators, while the macro syntax leaves room to make significant design changes in whatever eventually ships asgen {}
.We note that several other Rust features have gone along a similar path, such as the
try!
macro that was superseded by?
orawait!
which was superseded by the postfix.await
syntax.Decision flowchart
The dot represents where we are today. We can stabilize any point along the graph and later come back to stabilize along the edges leading away from it.
Design Overview
We would introduce an
iter!
macro which creates an iterator closure. Aniter!
expression evaluates to a closure that, when called, returns animpl Iterator
.The
impl Iterator
returned by an iterator closure is the same as today'sIterator
trait. The iterator will not support self borrows across yields or need to be pinned or otherwise kept immovable.Here's an example:
Note that we do not intend to stabilize iterator closure traits as part of this proposal. The underlying traits will be left as an implementation detail. For the purposes of discussion, we will refer to these traits using similar syntax to async closures, such as
IterFnOnce() -> i32
for aFnOnce
closure that returns an iterator that yieldsi32
.Possible Extension:
IntoIterator
for thunksAs a small ergonomic improvement, we could add a blanket impl from
IterFnOnce() -> Item
toIntoIterator<Item = Item>
, such as:This would help with common cases where someone wants to write an iterator closure that takes no arguments and use it in a place that expects an
Iterator
orIntoIterator
.This would let us rewrite the following to call
.into_iterator
:While there is not a huge different here, the
.into_iter()
version seems more intuitive to the author.This extension would also let you pass iterator closure thunks directly to a
for
loop:Further Possible Extension: multiple macro patterns
As another small extension, we could add another pattern to the
iter!
expansion that matches iterators without the||
and inserts them for you. This, combined with the previous extension, would let examples like the following work:We expect these kinds of iterator closures that look like plain iterators to be common, so it seems worth having a syntactic shorthand for them.
Rationale
Why Iterator Closures?
It might seem more obvious to have
iter!
evaluate directly to animpl Iterator
with no intermediate closure step. We instead recommend returning an iterator closure. This is largely as a result of what we have learned from our experience withasync
.Having a two step process between creating the iterator-like object and beginning iteration allows us to support scenarios such as where the result of
iter!
isSend
but the iterator is no longerSend
once iteration starts. See Yosh Wuyts' The Gen Auto-Trait Problem for more details. Inasync
, we've recently had a lot of discussion about usingIntoFuture
for this two stage process but decided that it is better represented through async closures. For iterators and generators, we'd like to set the same precedent from the beginning.Second, having convenient syntax for creating inline iterators will create an incentive to create more powerful combinators. With async, people very quickly started writing functions that took arguments with types like
impl FnOnce() -> F where F: Future
. Despite the clear desire to write this, these never worked particularly well until we had proper support for async closures. Still, this created an ecosystem hazard, as we wanted what Rust supported to be broadly compatible with how the ecosystem had already been experimenting. Again, using what we learned from async, we have the chance to do the right thing from the beginning with iterator closures.This approach to iterator closures supports patterns like the following:
Full worked example in Playground
Other relevant links:
Why not a preview crate?
Niko recently wrote about Preview Crates, which are a way to allow experimentation with features in blessed libraries that have access to compiler internals, similar to how std and core do. Generators would be a great fit for this approach, as the
iter!
would essentially signify a preview version of the generator feature.We propose to not block advancing support for generators on a new preview mechanism. That said, we hope that the experience with
iter!
can inform and generate support for a more general preview crates mechanism.Open Questions
Generators have a number of important questions. In this section we summarize these questions and explain what we expect to learn from experience with
iter!
to help resolve these questions.Self-borrows?
This was the primary question discussed in Design meeting 2025-01-29: Generators part 1. The question is whether generators should be able to hold borrows from their own stack across a
yield
. Doing so enables code like this:The
items
variable holds a borrow that must be live for the entire lifetime of the iterator, including all theyield
expressions.There was broad agreement that we want to support this pattern eventually. It was less clear how urgent it is to support it from the beginning. Furthermore, designs that support this generally require more usage of
Pin
, or some other hypothetical approach to supporting address-sensitive data.By having a stable version of
iter!
that does not support self borrows across yield like this, we will be able to get a much better idea of how often this becomes a problem in practice.Lending?
Looking at the previous example, what we would really like to write is
yield item
and notyield item.clone()
, but doing this requires lending generators. We have recently realized that the demand for lending iterators is likely to increase significantly with support for generators (oriter!
), as generators make it much easier to write lending patterns. This in turn raises questions of whether we want to support lending and non-lending generators, and what the migration story would be should we introduce non-lending generators first.Having the
iter!
macro would relieve pressure on this question because users would have access to many of the benefits of iterators. Thus, we would have space to explore a lending design. While there has been interest in lending iterators for a long time (it was a motivating example for GATs), to our knowledge there has not been a lot of in depth design work. We've mostly seen them as something to work out after we finish generators, but it now seems like generators would benefit significantly from having lending support to start with.The
iter!
macro would also let us get more examples of how often the desire for lending iterators occurs in the wild.Discussion
Attendance
Meeting roles
Vibe check
Josh: (Putting this at the top on the theory that we should start with it before diving in.)
Can we do a vibe check among lang members to see how we feel about saying "yes, this sounds good, let's do it"? And, in particular, doing it as proposed (without lending, and without self-borrows)?
Josh: +1. Ship it.
TC: +1. Ship it.
Tyler: I'm +1 on shipping an iter macro. The question mark for me is whether we want to ship the closure version of it. So that's the discussion point I raised.
nikomatsakis: +1.
yosh (not lang team but present): +0. Interested in hearing more (which is what this call is for).
Should we stabilize closures
tmandry: I'm trying to list out what we gain from having an
iter!
that does not evaluate to an Iterator. I think it's this:iter!
closure produces something that can beSend
even if the underlying Iterator is not.|| iter! {}
.iter!
closure can accept arguments.IterFn
closure can capture, and maybe even yield, references to its closure.IterFn
instead of going for the more generalGenFn
.TC: I want to note in particular that the ability to accept arguments is more powerful than one might imagine, as it enables these to be used in combinators that then pass in an iterator as an argument. This allows for an kind of "uber-combinator", like the example linked above. That particular example would obviate the need for a kind of otherwise useful but unpleasantly-complicated libs-api ACP:
https://github.com/rust-lang/libs-team/issues/379
Josh: While I would love to have closures and generators be orthogonal, I've heard some convincing arguments from compiler-errors and others that it's very difficult to fully separate them, for reasons having to do with lifetimes. If they are possible to separate I'd love to do so, but I will defer to type system experts for whether that's actually feasible.
TC: Yes, it's the same situation as for
AsyncFn*
.Josh: Exactly. And analogously, I wish we could have made the two completely orthogonal there, but I trust the expert arguments that we cannot.
tmandry: TC, the example I think you're referencing can be written with a normal closure returning a
gen
block. What am I missing?https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=1cd92dddd2a63039d717a07d6134b836
TC: It's the same issue as with
AsyncFn*
. The example I wrote carefully didn't lean on anything that would hit that (so that I could express the example).TC: The async closures RFC solved two problems. One was with bounds, and yes, that wouldn't be fixed until we did the bounds. But the other was literals.
tmandry: I don't see how this is compelling though:
TC: Broadly, my view here is that we already know based on our experience with async closures that we're going to end up needing to do gen closures. So I'm not sure why we wouldn't just start down the right path.
tmandry: The difference here is that we know that this doesn't do everything that we want, e.g. lending. So we're going to want to add more things anyway.
eholk: I'm torn and would be happy to go either way. I'm persuaded by the experience with async closures, but at the same time, it'd be a bit faster to ship without closures.
NM: Is the idea here that we'd fuse the closure and the iterator, or that it'd return a closure that returns an iterator?
TC: The idea is that it'd fuse them, but that we wouldn't stabilize the new bounds. But, to the point about implementation speed, we could in fact have it return a closure that returns an iterator, to start, and it would be then an "implementation limitation", and forward compatible for us to fuse them.
eholk: I do like that we could then just change the macro expansion.
NM: I'm interested in analyzing the
flat_map
case.NM: I frequently have
flat_map
s that iterate over a newly-created collection. But becauseIntoIter<T>
doesn't have a lifetime, I guess, it doesn't become a problem.Example:
(Discussion about having
iter! { .. }
return an iterator rather than an iterator closure.)TC: If you write:
tmandry: Worst case we could require you to write this to disambiguate:
TC: It's just a bit ad-hoc.
TC: More generally, I think there's a high level point here with respect to encouraging the right pattern.
Yosh: Why not have
iter!{...}
evaluate toimpl IntoIterator
?TC: It evaluates to a thunk iterator closures, and then yes, those are proposed to implement
IntoIterator
.Yosh: Won't people hit the bounds issue regardless? e.g.
tmandry:
Tyler: Ultimately we're trying to stabilize a local minumum that will allow us to reach a global maximum later on.
TC: Conceptually, we're trying to push this:
TC: The main thing, though, is that by putting the
||
inside theiter!
, we stay upstream of a lot of promising changes that we might want to make, since this leaves us in control of the expansion. Yes, if people put the bars on the outside, we could eventually get there too by issuing lints and carrying the ecosystem through migration steps, but since we already know the right answer here from our async closures experience, it seems worth skipping that step.Niko takes notes
Range of options niko sees…
iter!(|x| ...)
desugars to a special form of iterator that implementstrait IterFn { type Item; fn next(&mut self) -> impl Iterator<Item = Self::Item>; }
iter!(|x| ...)
desugars to|x| gen { ... }
impl SomeFn(): Iterator<Item = X>
iter!(|x| ...)
desugars to|x| gen { ... }
,iter!(for item in something { x.yield })
iter!(for item in something { x.yield })
that returnsIterator
Some of the angles
self
and arguments?IntoIterator
vsIterator
?What are the code samples we are judging from – use cases we want to work?
(The meeting ended here.)
Nit:
.into_iterator()
->.into_iter()
?Josh: I'm assuming everywhere that says
.into_iterator()
should say.into_iter()
?eholk: Yeah. I just changed the couple that I saw, but feel free to edit any I missed.
Nit: prior art in the language
Yosh: do note that while we had
await!
in the compiler for a while, we never actually stabilized it. That only existed in order to defer a decision on theawait
syntax. Similarlytry!
was introduced into the stdlib without anticipating the later addition of?
. The addition ofiter! {}
as a stable language feature with the prospect of being superseded does not have any precedent as far as I'm aware.eholk: I think a valuable question is whether
iter!
still has value once we havegen {}
. I believe tmandry had same cases where he felt likeiter!
would still be worthwhile.nikomatsakis: I'm trying to remember, I think that when we introduced
try!
we were already thinking about?
(and!
, which never made it). Not totally sure though.TC: To eholk's question, I don't think
iter!
would have any remaining value as long as we provided some way forgen || { .. }
to produce gen closures that returnedUnpin
generators when no self-borrows were used, either automatically or with some annotation.Nadri (chiming in, not on the call): this is typically the kind of thing I'd like to have on beta, with a migration lint or sth for when we decide to deprecate it. so ppl can try in a somewhat stable context without it needing to exist forever.
Value in accepting the
gen
syntax?Josh: Would there be value in having
iter!
accept the exactgen
syntax we think we might want to use? (NOTE: This is intended to be a boolean question, not a bikeshed. This should not be explored at length in this meeting.)eholk: Do you mean something like
iter!(gen || { yield 42; })
?Josh: Yes. With the idea in mind that we could experiment with syntax, and hopefully give people the advice of "just delete
iter(
and)
and your code should work".eholk: Definitely worth considering. I think there's a nonzero chance that
gen || {...}
will have different semantics thaniter!(|| {...})
, so that may not be possible.Josh: That's a good argument; it might be more confusing. Suggestion withdrawn.
Communication
yosh: One note I'd like to raise is that if we go with
iter!
rather thangen
for an initial stabilization of the feature we should give some thought to how we communicate this. It is easy for detractors to cast this as the language team being indecisive. I'd like us to think about how we will explain this as part of a broader process that gets features in users hand sooner.Maximally forward-compatible options
tmandry: Maximally forward-compatible options: