Cartesian For Loop

Here are three (I, II, III) implementations.

import Random.seed! as seed!
import Random.shuffle as shuffle

seed!(1); # [Code I]
for i = shuffle(1:3)
    for j = shuffle(1:4)
        println("($i, $j)")
    end
end

seed!(1); # [Code II]
for i = shuffle(1:3), j = shuffle(1:4)
    println("($i, $j)") # its inner layer sequence varies!
end

seed!(1); # [Code III]
let I = shuffle(1:3), J = shuffle(1:4)
    @show I J;
    for i = I, j = J
        println("($i, $j)") # its inner layer sequence are the same.
    end
end;

In my intuition (and I believe it should be so), Code II should resolves into Code III. (cf. my intuition here).
But the current behavior implies as if Code II instead resolves into Code I, which I think is not understandable.

The style in Code I and Code II are very different—e.g., only one break is entailed to escape the for in Code II, whereas you need two in Code I.

In the first two examples, you call shuffle(1:4) creating a new iterator in each iteration of the i loop. In the last example you call shuffle(1:4) once and reuse same iterator. It’s as if

julia> print(collect(shuffle(1:4)))
[1, 2, 3, 4]
julia> print(collect(shuffle(1:4)))
[3, 1, 2, 4]
julia> print(collect(shuffle(1:4)))
[4, 3, 1, 2]

vs

julia> j=shuffle(1:4)
4-element Vector{Int64}:
 2
 1
 3
 4

julia> print(collect(j))
[2, 1, 3, 4]
julia> print(collect(j))
[2, 1, 3, 4]
julia> print(collect(j))
[2, 1, 3, 4]

collect calls included to clarify that you are iterating the same (or different) iterators, although the iterators are simply vectors here.

2 Likes

Sorry but I don’t think you’ve clarified my confusion. The collect is not relevant here.

julia> shuffle(1:3)
3-element Vector{Int64}:
 3
 1
 2

It’s already a vector.

I don’t think so.

In my way to understand it,

julia> for i = shuffle(1:9)
       println(i)
       end

8
1
3
9
2
6
4
7
5

is equivalent to

let I = shuffle(1:9)
    for i = I
        println(i)
    end
end

isn’t it?

If this logic is true, then it should extends to the Cartesian for loop in my first post. (But the behavior is distinct, I’m wondering this)

It’s the j loop that you are reinitializing for each i

1 Like

It really is, maybe we just need a louder example:

julia> struct PrintRange{R<:AbstractRange} r::R end

julia> PrintRange(r::R) where R<:AbstractRange = (println("hello, new range $(r)!"); PrintRange{R}(r))
PrintRange

julia> Base.iterate(pr::PrintRange, args...) = iterate(pr.r, args...)

julia> for i in PrintRange(1:2), j in PrintRange(3:4)
         println(i, j)
       end
hello, new range 1:2!
hello, new range 3:4!
13
14
hello, new range 3:4!
23
24

This is documented under Control Flow:

Multiple nested for loops can be combined into a single outer loop, forming the >cartesian product of its iterables:

julia> for i = 1:2, j = 3:4
           println((i, j))
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

With this syntax, iterables may still refer to outer loop variables; e.g. for i = 1:n, j = 1:i is valid. However a break statement inside such a loop exits the entire nest of loops, not just the inner one.

PrintRange(3:4) executed for each i value (Code I behavior) because it could have depended on it, not together with PrintRange(1:2) in the header prior to all iterations as you expected (Code III behavior). Code I+II and Code III are semantically different, so 1) the results can differ if the inner loop’s call does not only depend on its arguments, as you demonstrated with a PRNG, and 2) if the call is expensive, Code III saves time at no cost. Common subexpression elimination may optimize Code I+II to an equivalent of Code III if the compiler recognizes the inner call is pure, but that’s not as reliable as us writing what we mean, and sometimes we really would like side effects like sleep.

There’s nothing that can be done about the break exiting all the loops of the same block; if it only exited the inner one, then there’s no way to exit any of the other loops. break-ing out of loops separately is already an option with multiple nested blocks. continue does work across the inner loop either way because it skips to the next iteration.

2 Likes

So, Code II does not resolve into Code III, nor does it resolve into Code I.
I got it. Thank you.

I’m not sure what you mean by resolve, but all 3 versions certainly parse differently, and the multiple blocks in Code II is only practically equivalent to the single block in Code I because only the innermost for-block has code for iterations which lacks a break. If you need to write code in between for blocks or need single-level breaks, then Code II is not usable. The merged for-loop syntax of Code II is really only useful for that all-level break when you can’t easily combine all of the possibly interdependent loop iterables into one big one.

Literal meaning: resolve into something := to gradually become or be understood as something.

The info you provided is very useful!
I think the present behavior is good.