Broadcast two vectors to form a matrix

I made a simple function:

f(x,y)=x+y
res=[f.(x,1:9) for x in 1:9]

But, now it outputs a vector of vectors. I’d like instead for it to be a matrix.
Actually, I’m still very new to julia and I’ve grown pretty confused on weather I should use for loops or not; as much as I really like the broadcasting function for readability, nested for loops are also not that hard to read. My main concern is I have many functions like the above, and my simple approach has always been to not write a function and just do:

res=reshape(1:9,:,1) .+ reshape(1:9,1,:)

Having a bunch of scalar functions instead is much easier to troubleshoot, and change if required. But my functions sometimes need 3d output and do things like:

res=fill(NaN,9,9,9)
for i=1:9
    res[:,:,i]= i .+ reshape(1:9,:,1) .+ reshape(1:9,1,:)
end

What’s the best way to handle things like this, I definitely want better readability, but I also really want better performance.

first off, it’s much easier to just:

julia> (1:9) .+ (1:9)'
9×9 Matrix{Int64}:
  2   3   4   5   6   7   8   9  10
  3   4   5   6   7   8   9  10  11
  4   5   6   7   8   9  10  11  12
  5   6   7   8   9  10  11  12  13
  6   7   8   9  10  11  12  13  14
  7   8   9  10  11  12  13  14  15
  8   9  10  11  12  13  14  15  16
  9  10  11  12  13  14  15  16  17
 10  11  12  13  14  15  16  17  18

whichever is more clear for your use case, Julia is not a language where for-loop would be slow anyway, it doesn’t matter much as long as you try to find the way to do it such that it’s clear and minimize memory stress

3 Likes

That’s excellent; thanks for such a quick response! That solved my first problem too! Now I have:

res=f.((1:9),(1:9)')

And it broadcasts properly! Now I’m only confused on how to implement more dimensions like the function:

g(x,y,z)=x+y+z

It should become something like:

res=fill(NaN,9,9,9)
for i=1:9
    res[:,:,i]= g.((1:9),(1:9)',i)
end

I guess the only thing I’m worried about is having to declare before the for loop, is that a problem?

no it’s fine, pre-allocating is a pretty common pattern.

zeros(9,9,9)

might be easier, if you really care, you can even do:

Array{Float64, 3}(undef, 9,9,9)
2 Likes

I also really like this solution:

g(x,y,z)=x+y+z
res=g.(reshape(1:9,:,1),reshape(1:9,1,:),reshape(1:9,1,1,:))

Do you think there is a more compact way of writing this? I haven’t timed it but it seems like it’ll be faster than pre-allocating. Actually that’s another thing that confuses me, julia documentations said that for loops are faster than broadcasting but it said pre-allocating takes up memory. So which is faster, does preallocating and a for loop take up fewer resources than broadcasting?

So reshape(1:9, :, 1) can be replaced by just 1:9, and reshape(1:9, 1, :) by (1:9)'. For reshape(1:9, 1, 1, :) it’s not as easy, so just keep that.

Loops and broadcasting aren’t necessarily different in performance, but loops sometimes lets you avoid unnecessary allocations and do other little tricks. But here, the allocations are not unnecessary, and broadcasting is much more convenient, so just use that.

1 Like

Could I generalize this statement some and say: “If pre-allocation is necessary; then broadcasting might be better. If pre-allocation is unnecessary then use a for loop.”?

You can probably find counter-examples to that, but it’s not too bad. Perhaps: if broadcasting doesn’t cause unnecessary extra allocations and makes your code clearer, then definitely use that.

1 Like

Terminating semicolons add trailing length 1 dimensions to:

[1:9;;;]

It is close but the dimensions are in reverse order (9×1×1) instead of (1x1x9).
Is there any sugar for this?

I don’t know. But this will materialize the range into an allocated array, unlike the reshape, so I would prefer reshape anyway.

1 Like