rust-itertools / itertools

Extra iterator adaptors, iterator methods, free functions, and macros.
https://docs.rs/itertools/
Apache License 2.0
2.69k stars 303 forks source link

iproduct should allow empty product (yielding unit once) #869

Closed ijackson closed 7 months ago

ijackson commented 7 months ago

To reproduce:

use itertools::iproduct;

fn main() {
    for e in iproduct!(
    ) {
        println!("{e:?}");
    }
}

Expected output:

()

Actual output:

error: unexpected end of macro invocation
   --> src/main.rs:4:14
    |
4   |       for e in iproduct!(
    |  ______________^
5   | |     ) {
    | |_____^ missing tokens in macro arguments
    |
note: while trying to match `@`
   --> /playground/.cargo/registry/src/index.crates.io-6f17d22bba15001f/itertools-0.12.0/src/lib.rs:247:6
    |
247 |     (@flatten $I:expr,) => (
    |      ^

This wouldn't be particularly useful for humans, but macros that generate iproduct! invocations might produce this case sometimes.

Philippe-Cholet commented 7 months ago

I'm not sure.

iproduct!(0..2, 0..2) -> (0, 0), ...   yes tuples
iproduct!(0..2) --> 0, ...             not tuples (change this would be a breaking change)
iproduct!()                            then why would it be a tuple?
scottmcm commented 7 months ago

but macros that generate iproduct! invocations might produce this case sometimes.

I think it would be perfectly fine to not do this until some specific macro that wants to use it shows up to say why and how it'd be useful. Otherwise an error seems perfectly fine here.

ijackson commented 7 months ago
iproduct!(0..2, 0..2) -> (0, 0), ...   yes tuples
iproduct!(0..2) --> 0, ...             not tuples (change this would be a breaking change)
iproduct!()                            then why would it be a tuple?

Oh! The lack of tuples for a single argument is strange. My hypothetical macro generating iproduct! calls would already break.

I guess there's a workaround:

use itertools::Itertools;

macro_rules! iproduct_fully_general {
   { } => { std::iter::once(()) };
   { $e0:expr $(,)? } => { std::iter::IntoIterator::into_iter($e0).map(|v| (v,)) };
   { $e0:expr, $($e:expr),+ } => { itertools::iproduct!($e0, $($e),+) };
}  // the type depends funkily on the number of arguments, but that's true of iproduct! anyway

fn main() {
    println!("{:?}\n{:?}\n{:?}",
        iproduct_fully_general!().collect_vec(),
        iproduct_fully_general!([1,2]).collect_vec(),
        iproduct_fully_general!([1,2], [3,4]).collect_vec(),
    )
}

I really think we should fix this in a future incompatible version.

Philippe-Cholet commented 7 months ago

I rewrote Itertools::multi_cartesian_product internals so the next release is a breaking one (it will finally produce a single vector for an empty product ^^). I guess it has some sense to do that as well.