::next_gen
Safe generators on stable Rust.
range
iterator```rust use ::next_gen::prelude::*;
fn range (start: u8, end: u8) { let mut current = start; while current < end { yield_!(current); current += 1; } }
mkgen!(let generator = range(3, 10));
asserteq!(
generator.into_iter().collect::
```rust use ::next_gen::prelude::*;
enum NeverSome {}
/// Generator over all the primes less or equal to up_to
.
fn primesupto (upto: usize)
-> Option
mkgen!(let primes = primesupto(10000)); for prime in primes { assert!( (2usize ..) .takewhile(|&n| n.saturating_mul(n) <= prime) .all(|n| prime % n != 0) ); } ```
This is surely the most useful feature of a generator.
Consider, for instance, the following problem:
rust
fn iter_locked (elems: &'_ Mutex<Set<i32>>)
-> impl '_ + Iterator<Item = i32>
No matter how hard you try, without using unsafe
, or some other
unsafe
-using self-referential library/tool, you won't be able to feature such
a signature!
The following fails:
rust
fn iter_locked (mutexed_elems: &'_ Mutex<Set<i32>>)
-> impl '_ + Iterator<Item = i32>
{
::std::iter::from_fn({
let locked_elems = mutexed_elems.lock().unwrap();
let mut elems = locked_elems.iter().copied();
move || {
// let _ = locked_elems;
elems.next()
} // Error, borrowed `locked_elems` is not captured and is thus dropped!
})
}
rust
error[E0515]: cannot return value referencing local variable `locked_elems`
--> src/lib.rs:122:5
|
11 | / ::std::iter::from_fn({
12 | | let locked_elems = mutexed_elems.lock().unwrap();
13 | | let mut elems = locked_elems.iter().copied();
| | ------------------- `locked_elems` is borrowed here
14 | | move || {
... |
17 | | } // Error, borrowed `locked_elems` is not captured and is thus dropped!
18 | | })
| |______^ returns a value referencing data owned by the current function
|
= help: use `.collect()` to allocate the iterator
as well as this:
rust
fn iter_locked (mutexed_elems: &'_ Mutex<Set<i32>>)
-> impl '_ + Iterator<Item = i32>
{
::std::iter::from_fn({
let locked_elems = mutexed_elems.lock().unwrap();
let mut elems = locked_elems.iter().copied();
move || {
let _ = &locked_elems; // ensure `locked_elems` is captured (and thus moved)
elems.next() // Error, can't use borrow of moved value!
}
})
}
``rust
error[E0515]: cannot return value referencing local variable
lockedelems
--> src/lib.rs:144:5
|
11 | / ::std::iter::from_fn({
12 | | let locked_elems = mutexed_elems.lock().unwrap();
13 | | let mut elems = locked_elems.iter().copied();
| | -------------------
lockedelemsis borrowed here
14 | | move || {
... |
17 | | }
18 | | })
| |______^ returns a value referencing data owned by the current function
|
= help: use
.collect()` to allocate the iterator
error[E0505]: cannot move out of locked_elems
because it is borrowed
--> src/lib.rs:147:9
|
8 | fn iterlocked (mutexedelems: &'_ Mutex'1
...
11 | / ::std::iter::fromfn({
12 | | let lockedelems = mutexedelems.lock().unwrap();
13 | | let mut elems = lockedelems.iter().copied();
| | ------------------- borrow of locked_elems
occurs here
14 | | move || {
| | ^^^^^^^ move out of locked_elems
occurs here
15 | | let _ = &lockedelems; // ensure locked_elems
is captured (and thus moved)
| | ------------ move occurs due to use in closure
16 | | elems.next() // Error, can't use borrow of moved value!
17 | | }
18 | | })
| |_- returning this value requires that locked_elems
is borrowed for '1
error: aborting due to 2 previous errors ```
In other cases sub-efficient workarounds may be available
Such as when that Set
would be a Vec
instead. In that case, we can use
indices as a poorman's self-reference, with no "official" lifetimes and thus
Rust not complaining:
rust
fn iter_locked (mutexed_vec: &'_ Mutex<Vec<i32>>)
-> impl '_ + Iterator<Item = i32>
{
::std::iter::from_fn({
let locked_vec = mutexed_vec.lock().unwrap();
let mut indices = 0.. locked_vec.len();
move /* locked_vec, indices */ || {
let i = indices.next()?;
Some(locked_vec[i]) // copies, so OK.
}
})
}
let mutexed_elems = Mutex::new(vec![27, 42]);
let mut iter = iter_locked(&mutexed_elems);
assert_eq!(iter.next(), Some(27));
assert_eq!(iter.next(), Some(42));
assert_eq!(iter.next(), None);
```rust use ::next_gen::prelude::*;
fn geniterlocked (mutexedelems: &' Mutex
and voilĂ !
That #[generator] fn
is the key constructor for our safe self-referential
iterator!
Now, instantiating an iterator off a self-referential generator has a subtle
aspect, muck alike that of polling a self-referential Future
(that's what a
missing Unpin
bound means): we need to get it pinned before it can be polled!
About pinning "before use", and the two forms of pinning
Getting a Future
:
rust
let future = async { ... };
// or
let future = some_async_fn(...);
Future
in the heap (Box
ed):rust
// Pinning it in the heap (boxed):
let mut pinned_future = Box::pin(future)
// or, through an extension trait (`::futures::future::FutureExt`):
let mut pinned_future = future.boxed() // this also incidentally `dyn`-erases the future.
Now we can return it, or poll it:
rust
if true {
pinned_future.as_mut().poll(...);
}
// and/or return it:
return pinned_future;
Future
in the stack (pinned to the local scope):``rust
use ::some_lib::some_pinning_macro as stack_pinned;
// Pinning it in the "stack"
stack_pinned!(mut future);
/* the above shadows
future`, thus acting as:
let mut future = magic::Stack::pin(future); // */
// Let's rename it for clarity: let mut pinned_future = future; ```
Now we can poll it / use it within the current stack frame, but we cannot return it.
rust
pinned_future.as_mut().poll(...)
Well, it turns out that for generators it's similar:
Once you have a #[generator] fn
"generator constructor"
```rust use ::next_gen::prelude::*;
fn foo () { yield_!(42); } ```
Instantiation requires pinning, and thus:
Stack-pinning: cheap, no_std
compatible, usable within the same scope.
But it cannot be returned.
```rust mk_gen!(let mut generator = foo());
// can be used within the same scope asserteq!(generator.next(), Some(42)); asserteq!(generator.next(), None);
// but it can't be returned // return generator; /* Error, can't return borrow to local value */ ```
Heap-pinning: a bit more expensive, requires an ::alloc
ator or not
being no_std
, but the so-pinned generator can be returned.
```rust mk_gen!(let mut generator = box foo());
// can be used within the same scope if somecondition { asserteq!(generator.next(), Some(42)); assert_eq!(generator.next(), None); }
// and/or it can be returned return generator; // OK ```
So, back to our example, this is what we need to do:
```rust use ::next_gen::prelude::*;
/// We already have:
fn geniterlocked (mutexedelems: &' Mutex
/// Now let's wrap-it so that it yields a nice iterator:
fn iterlocked (mutexedelems: &'_ Mutexbox
-ing, we can directly do:
geniterlocked.callboxed((mutexedelems, ))
}
// : Pin
let mutexedelems = Mutex::new([27, 42].iter().copied().collect::
If the iter_locked()
function you are trying to implement is part of
a trait definition and thus need to name the type, at which point the
impl '_ + Iterator…
existential syntax can be problematic, you can then
use dyn
instead of impl
, at the cost of having to mention the
Pin<Box<>>
layer:
rust
// instead of
-> impl '_ + Iterator<Item = i32>
// write:
-> Pin<Box<dyn '_ + Generator<Yield = i32, Return = ()>>>
An example
```rust use ::next_gen::prelude::*;
struct Once
fn into_iter (self: Once<T>)
-> Self::IntoIter
{
#[generator(yield(T))]
fn once_generator<T> (value: T)
{
yield_!(value);
}
once_generator.call_boxed((self.0, ))
}
} asserteq!(Once(42).intoiter().next(), Some(42)); ```
See Generator
for more
info.
The crate enables no-allocation generators, thanks the usage of stack pinning. When used in that fashion, it should thus be close to zero-cost.
A lot of effort has been put into macros and an attribute macro providing the most ergonomic experience when dealing with these generators, despite the complex / subtle internals involved, such as stack pinning.
Almost no unsafe
is used, the exception being:
Stack pinning, where it uses the official ::pin_utils::pin_mut
implementation;
Using the pinning guarantee to extend a lifetime;
no_std
supportThis crates supports #![no_std]
. For it, just disable the default "std"
feature:
toml
[dependencies]
next-gen.version = "..."
next-gen.default-features = false # <- ADD THIS to disable `std`&`alloc` for `no_std` compat
next-gen.features = [
"alloc", # If your no_std platform has access to allocators.
# "std", # `default-features` bundles this.
]
Since generators and coroutines rely on the same internals, one can derive a
safe implementation of generators using the async
/ await
machinery, which
is only already in stable Rust.
A similar idea has also been implemented in https://docs.rs/genawaiter.