Determining dimension of array via position of "for" and "," in list comprehension

I am new to Julia and noticed the following when playing around with list comprehensions:
Given some integers a, b,c, the comprehension
[[i,j] for i=1:a for j=1:b]
returns an a*b \times 1 array while
[[i,j] for i=1:a, j=1:b]
returns an a \times b array of pairs.

Similarly for three generators,
[[i,j,k] for i=1:a for j=1:b for k=1:c]
returns an a*b*c \times 1 array while
[[i,j] for i=1:a, j=1:b, k=1:c]
returns an a \times b \times c array.

This lead me to believe that I could determine the shape of the “outer array” that results from the above kind of comprehensions by connecting the generators (which is how I call the expressions ‘i=1:a’ etc.) by “,” and “for” in the right way. For instance,
[[i,j,k] for i=1:a, j=1:b for k=1:c]
would return a a \times b*c array and
[[i,j,k] for i=1:a, j=1:b for k=1:c]
a a*b \times c dimensional array, etc.

But this is not the case. Instead any call involving more than one “for” expression returns a vector, with the ordering of the elements depending on the position of the “for” expressions.

This brings up two questions:

  1. What is the simplest way of generating the kinds of arrays that I would have assumed the above calls generate?
  2. Is there a simple rule for determining what kind of vector (or array) a specific combination of “for” and “,” will generate under the actual syntax?

Thanks!

You could reshape (which does not allocate):

julia> x = [[i,j,k] for i=1:2, j=1:3, k=1:4]
2×3×4 Array{Vector{Int64}, 3}:
[:, :, 1] =
 [1, 1, 1]  [1, 2, 1]  [1, 3, 1]
 [2, 1, 1]  [2, 2, 1]  [2, 3, 1]

[:, :, 2] =
 [1, 1, 2]  [1, 2, 2]  [1, 3, 2]
 [2, 1, 2]  [2, 2, 2]  [2, 3, 2]

[:, :, 3] =
 [1, 1, 3]  [1, 2, 3]  [1, 3, 3]
 [2, 1, 3]  [2, 2, 3]  [2, 3, 3]

[:, :, 4] =
 [1, 1, 4]  [1, 2, 4]  [1, 3, 4]
 [2, 1, 4]  [2, 2, 4]  [2, 3, 4]

julia> y=reshape(x, (6,4))
6×4 Matrix{Vector{Int64}}:
 [1, 1, 1]  [1, 1, 2]  [1, 1, 3]  [1, 1, 4]
 [2, 1, 1]  [2, 1, 2]  [2, 1, 3]  [2, 1, 4]
 [1, 2, 1]  [1, 2, 2]  [1, 2, 3]  [1, 2, 4]
 [2, 2, 1]  [2, 2, 2]  [2, 2, 3]  [2, 2, 4]
 [1, 3, 1]  [1, 3, 2]  [1, 3, 3]  [1, 3, 4]
 [2, 3, 1]  [2, 3, 2]  [2, 3, 3]  [2, 3, 4]

julia> z=reshape(x, (2, 12))
2×12 Matrix{Vector{Int64}}:
 [1, 1, 1]  [1, 2, 1]  [1, 3, 1]  [1, 1, 2]  [1, 2, 2]  [1, 3, 2]  [1, 1, 3]  [1, 2, 3]  [1, 3, 3]  [1, 1, 4]  [1, 2, 4]  [1, 3, 4]
 [2, 1, 1]  [2, 2, 1]  [2, 3, 1]  [2, 1, 2]  [2, 2, 2]  [2, 3, 2]  [2, 1, 3]  [2, 2, 3]  [2, 3, 3]  [2, 1, 4]  [2, 2, 4]  [2, 3, 4]

But for my taste, there is an ambiguity in this that I would like to avoid: What exactly is the layout of the 2x12 array supposed to be? If it were me, I would just write out the nested loop and specify “by hand” where each element [i,j,k] will go.

2 Likes

Nested comprehensions allow for instance:

a =3; b=5; c=4

M = [[[i,j,k] for i=1:a, j=1:b] for k=1:c]  # c-Vector of a x b Matrices

N = [[[i,j,k] for i=1:a] for j=1:b, k=1:c]  # b x c Matrix of a-Vectors
1 Like

I’m not entirely sure that mixing for and , in comprehensions is intentionally allowed. The point of multiple fors is to allow the later clauses to depend on the earlier ones, e.g.:

julia> [(i, j) for i=1:4 for j=i+1:4]
6-element Vector{Tuple{Int64, Int64}}:
 (1, 2)
 (1, 3)
 (1, 4)
 (2, 3)
 (2, 4)
 (3, 4)

This must produce a vector of values rather than a matrix since the resulting “shape” isn’t rectangular. When using , to separate the clauses, they are independent and the result must be rectangular. As soon as there’s more than one for iteration, you cannot in general guarantee rectangular shape, which may be the reasoning for this behavior. @jeff.bezanson implemented it, so he may be able to say more.

3 Likes

Nice, that’s clean!

Thanks for the reply. That makes a lot of sense. I guess the best practice is to either work with reshape or with nested comprehensions (as suggested by @rafael.guerra) and in doing so follow the rule to “keep the number of fors in every single comprehension to exactly the depth of the nesting of the indices within this comprehension”