A syntax exploration of eventually stable Rust Iterator items

Overview

Rust Iterator Items: a syntax exploration

This crate is a thin wrapper around the unstable generator feature, allowing users to create new items that act as generators. It follows the general semantics of the Propane crate, but my interest for this crate is for interested people to fork it and come up with their own syntax for these.

The initial syntax looks like this and needs to be surrounded by an invocation of the iterator_item macro:

fn* foo() yields i32 {
    for n in 0i32..10 {
        yield n;
    }
}

Because it is a macro, it does not work as well as a native language feature would, and has worse error messages, but some effort has been made to make them usable.

Design decisions of propane

Because the semantics are heavily leaning on Propane, the following considerations also apply to this crate.

Propane is designed to allow users to write generators for the purpose of implementing iterators. For that reason, its generators are restricted in some important ways. These are the intentional design restrictions of propane (that is, these are not limitations because of bugs, they are not intended to be lifted):

  1. A propane generator becomes a function that returns an impl Iterator; the iterator interface is the only interface users can use with the generator's return type.
  2. A propane generator can only return (), it cannot yield one type and then return another interesting type. The ? operator yields the error and then, on the next resumption, returns.
  3. A propane generator implements Unpin, and cannot be self-referential (unlike async functions).

Notes on the Unpin requirement

Because of the signature of Iterator::next, it is always safe to move iterators between calls to next. This makes unboxed, self-referential iterators unsound. We did not have Pin when we designed the Iterator API.

However, in general, users can push unowned data outside of the iterator in a way they can't with futures. Futures, usually, ultimately have to be 'static, so they can spawned, but iterators usually are consumed in a way that does not require them to own all of their data.

Therefore, it is potentially the case that generators restricted to not contain self-references are sufficient for this use case. Propane intends to explore that possibility.

Comments
  • Implement RFC 2996 syntax

    Implement RFC 2996 syntax

    Opening a PR to help test things out, as another example syntax.

    This time it's the syntax from RFC 2996 which looks like this:

    gen fn foo() -> i32 {
        for n in 0i32..10 {
            yield n;
        }
    }
    
    opened by lqd 1
  • Alternative syntax suggestion: `=>` for yield type

    Alternative syntax suggestion: `=>` for yield type

    I have toyed with generator syntax myself, and my favorite syntax would be to use => for the yield type, which IMHO is very readable and succinct, using the mnemonic "one-lined arrow for the final return that happens once, two (i.e., multi)-lined arrow for yields that may happen more than once/multiple times". This syntax composes quite nicely with -> for the return type, and with async streams. Using the type names from the Generator trait:

    fn(...) -> Return { ... }                 // regular function
    fn(...) => Yield { ... }                  // generator function
    fn(...) => Yield -> Return { ... }        // generator function that has a non-() final return
    
    async fn(...) -> Return { ... }           // regular async function
    async fn(...) => Yield { ... }            // async stream
    async fn(...) => Yield -> Return { ... }  // async stream, returning non-() final return
    
    |...| => Yield -> Return { ... }          // generator closure
    async | ... | => Yield -> Return { ... }  // async stream closure
    

    Arguably, that the difference between => and -> could be hard to spot on a glance, and a separate keyword in front, in order to indicate that the body of the function is re-written into a state machine, would be preferable. In this case I would argue for the commonly proposed yield fn syntax, but still with using => to indicate the yield type:

    fn(...) -> Return { ... }                       // regular function
    yield fn(...) => Yield { ... }                  // generator function
    yield fn(...) => Yield -> Return { ... }        // generator function that has a non-() final return
    
    async fn(...) -> Return { ... }                 // regular async function
    async yield fn(...) => Yield { ... }            // async stream
    async yield fn(...) => Yield -> Return { ... }  // async stream, returning non-() final return``` 
    
    yield | ... | => Yield -> Return { ... }        // generator closure
    async yield | ... | => Yield -> Return { ... }  // async stream closure
    

    Finally, one could shoe-horn the semicoroutine resumption type R in here as well if required. My suggestion for this would be:

    yield fn (...) => Yield <= | R1, R2, ... | -> Return { ... }  // R1, R2, ... being members of the resumption tuple R
    
    opened by rolandsteiner 3
  • An alternative: just use FnMut

    An alternative: just use FnMut

    This issue is primarily meant to log the semantic alternative I prefer. As it revolves around syntax to make fn() -> impl FnMut() -> Option<T> rather than fn() -> impl Iterator<Item=T>, it doesn't quite seem correct to implement it as a macro sketch here.

    Full text: https://internals.rust-lang.org/t/rust-generators-exploration-of-potential-syntax/15586/8?u=cad97

    It could look like:

    fn merge_overlapping_intervals(input: impl Iterator<Interval>) -> impl Iterator<Item = Interval> {
        //         !1              !2
        std::iter::from_fn(move || loop {
            //                        !3
            let mut prev = input.next()?;
            for i in input {
                if prev.overlaps(&i) {
                    prev = prev.merge(&i);
                } else {
                    yield Some(prev); // !4
                    prev = i;
                }
            }
            yield Some(prev); // !4
            yield None;       // !5
        });
    }
    

    Notes:

    1. Our semicoroutine just produces impl FnMut() -> Option<Interval>. To turn this into an iterator, we use iter::from_fn.
    2. We wrap the body in a loop. This is for convenience: otherwise, the implicit tail expression would return a (), which is not Option<Interval>, leading to a type error. As loop evaluates to type !, this coerces to our closure's return type just fine. Alternatively, we could have written the last line as a tail None. Note that return None; would not work[4], as we would still have the implicit tail expression returning () after it.
    3. ? works here. As our closure returns Option<Interval>, ? just works as normal. If input.next() yields a None, we resume from the top of the function the next time the closure is called.
    4. We yield Some(interval) here, because this is a -> Option<Interval> closure.[5] Through from_fn, this fills the Iterator contract of returning items via Some(interval).
    5. We yield None to indicate the end of the iterator stream. If and only if input is fused, we could instead leave this off, looping back to the start of the closure, and input.next()? would yield None; however, because we don't require input to be fused, we have to yield it ourselves to ensure None is yielded before calling next on the input iterator again. It also just helps with code clarity to do so explicitly. This could also be written as a tail-return None rather than the closure being a big loop (see point 2).

    A more limited stabilization would avoid yield closures and instead only allow yield fn:

    yield fn merge_overlapping_intervals(input: impl Iterator<Interval>) -> Option<Interval> {
        let mut prev = input.next()?;
        for i in input {
            if prev.overlaps(&i) {
                prev = prev.merge(&i);
            } else {
                yield Some(prev);
                prev = i;
            }
        }
        yield Some(prev);
        None
    }
    

    See the linked irlo post (practically a blog post) for more context into why I think this model is a good model.

    opened by CAD97 0
  • [question] Is the marker `fn*` justified?

    [question] Is the marker `fn*` justified?

    I don't understand why a specific marker (like the star next to fn) would be needed? I don't think that it was correctly justified.

    Bjarne Stroustroup, creator of C++, said that for every new features, people wants extra marker, but once they are used to it, they want it removed. It's why there is template<typename T> void foo(t T) in C++ and not void foo<T>(t T)> in C++.

    I also don't understand why a marker need to be in the public part of the API. Does this means that if at one point I want to replace a manually written iterator by a generator (or vice-versa) this is a breaking change even if it should only be an implementation detail?

    opened by robinmoussu 2
  • Add generator blocks

    Add generator blocks

    This code is to support generator blocks (as seen on zulip!):

    type MyIterator = impl Iterator<Item=i32>;
    
    let count = 20;
    let my_iter: MyIterator = gen {
        for i in 0..count {
            yield i;
        }
    };
    

    The implementation here is a bit invasive because I wanted to work with the existing macro in case someone wants to try any sort of mixed syntax. The general idea is I convert gen { ... } to gen! { ... } everywhere before parsing, so that syn is happy parsing the code overall, and a second pass can be made by a visitor.

    opened by samsartor 9
  • Proposed alternate syntax: -> impl Iterator<Item = ReturnType>

    Proposed alternate syntax: -> impl Iterator

    I very much like the proposed yield syntax, and I think we should use that syntax.

    However, I don't want to hide the iterator type behind yields. I'd like to show the iterator type, with something like -> impl Iterator<Item = ReturnType> (or analogously for AsyncIterator).

    I acknowledge that this is more verbose. However, I think it'll make the behavior and function type more obvious, and I consider it analogous to -> Result<T, E> (which we don't hide).

    opened by joshtriplett 3
  • A mix between your initial syntax and RFC 2996

    A mix between your initial syntax and RFC 2996

    Here's another quick test :)

    Your & javascript's fn* + RFC 2996 (and regular rust) return types syntax.

    fn* foo() -> i32 {
        for n in 0i32..10 {
            yield n;
        }
    }
    

    And for fun, something more involved (but not as involved as parsing invalid syntax as if it were valid): a simile yield from some_iterator; — made easier to parse with yield #[from] some_iterator;.

    Making the first example expressible as:

    fn* foo() -> i32 {
        yield #[from] 0i32..10;
    }
    

    (and only for sync code, not async generators)

    opened by lqd 1
Owner
Esteban Kuber
Off-by-one error fixer/introducer. I 💖@rust-lang⚙️ PST tz
Esteban Kuber
The never type (the true one!) in stable Rust.

::never-say-never The ! type. In stable Rust. Since 1.14.0. Better than an enum Never {} definition would be, since an instance of type ! automagicall

Daniel Henry-Mantilla 17 Jan 3, 2023
The nightly_crimes!{} macro commits horrible crimes to allow you to enable nightly features on the stable compiler.

The nightly_crimes!{} macro commits horrible crimes to allow you to enable nightly features on the stable compiler.

Mara Bos 151 Dec 16, 2022
osu-link is a program which links osu!stable beatmaps to osu!lazer's new store format, saving you disk space.

osu-link is a program which links osu!stable beatmaps to osu!lazer's new store format, saving you disk space.

LavaDesu 2 Nov 8, 2021
A simple, stable and thread-safe implementation of a lazy value

Laizy Laizy is a Rust library that provides a simple, stable and thread-safe implementation of a Lazy Features Name Description Dependencies nightly A

Alex 5 May 15, 2022
`Debug` in rust, but only supports valid rust syntax and outputs nicely formatted using pretty-please

dbg-pls A Debug-like trait for rust that outputs properly formatted code Showcase Take the following code: let code = r#" [ "Hello, World!

Conrad Ludgate 12 Dec 22, 2022
a simple compiled language i made in rust. it uses intermediate representation (IR) instead of an abstract syntax tree (AST).

a simple compiled language i made in rust. it uses intermediate representation (IR) instead of an abstract syntax tree (AST).

null 4 Oct 3, 2022
Analogous, indented syntax for the Rust programming language.

Note: After experimenting with this in the wild, I have found representing keywords as symbols to be far less readable in large codebases. Additionall

null 42 Oct 2, 2021
This crate converts Rust compatible regex-syntax to Vim's NFA engine compatible regex.

This crate converts Rust compatible regex-syntax to Vim's NFA engine compatible regex.

kaiuri 1 Feb 11, 2022
Rust macro to use a match-like syntax as a elegant alternative to nesting if-else statement

cond Rust macro to use a match-like syntax as an elegant alternative to many if-else statements. I got the idea from empty Go switch statements. I tho

CheckM4te 3 Nov 23, 2023
tr-lang is a language that aims to bring programming language syntax closer to Turkish.

tr-lang Made with ❤️ in ???? tr-lang is a language that aims to bring programming language syntax closer to Turkish. tr-lang is a stack based language

Kerem Göksu 10 Apr 2, 2022
`grep` but with PEG patterns. Define grammars (e.g. `digit`), functions for matching. No more regex syntax!

PEG peggrep Example file demo_file: THIS LINE IS THE 1ST UPPER CASE LINE IN THIS FILE. this line is the 1st lower case line in this file. This Line Ha

IchHabeKeineNamen 3 Apr 18, 2023
First Git on Rust is reimplementation with rust in order to learn about rust, c and git.

First Git on Rust First Git on Rust is reimplementation with rust in order to learn about rust, c and git. Reference project This project refer to the

Nobkz 1 Jan 28, 2022
A stupid macro that compiles and executes Rust and spits the output directly into your Rust code

inline-rust This is a stupid macro inspired by inline-python that compiles and executes Rust and spits the output directly into your Rust code. There

William 19 Nov 29, 2022
Learn-rust - An in-depth resource to learn Rust 🦀

Learning Rust ?? Hello friend! ?? Welcome to my "Learning Rust" repo, a home for my notes as I'm learning Rust. I'm structuring everything into lesson

Lazar Nikolov 7 Jan 28, 2022
A highly modular Bitcoin Lightning library written in Rust. Its Rust-Lightning, not Rusty's Lightning!

Rust-Lightning is a Bitcoin Lightning library written in Rust. The main crate, lightning, does not handle networking, persistence, or any other I/O. Thus, it is runtime-agnostic, but users must implement basic networking logic, chain interactions, and disk storage. More information is available in the About section.

Lightning Dev Kit 850 Jan 3, 2023
Telegram bot help you to run Rust code in Telegram via Rust playground

RPG_BOT (Rust Playground Bot) Telegram bot help you to run Rust code in Telegram via Rust playground Bot interface The bot supports 3 straightforward

TheAwiteb 8 Dec 6, 2022
Playing with web dev in Rust. This is a sample Rust microservice that can be deployed on Kubernetes.

Playing with web dev in Rust. This is a sample Rust microservice that can be deployed on Kubernetes.

André Gomes 10 Nov 17, 2022
🐀 Building a federated alternative to reddit in rust

Lemmy A link aggregator / Reddit clone for the fediverse. Join Lemmy · Documentation · Report Bug · Request Feature · Releases · Code of Conduct About

LemmyNet 7.2k Jan 3, 2023
Applied offensive security with Rust

Black Hat Rust - Early Access Deep dive into offensive security with the Rust programming language Buy the book now! Summary Whether in movies or main

Sylvain Kerkour 2.2k Jan 2, 2023