Should there be a macro or command for variable-depth nested loops?

Recently I needed N (a variable number of) nested for-loops. A vector len contained the max counter of each loop.

The dumb way would have been this:

if n==1
    for a=1:len[1]
        # Loop body
    end
elseif n==2
    for a=1:len[1], b=1:len[2]
        # Loop body
    end
# ... and so on

Instead, here’s what I came up with, which at least is less dumb:

a = ones(Int64,n)
a[n] = 0
breakwhile = false
while true
    for k=0:n-1
        if a[n-k] < len[n-k]
            a[n-k] += 1
            break
        elseif n-k == 1
            breakwhile = true
            break
        else a[n-k]=1 end
    end
    breakwhile && break
    # Loop body goes here.
end

It was fun coming up with that and it worked, but I feel like this situation arises often enough that, in a perfect world, there would be a macro or command for it.

Maybe something like “@nest for k in v”

where v is a Vector{Int64} of the loop durations, and k is a vector of counters (created locally if not already existing).

Is this just noob babbling, or a good idea? Does it already exist in some form? If not, perhaps I’ll take a stab at making a macro.

Have you read https://julialang.org/blog/2016/02/iteration? Seems like a more flexible tool.

3 Likes

It seems like Iterators.product already does what you’re looking for:

julia> for k in Iterators.product(1:2, 1:3, 1:4)
         @show k
       end
k = (1, 1, 1)
k = (2, 1, 1)
k = (1, 2, 1)
k = (2, 2, 1)
k = (1, 3, 1)
k = (2, 3, 1)                                                                                                                                                                                                                                                                                                                         
k = (1, 1, 2)
...

For example, you could do something like this:

julia> function loops(v)
         for k in Iterators.product(Base.OneTo.(v)...)
           @show k
         end
       end
loops (generic function with 1 method)

julia> loops((2, 3))
k = (1, 1)
k = (2, 1)
k = (1, 2)
k = (2, 2)
k = (1, 3)
k = (2, 3)
6 Likes

Thanks to both! I will play around with those.

While I strongly recommend using a smart multidimensional iterator like Iterators.product or CartesianIndices, we do have an internal macro that generates an arbitrary (albeit constant-at-compile-time) number of nested loops: Base.Cartesian.@nloops.

3 Likes

@nloops doesn’t appear to be a solution. For instance I have a function whose parameters determine the # of nested loops that will be needed, so I don’t know in advance what the # will be.

The docs say:

The first argument must be an integer (not a variable) specifying the number of loops.

Indeed, when I try

n = 3
@nloops n i A begin ...

an error is thrown: no method matching _nloops(::symbol, …

Is there a workaround I’m overlooking?

(Sorry if this topic was better suited for Usage)

That’s the constant-at-compile-time caveat that I list. The way around it is to use a parametric type and a generated function — the typical answer is to capture the N parameter of an array and splice it into the generated body like this:

https://docs.julialang.org/en/v1/devdocs/cartesian/#Supplying-the-number-of-expressions-1

But you can also use a ::Val{N} argument similarly, passing Val(3) to the generated function that creates the loops.

3 Likes

And use a function barrier!

Using recursion is the traditional way to do this though and probably more efficient for many use cases.

2 Likes