`for i = 1:M, j = 1:N` discrepancy (loop vs comprehension/generator)

Well, in your comprehension the i is closer to the body of the loop, and in your loops, that’s j.

the “closest rule” only applies to the shorthand: for i in, j in. If you have two for ..., it doesn’t matter if you write them in one line or two lines, it’s the same as:

for i in
    for j in
2 Likes

A level? When I’m writing tests I’m frequently nesting like six variables and saving five levels of indentation. And I’m totally unconcerned about the loop order in that context.

A more interesting property than indentation is how break works.

You could always write

for i for j for k for l for m for n
   # one level of indentation
end end end end end end

:wink: (I actually do sometimes write nested loops like this because @threads for i, j doesn’t work.)

I hadn’t thought about that (I rarely use break). My initial thought is that the current way makes sense:

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

vs

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

In the first case, there’s only one for, so break exits the only loop, but in the second case there are two fors, so break exits the for in which it resides.

2 Likes

I think the problem stems from the way Julia stores n-d arrays in memory. The odd execution order of list comprehension like [(i, j) for i = 1:2, j = 1:3] is IMO mostly an artifact of its memory configuration.

In Julia, an n-d array is stored in such a way that the adjacent elements in the first dimension are consecutive in memory. Consider an array A[i, j]. This memory configuration means that A[1, 2] is adjacent to A[2, 2], not A[1, 3]. Therefore, it becomes natural that

julia> [(i, j) for i = 1:2, j = 1:3]
2×3 Matrix{Tuple{Int64, Int64}}:
 (1, 1)  (1, 2)  (1, 3)
 (2, 1)  (2, 2)  (2, 3)

traverses i first in order to fill consecutive spots in memory. This conflicts with our expectation of loops, where the first iterator (i in for i in 1:2, j in 1:3) is expected to be slower changing compared to the ones come after it (j).

I think the best approach to this is assuming no inner-dependency between the iterators i and j when they are used to produce an n-d array. Instead, view them as an single iterator over all elements of such array.

I find @sijo’s argument to be persuasive, and I also tend to avoid for i, j loops unless I know that I don’t care about the order of iteration, because I have hardwired the [... for i, j] indexing of array comprehensions into my brain and don’t want to pollute it with a reversed convention.

How do you feel about this?

julia> for i in 1:4; for j in 1:i
           @show i, j
       end; end
(i, j) = (1, 1)
(i, j) = (2, 1)
(i, j) = (2, 2)
(i, j) = (3, 1)
(i, j) = (3, 2)
(i, j) = (3, 3)
(i, j) = (4, 1)
(i, j) = (4, 2)
(i, j) = (4, 3)
(i, j) = (4, 4)

Edit: Oops, bumped an old thread by accident again…

2 Likes

In the thread, weights are put on which index should iterates first, with the intrinsic difference between loop and array comprehension being addressed. After reading the entire thread, I find controversies can be reconciled if we focusing on which index should be defined first (if it should be).

Here is my summary, hope it can be helpful. If there are something wrong please point out, thanks!

Following the reply above, four cases are considered:
Case 1: for i = 1:M, j = 1:N ... end where j iterate first
Case 2: for i = 1:M for j = 1:N ... end end where j iterate first
Case 3: [... for i = 1:M, j = 1:N] where i iterate first
Case 4: [... for i = 1:M for j = 1:N] where j iterate first

For Case 3, as clearly indicated in this reply which acknowledged us that, for i = 1:M, j = 1:N in array comprehension the i and j separated by comma denotes the index of the returned array. As both [println((; i, j)) for i = j:3, j = 1:3] and [println((; i, j)) for i = 1:3, j = i:3] returned error, obviously i and j are always independent. In brief, use and think this a syntax for constructing a multi-dimension array where the “order” of iteration follows default behavior for array (column based, which means index i for “row” will iterate first if knowing the order is really an issue).

For Case 2 and 4, the dependency between iterator index is allowed, that we can simply realize the syntax in this case the order of defining variable i and j (the first defined variable should appear first).
For example:

[println((; i, j)) for i = 1:3 for j = i:3]

or

for i=1:3 for j=i:3
         println((; i, j))
    end
end

that both return

(i = 1, j = 1)
(i = 1, j = 2)
(i = 1, j = 3)
(i = 2, j = 2)
(i = 2, j = 3)
(i = 3, j = 3)

In final for Case 1, I realize it as a syntax sugar that this nested loop for i = 1:M, j = 1:N tells us i is defined first, with j defined subsequentially to be iterate, for example:

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

where in reverse (for i=j:3, j=1:3 ...) also returns ERROR: UndefVarError: j not defined.

To sum up, controversies arise only when we regard array comprehension [... for i = 1:M, j = 1:N] as a “nested for loop” which allows tasks to be dependent one by one; but it is not.

In all variant syntaxes for “nested for loop”, the index to be defined first have to appear first as dependencies between iterations or iterations of different iterators are allowed, and nested loops are executed in the order that just allows the cases that one have to wait the previous result. Thus, the order of executing nested for loop becomes intuitive considering the scope should not be refreshed (e.g., earlier defined i jumping to the next value) until all dependent variables (later defined j, k and etc. ) are all consumed.

In the case of array comprehension using comma separates indices, e.g., [... for i = 1:M, j = 1:N], the syntax shouldn’t be realized as “nested for loop” since any interdependency between iterations of different iterators is not allowed. Thus, the “order” of executing the body of array comprehension just depends on the default behavior of constructing an array in julia.

1 Like