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.

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:

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:

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:

// without bridge impl

fn count_to_n(n: i32) -> impl Iterator<Item = i32> {
    iter!(|| {
        for i in 0..n {
            yield i;
        }
    })()
}
// 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:

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:

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.

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:

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

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:

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:

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_maps 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:

iter!(|| {
    let x = vec![1, 2, 3];
    for i in &x {
        yield *i; // ERROR (needs pinning, because borrowed)
    }
})
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:

_ = 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:

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.

// 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:

// 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:

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:

iter!({}).into_iter();
let _: impl Fn() -> impl Iterator = iter!(|| {});
Select a repo