Iterator yielding tuples vs arrays

This iterator yields

    n=3
    P=Iterators.product(((1, -1) for i=1:n)...)
    foreach(println, P)

(1, 1, 1)
(-1, 1, 1)
(1, -1, 1)
(-1, -1, 1)
(1, 1, -1)
(-1, 1, -1)
(1, -1, -1)
(-1, -1, -1)

which is what i need for any n.
How should i modify the iterator to yield arrays instead of tuples, that is:

 [1, 1, 1]
[-1, 1, 1]
[1, -1, 1]
[-1, -1, 1]
[1, 1, -1]
[-1, 1, -1]
[1, -1, -1]
[-1, -1, -1]

?

foreach(println, map(collect, P))
# or
foreach(println, collect.(P))

I don’t want to collect the iterations, i would like that the iterator format the iterations as array, that is, that each element of the iterator behaves like an array, not as a tuple. This is for example what permutations in Combinatorics.jl does.
For example:

using Combinatorics
Q=permutations(1:3)
foreach(println, Q)

[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

How is this?

Q=Iterators.map(collect, P)
foreach(println, Q)
[1, 1, 1]
[-1, 1, 1]
[1, -1, 1]
[-1, -1, 1]
[1, 1, -1]
[-1, 1, -1]
[1, -1, -1]
[-1, -1, -1]

[/quote]

If don’t like that, then I think you need to reimplement Iterators.Product.

I guess this would still collect the iterations, wouldn’t it? I want a lazy iterator, very fast to create for large n without collecting anything.

FWIW

https://docs.julialang.org/en/v1/base/iterators/#Base.Iterators.map

Iterators.map(f, iterators...)

Create a lazy mapping.

Great, thanks @Jeff_Emanuel. It does exactly what i need

Right, tend to forget that map (eager) and Iterators.map (lazy) are not the same. In any case, here is a lazy alternative using Transducers.jl:

P = Iterators.product(((1, -1) for i=1:n)...) |> Transducers.Map(collect)

The think is, the usage of the iterator gets slower. This is true also for the solution given by @Jeff_Emanuel :

    using BenchmarkTools, Transducers

    n=15
    y=randn(n)

    P=Iterators.map(collect, Iterators.product(((1., -1.) for i=1:n)...)) # yields arrays
    function foo1(P, y)
        for p in P
            a=p.*y
        end
    end

    Q=Iterators.product(((1., -1.) for i=1:n)...) # yields tuples
    function foo2(Q, y)
        for q in Q
            a=q.*y
        end
    end

    R=Iterators.product(((1., -1.) for i=1:n)...) |> Transducers.Map(collect) 
    function foo3(R, y)
        for r in R
            a=r.*y
        end
    end

    @btime foo1($P, $y) #  2.486 ms (65536 allocations: 11.00 MiB)
    @btime foo2($Q, $y) #  1.150 ms (32768 allocations: 5.50 MiB)
    @btime foo3($R, $y) #  4.689 ms (98307 allocations: 20.00 MiB)

product generates tuples. If you must convert the tuples to arrays, then you have to pay for their allocation. To avoid that conversion you need to implement your own product to generate arrays instead.

https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-iteration

An alternative is to use the existing StaticArrays.jl package to wrap the tuples. A StaticArray is just a Tuple wrapped to behave like an array. It incurs no heap allocations in most situations.

julia> using StaticArrays

julia> Iterators.map(SVector, P) |> collect # remove the collect in actual use
2×2×2 Array{SVector{3, Int64}, 3}:
[:, :, 1] =
 [1, 1, 1]   [1, -1, 1]
 [-1, 1, 1]  [-1, -1, 1]

[:, :, 2] =
 [1, 1, -1]   [1, -1, -1]
 [-1, 1, -1]  [-1, -1, -1]
2 Likes

That’s a nice trick. It seems like I get the same performance as of the original iterator.

    using BenchmarkTools, Transducers, StaticArrays

    n=15
    y=randn(n)

    P=Iterators.map(collect, Iterators.product(((1., -1.) for i=1:n)...)) # yields arrays
    function foo1(P, y)
        for p in P
            a=p.*y
        end
    end

    Q=Iterators.product(((1., -1.) for i=1:n)...) # yields typles
    function foo2(Q, y)
        for q in Q
            a=q.*y
        end
    end

    R=Iterators.product(((1., -1.) for i=1:n)...) |> Transducers.Map(collect) # yields arrays
    function foo3(R, y)
        for r in R
            a=r.*y
        end
    end

    S=Iterators.map(SVector, Q)
    function foo4(S, y)
        for s in S
            a=s.*y
        end
    end

    @btime foo1($P, $y) #  2.486 ms (65536 allocations: 11.00 MiB)
    @btime foo2($Q, $y) #  1.150 ms (32768 allocations: 5.50 MiB)
    @btime foo3($R, $y) #  4.689 ms (98307 allocations: 20.00 MiB)
    @btime foo4($S, $y) #  1.137 ms (32768 allocations: 5.50 MiB)

    @btime S=Iterators.map(SVector, Q) # 371.707 ns (2 allocations: 512 bytes)