Is this an expected behavior of rand.()?

julia> A = zeros(3,3)
3×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> true ? A .= rand.() : nothing
3×3 Matrix{Float64}:
 0.191487  0.0617435  0.279438
 0.47524   0.467929   0.45603
 0.212515  0.21998    0.51552

julia> A.= true ? rand.() : 0
3×3 Matrix{Float64}:
 0.30043  0.30043  0.30043
 0.30043  0.30043  0.30043
 0.30043  0.30043  0.30043

The behavior of A.= true ? rand.() : 0 is equivalent to A.= true ? rand() : 0.

I would say yes, because they can be translated to

if true
  A .= rand.()
else
  nothing
end

and

for i = 1:length(A)
   if true
      A[i] = rand.()
   else
      A[i] = 0
   end
end

Note that rand.() alone gives only one random number, unless it is used in a broadcast.

2 Likes

I agree with @fatteneder, it is expected. To explain why, your code is an example of how syntactic loop fusion does not fuse across non-broadcasted calls (in this case, ternary ⋯ ? ⋯ : ⋯ is not broadcasted) although it is an admittedly opaque example.

Here’s an excellent blog post on syntactic loop fusion. In brief:

Syntactic loop fusion transforms an expression like A .= f.(g.(h.(x))) into a single loop:

for i in eachindex(x)
    A[i] = f(g(h(x[i])))
end

But loops are not fused across non-broadcasted barriers, so A .= f.(g(h.(x))) is equivalent to:

temp1 = h.(x) # one loop
temp2 = g(temp1) # no loop
A .= f.(temp2) # another loop

In your case, it might help to rephrase it as it as:

ifelse(true, A .= rand.(), nothing)
# vs
A .= ifelse(true, rand.(), 0)

This makes it clearer why the second example results in two separate loops (the first loop is just rand.() generating a single number). The first example is one loop, where rand is broadcasted to the shape A, producing different numbers…

3 Likes

I see, thanks for the clear explanation.

Careful: ifelse(false, A .= rand.(), nothing) will still overwrite A, because ifelse is a function (not special syntax like if or ?) that evaluates all arguments before deciding which one to return.

8 Likes