owned this note
owned this note
Published
Linked with GitHub
---
title: "Design meeting 2025-02-12: Generators, part 2"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2025-02-12
discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-02-12
url: https://hackmd.io/iQDQ_J3MTzaKBhq1FTbToQ
---
# `iter!` Pre-RFC
This 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 as `gen {}`.
We note that several other Rust features have gone along a similar path, such as the `try!` macro that was superseded by `?` or `await!` 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.
```mermaid
stateDiagram-v2
[*] --> Iterator
Iterator: iter! that implements Iterator
state Iterator {
block_iter: No arguments\n\niter! {...}\n\nfor v in foo {}
}
[*] --> NotIterator
NotIterator: iter! that does not implement Iterator
state NotIterator {
args: Argument list\n\niter!(|| {...})()\n\n for v in foo() {}\nfor v in foo(x, y) {}\n\nCommitment to\neventual bound syntax\nfor "plain" iterator fns
into_iter: No argument list\n\niter! {...}.into_iter()\n\n for v in foo {}
opt: Optional argument list
state "Fn bounds\n\nPossible examples:\nF: IterFn()\nF: GenFn() + Unpin" as fn_bounds
flex_send: iter! {} can be Send, even\n if its iterator is not Send
ret_ref: Return captured state\nreferenced by iter block\n\nYield references to\n captured state for\n IterFn[Mut], but not\n IterFnOnce/IntoIterator
into_iter --> opt
args --> opt
opt --> fn_bounds
args --> fn_bounds
fn_bounds --> ret_ref
}
into_iter --> Iterator
[*] --> blend
Iterator --> blend
NotIterator --> blend
blend: Self-borrowing, lending traits
state blend {
lend: Yield references\nto captured state
borrow: Hold borrows\nto captured state\nacross yields
gen
gen_fn: GenFn
}
```
## Design Overview
We would introduce an `iter!` macro which creates an *iterator closure*.
An `iter!` expression evaluates to a closure that, when called, returns an `impl Iterator`.
The `impl Iterator` returned by an iterator closure is the same as today's `Iterator` trait.
The iterator will not support self borrows across yields or need to be pinned or otherwise kept immovable.
Here's an example:
```rust
fn main() {
let all_pairs = iter!(|x, y| {
for i in 0..x {
for j in 0..y {
yield (i, j);
}
}
});
for (i, j) in all_pairs(5, 10) {
println!("{i} + {j} = {}", i + j);
}
}
```
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 a `FnOnce` closure that returns an iterator that yields `i32`.
### Possible Extension: `IntoIterator` for thunks
As a small ergonomic improvement, we could add a blanket impl from `IterFnOnce() -> Item` to `IntoIterator<Item = Item>`, such as:
```rust
impl<I, Item> IntoIterator for I
where
I: IterFnOnce() -> Item
{
type Item = Item;
...
}
```
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` or `IntoIterator`.
This would let us rewrite the following to call `.into_iterator`:
```rust
// without bridge impl
fn count_to_n(n: i32) -> impl Iterator<Item = i32> {
iter!(|| {
for i in 0..n {
yield i;
}
})()
}
```
```rust
// with bridge impl
fn count_to_n(n: i32) -> impl Iterator<Item = i32> {
iter!(|| {
for i in 0..n {
yield i;
}
}).into_iter()
}
```
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:
```rust
let counter = iter!(|| {
for i in 0..100 {
yield i;
}
});
for i in counter { // instead of `for i in counter()`
println!("{i} * {i} = {}", i * i);
}
```
### 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:
```rust
let iter = iter! {
yield ();
};
for i in iter {
println!("{i}");
}
```
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 an `impl 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 with `async`.
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!` is `Send` but the iterator is no longer `Send` once iteration starts.
See Yosh Wuyts' [The Gen Auto-Trait Problem] for more details.
In `async`, we've recently had a lot of discussion about using `IntoFuture` 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.
[The Gen Auto-Trait Problem]: https://blog.yoshuawuyts.com/gen-auto-trait-problem/
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:
```rust
fn main() {
let rl_encode = iter!(|iter| {
// do run length encoding on the items yielded by iter
// and yield each value followed by the run length.
})
for x in [1u8; 513].into_iter().then(rl_encode) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ^ Produces an iterator that yields the run-length
// encoding of this array.
println!("{:?}", x);
}
}
```
[Full worked example in Playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&code=%2F%2F+This+demonstrates+run-length+encoding+using+gen+blocks+and+how+we%0A%2F%2F+might+use+this+via+a+method+combinator+on+%60Iterator%60.%0A%2F%2F%0A%2F%2F+Author%3A+TC%0A%2F%2F+Date%3A+2024-05-23%0A%0A%2F%2F%40+edition%3A+2024%0A%23%21%5Bfeature%28gen_blocks%29%5D%0Afn+rl_encode%3CI%3A+IntoIterator%3CItem+%3D+u8%3E%3E%28%0A++++xs%3A+I%2C%0A%29+-%3E+impl+Iterator%3CItem+%3D+u8%3E+%7B%0A++++gen+%7B%0A++++++++let+mut+xs+%3D+xs.into_iter%28%29%3B%0A++++++++let+%28Some%28mut+cur%29%2C+mut+n%29+%3D+%28xs.next%28%29%2C+0%29+else+%7B+return+%7D%3B%0A++++++++for+x+in+xs+%7B%0A++++++++++++if+x+%3D%3D+cur+%26%26+n+%3C+u8%3A%3AMAX+%7B%0A++++++++++++++++n+%2B%3D+1%3B%0A++++++++++++%7D+else+%7B%0A++++++++++++++++yield+n%3B+yield+cur%3B%0A++++++++++++++++%28cur%2C+n%29+%3D+%28x%2C+0%29%3B%0A++++++++++++%7D%0A++++++++%7D%0A++++++++yield+n%3B+yield+cur%3B%0A++++%7D.into_iter%28%29%0A%7D%0A%0Atrait+IteratorExt%3A+Iterator+%2B+Sized+%7B%0A++++fn+then%3CF%2C+I%3E%28self%2C+f%3A+F%29+-%3E+impl+Iterator%3CItem+%3D+Self%3A%3AItem%3E%0A++++where%0A++++++++%2F%2F+For+the+same+reasons+that+we+need+async+closures%2C+we%27d%0A++++++++%2F%2F+actually+want+to+use+gen+closures+here+in+a+real%0A++++++++%2F%2F+implementation.%0A++++++++F%3A+FnOnce%28Self%29+-%3E+I%2C%0A++++++++I%3A+IntoIterator%3CItem+%3D+Self%3A%3AItem%3E%2C%0A++++%7B%0A++++++++f%28self%29.into_iter%28%29%0A++++%7D%0A%7D%0A%0Aimpl%3CI%3A+Iterator%3E+IteratorExt+for+I+%7B%7D%0A%0Afn+main%28%29+%7B%0A++++for+x+in+%5B1u8%3B+513%5D.into_iter%28%29.then%28rl_encode%29+%7B%0A++++++++%2F%2F+++%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%5E%0A++++++++%2F%2F+++%5E+Produces+an+iterator+that+yields+the+run-length%0A++++++++%2F%2F+++++encoding+of+this+array.%0A++++++++println%21%28%22%7B%3A%3F%7D%22%2C+x%29%3B%0A++++%7D%0A%7D%0A)
Other relevant links:
- https://github.com/rust-lang/libs-team/issues/379
- https://github.com/rust-lang/libs-team/issues/379#issuecomment-2128076515
### Why not a preview crate?
Niko recently wrote about [Preview Crates](https://smallcultfollowing.com/babysteps/blog/2025/01/29/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](/7O9IyhHvRmqaMd-NS6dYyw).
The question is whether generators should be able to hold borrows from their own stack across a `yield`.
Doing so enables code like this:
```rust
gen fn interesting_items(items: Rc<RefCell<Vec<Item>>>) -> Item {
let items = items.borrow();
for item in items.iter() {
if is_interesting(item) {
// clone is needed so this is not a lending generator
yield item.clone()
}
}
}
```
The `items` variable holds a borrow that must be live for the entire lifetime of the iterator, including all the `yield` 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 not `yield 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 (or `iter!`), 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
- People: TC, Josh, nikomatsakis, tmandry, cramertj, eholk, yosh
## Meeting roles
- Minutes, driver: TC
## 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)?
- +1. Ship it
- +0. No concerns
- -0. Mild non-blocking concerns
- -1. Blocking concerns
---
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:
* An `iter!` closure produces something that can be `Send` even if the underlying Iterator is not.
* But you can do that anyway with a regular closure, `|| iter! {}`.
* An `iter!` closure can accept arguments.
* Same as above.
* When we stabilize bounds, an `IterFn` closure can capture, and maybe even yield, references to its closure.
* But it's not clear to me that we want `IterFn` instead of going for the more general `GenFn`.
* This is also assuming a lot of work in the compiler, probably comparable to what it took to get async closures.
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:
```rust
let mygen = {
let v = vec![];
iter!(|| { for x in v { yield x }})
};
for x in mygen() {}
```
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 because `IntoIter<T>` doesn't have a lifetime, I guess, it doesn't become a problem.
Example:
```rust
iter!(|| {
let x = vec![1, 2, 3];
for i in &x {
yield *i; // ERROR (needs pinning, because borrowed)
}
})
```
```rust
iter!(|| {
let x = vec![1, 2, 3];
for i in x {
yield i; // OK (no borrow)
}
})
```
(Discussion about having `iter! { .. }` return an iterator rather than an iterator closure.)
TC: If you write:
```rust
_ = iter! {
|| ()
}
// If that expands to:
let _ = gen {
|| ()
}
// That is a type error because the body of `gen` needs to have type `()`.
```
tmandry: Worst case we could require you to write this to disambiguate:
```rust
iter!({
yield 1;
})
```
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 to `impl 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.
```rust
// This will work.
fn my_iter(foo: T) -> impl Iterator {
gen!(move || {
// uses `foo` here
}).into_iter()
}
// This will not.
fn my_iter(foo: &T) -> impl Iterator {
gen!(|| {
// using `foo` leads to a compiler error
}).into_iter()
}
fn bar<F, T>(iter: F)
where
F: Fn() -> T,
T: Iterator,
{}
```
tmandry:
```rust
// this will work
fn my_iter(foo: impl Iterator) -> impl Iterator {
gen! {
for x in foo { yield x }
}
}
```
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:
```rust!
fn f(x: impl IntoIterator<..>) {}
// I.e.:
fn f(x: impl IterFnOnce() -> ..) {}
fn main() {
f(iter! { .. });
}
```
TC: The main thing, though, is that by putting the `||` inside the `iter!`, 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 implements `trait IterFn { type Item; fn next(&mut self) -> impl Iterator<Item = Self::Item>; }`
* `iter!(|x| ...)` desugars to `|x| gen { ... }`
* this returns `impl SomeFn(): Iterator<Item = X>`
* `iter!(|x| ...)` desugars to `|x| gen { ... }`, `iter!(for item in something { x.yield })`
* just `iter!(for item in something { x.yield })` that returns `Iterator`
Some of the angles
* Can the returned closure borrow from `self` and arguments?
* Should we generate an `IntoIterator` vs `Iterator`?
What are the code samples we are judging from -- use cases we want to work?
* Do we have a list of great examples?
(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
> We note that several other Rust features have gone along a similar path, such as the `try!` macro that was superseded by `?` or `await!` which was superseded by the postfix `.await` syntax.
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 the `await` syntax. Similarly `try!` was introduced into the stdlib without anticipating the later addition of `?`. The addition of `iter! {}` 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 have `gen {}`. I believe tmandry had same cases where he felt like `iter!` 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 for `gen || { .. }` to produce gen closures that returned `Unpin` 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 exact `gen` 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 than `iter!(|| {...})`, 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 than `gen` 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:
```rust
iter!({}).into_iter();
let _: impl Fn() -> impl Iterator = iter!(|| {});
```