rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: Add `iter!` macro

Open eholk opened this issue 3 months ago • 19 comments

Tracking issue: https://github.com/rust-lang/rust/issues/142269

Summary

Add an iter! macro to provide a better way to create iterators.

Implementing the Iterator trait directly can be tedious. Generators (see RFC 3513) are available on nightly but there are enough open design questions that generators are unlikely to be stabilized in the foreseeable future.

On the other hand, we have an iter! macro available on nightly that provides a subset of the full generator functionality. Stabilizing this version now would have several benefits:

  • Users immediately gain a more ergonomic way to write many iterators.
  • We can better inform the design of generators by getting more real-world usage experience.
  • Saves the gen { ... } and gen || { ... } syntax for a more complete feature in the future.

Rendered

eholk avatar Sep 23 '25 06:09 eholk

so std::iter::from_coroutine should be deprecated?

kennytm avatar Sep 23 '25 12:09 kennytm

This looks amazing! This does look like a nice incremental step forward.

Has any thought been given to how this might be used in library APIs? One could of course return an impl IntoIterator, but this is usually sub-optimal for various reasons. I guess this would be blocked on being able to name the type returned by iter!(...). This sort of thing is viral, so it means it would be difficult to use iter!(...) to implement any publicly exported iterators unless you're okay with returning impl Trait. Relatedly, there is the question of how to make iterators created by iter!(...) implement the various other iterator traits (e.g., ExactSizeIterator or FusedIterator) where applicable.

BurntSushi avatar Sep 23 '25 12:09 BurntSushi

so std::iter::from_coroutine should be deprecated?

from_coroutine would be useful if you wanted to convert an existing Coroutine from elsewhere to an iterator, so I don’t think so.

bluebear94 avatar Sep 23 '25 17:09 bluebear94

Can you go into more detail on why this couldn't be a library?

sanbox-irl avatar Sep 23 '25 18:09 sanbox-irl

My biggest concern is that this is supposed to be about a macro for iterator creation, but it can use yield too, which isn't available in Stable, so it's really making the subset of generators that fit the iterator pattern.

It's mildly confusing, and not what I expected from the RFC title, or what I'd usually expect from finding iter! in a project. My concern is purely a naming/organization concern though. If it was called generator! instead, then it would "make sense" that this macro allows use of the yield keyword.

EDIT: also, personally I've never had a problem making custom iterators once I learned about using core::iter::from_fn with a move closure. Maybe we could give people better messaging about that if it's such a concern.

Lokathor avatar Sep 23 '25 19:09 Lokathor

EDIT: also, personally I've never had a problem making custom iterators once I learned about using core::iter::from_fn with a move closure. Maybe we could give people better messaging about that if it's such a concern.

To me, this is very motivating -- core::iter::from_fn is strictly more powerful than this macro, as it can return borrowed data.

sanbox-irl avatar Sep 23 '25 19:09 sanbox-irl

Has any thought been given to how this might be used in library APIs? One could of course return an impl IntoIterator, but this is usually sub-optimal for various reasons. I guess this would be blocked on being able to name the type returned by iter!(...). This sort of thing is viral, so it means it would be difficult to use iter!(...) to implement any publicly exported iterators unless you're okay with returning impl Trait. Relatedly, there is the question of how to make iterators created by iter!(...) implement the various other iterator traits (e.g., ExactSizeIterator or FusedIterator) where applicable.

I could think of a few future things that could help. For example, we could add something like #[iter] fn foo() -> i32 { yield 42; }, and adding the IterFn* trait family could help too.

That doesn't help with the fundamental problem of wanting to name a concrete type though, or implement other traits.

A while back I was musing some about anonymous impls. I could see riffing on that with something like:

iter!(|| { yield 1; yield 2; yield 3; } with fn size_hint(&self) { (3, Some(3)) });

or maybe

iter!(|| { yield 1; yield 2; yield 3; } with impl DoubleEndedIterator {
    fn next_back(&self) -> Option<Self::Item> { ... }
});

So I think there are possibilities, but it's definitely future work. Even being able to write one-off iterators inline seems like an improvement though!

eholk avatar Sep 23 '25 22:09 eholk

I definitely think that the inability to pin these iterators and use them for references is more limiting than helpful. Perhaps it's worth investigating whether something like #3851 could be done to allow a supertrait of Iterator which takes Pin<&mut self> instead of &mut self.

clarfonthey avatar Sep 23 '25 22:09 clarfonthey

That doesn't help with the fundamental problem of wanting to name a concrete type though, or implement other traits.

You might be able to work around the concrete type issue with TAIT.

type MapIter = impl Iterator;

struct Map(MapIter);

fn map<I: Iterator, T, F: Fn(I::Item) -> T>(iter: I, f: F) -> Map {
    Map(iter!(|| {
        for i in iter {
            yield f(i);
        }
    })())
}

I don't think this example will work exactly, since Map and MapIter will need some type parameters, but maybe some more exploration here will yield something?

eholk avatar Sep 23 '25 23:09 eholk

My biggest concern is that this is supposed to be about a macro for iterator creation, but it can use yield too, which isn't available in Stable, so it's really making the subset of generators that fit the iterator pattern.

This RFC also includes stabilizing yield expressions, so they would be available on stable after stabilizing the iter! macro.

It's mildly confusing, and not what I expected from the RFC title, or what I'd usually expect from finding iter! in a project. My concern is purely a naming/organization concern though. If it was called generator! instead, then it would "make sense" that this macro allows use of the yield keyword.

In my mind I've kind of been saving the generator name for the more powerful version that allows self-borrows and borrowing across yield. The iter! name corresponds better to something that returns an object that implements Iterator. I'd expect generator! to return an impl Generator or something like that.

EDIT: also, personally I've never had a problem making custom iterators once I learned about using core::iter::from_fn with a move closure. Maybe we could give people better messaging about that if it's such a concern.

I think there generators or iter! shine are where you have different phases or states your iterator needs to move through, because you can encode this state in the control flow instead of having to be explicit about it. For example, with from_fn to concatenate two iterators you have to do something like:

fn concat(mut a: impl Iterator<Item = i32>, mut b: impl Iterator<Item = i32>) -> impl Iterator<Item = i32> {
    let mut first_done = false;
    core::iter::from_fn(move || {
        if !first_done {
            match a.next() {
                Some(i) => return Some(i),
                None => {
                    first_done = true;
                    return b.next();
                }
            }
        }
        b.next()
    })
}

On the other hand, with iter! it's:

fn concat(a: impl Iterator<Item = i32>, b: impl Iterator<Item = i32>) -> impl Iterator<Item = i32> {
    iter!(move || {
        for i in a {
            yield i;
        }
        for i in b {
            yield i;
       }
    })()
}

I think there's room for both approaches. If from_fn works, then great! But for some patterns, letting the compiler generate the state machine is going to be much nicer.

eholk avatar Sep 24 '25 00:09 eholk

Well if this is a macro that expanded to a new "iterator closure" type category (which I've never really heard of as being its own "thing" before, and which the RFC does not particularly define despite having a section header about it), then why not call the macro iter_closure!.

I think there generators or iter! shine are where you have different phases or states your iterator needs to move through, because you can encode this state in the control flow instead of having to be explicit about it.

Yes, using yield makes for better code. I don't dispute that at all. However, using from_fn with a fn that uses yield would give equally clear code I think. So the big "win" is the yield keyword, I would say.

Why can't the yield keyword be stabilized on its own, without this macro. That doesn't seem to be discussed in Rationale and alternatives, though the RFC is so long perhaps I missed it.

Lokathor avatar Sep 24 '25 00:09 Lokathor

Relatedly, there is the question of how to make iterators created by iter!(...) implement the various other iterator traits (e.g., ExactSizeIterator or FusedIterator) where applicable.

Perhaps FusedIterator should be implemented for you? After all, there is no way to use this feature to implement an iterator that isn’t fused. And, due to the type being unnameable except via impl Trait, there is no semver hazard…

Jules-Bertholet avatar Sep 24 '25 03:09 Jules-Bertholet

There's a rather annoying issue with coroutines where taking a shared reference to a !Sync value owned by the coroutine across a yield point results in a non-Send coroutine. I worry that increasing coroutine surface area to include iterators will make this issue more prevalent. Are there any plans to address this?

ds84182 avatar Sep 24 '25 08:09 ds84182

@Jules-Bertholet That's just one example. It doesn't really address the grander point. There are other traits you might want to implement too for an iterator exported in a library API. Notably Debug and Clone. And ExactSizeIterator.

BurntSushi avatar Sep 24 '25 11:09 BurntSushi

@Jules-Bertholet That's just one example. It doesn't really address the grander point.

And it did not intend to.

Jules-Bertholet avatar Sep 24 '25 13:09 Jules-Bertholet

Why can't the yield keyword be stabilized on its own, without this macro. That doesn't seem to be discussed in Rationale and alternatives, though the RFC is so long perhaps I missed it.

One of the things the macro does is it creates a context where yield is allowed, similar to how await operators are only allowed in async {} contexts. That's somewhat a design decision for Rust, as I think other languages let you yield anywhere. Early versions of generators in nightly Rust let you do this; a closure was a generator if it contained the yield keyword. More recent versions need a #[coroutine] attribute to make a closure into a coroutine. Besides being clearer (at least in my opinion), it also lets you do things like have a coroutine (or iterator, in the case of this RFC) that yield no items.

eholk avatar Sep 29 '25 23:09 eholk

There's a rather annoying issue with coroutines where taking a shared reference to a !Sync value owned by the coroutine across a yield point results in a non-Send coroutine. I worry that increasing coroutine surface area to include iterators will make this issue more prevalent. Are there any plans to address this?

This is one of the reasons iter! makes a closure instead of an iterator directly, since the closure can still be Send even if the iterator borrows a !Sync value across a yield point. It doesn't totally alleviate the problem, but it does give some ways around it.

eholk avatar Sep 29 '25 23:09 eholk

I feel that if this were added it will end up being a legacy duplicate of gen

TimTheBig avatar Oct 01 '25 18:10 TimTheBig

core::iter::from_fn is strictly more powerful than this macro, as it can return borrowed data.

That's actually a misunderstanding. iter! is perfectly capable of yielding borrowed data. Its limitation in this regard is exactly the same as from_fn: It can't yield data borrowed from the function itself.

I feel that if this were added it will end up being a legacy duplicate of gen

As the motivation says, the intent of the RFC is to learn as much as we can to inform the design of gen down the line. If we end up deprecating iter! with a more powerful gen feature later on, that's success. If we keep both because we end up with a design where iter! is a convenient way of returning iterators directly while gen requires pinning, that's another kind of success.

tmandry avatar Nov 26 '25 23:11 tmandry