Understanding the behaviour of @. macro

Hi,

I am very new to Julia and was playing around with @. (using Julia 1.10.3) and noticed that I can compute the expression

5 .* rand(10) .+ 2

as expected, however when I use

@. 5*rand(10) + 2

it does not work and gives the error “MethodError: no method matching +(::Vector{Float64}, ::Int64)”.

@macroexpand @. 5*rand(10) + 2
:((+).((*).(5, rand.(10)), 2))

Using @macroexpand I realised the error seems to be in the difference of rand.(10) and rand(10) as

(+).(rand(10), 2)  # works
(+).(rand.(10), 2) # does not work, MethodError: no method matching +(::Vector{Float64}, ::Int64)

which seems weird to me, as rand(10) produces a Vector of floats, but so does rand.(10), hence there seems to be a method available to add a float vector and an integer.

What am I missing here?
Thank you very much in advance!

3 Likes

Yeah, this is a very confusing one, verging on cursed. One way to think of it is that broadcast is creating a simple loop over all the dotted expressions, so you can think of rand.(xs) .+ ys as something kinda like [rand(x) + y for x in xs, y in ys]. But that won’t work because rand(10) gives you a vector and you can’t add (without broadcasting, but remember we already did the broadcast transform) a scalar to a vector.

There are a few things that make this especially confusing:

  • 5 .* rand.(10) works, because you can multiply a scalar by a vector.
  • rand.(10)by itself — is functionally equivalent to rand(10) because, at the end of a broadcast expression, Julia “unwraps” zero-dimensional results. Theoretically, the “correct” answer to rand.(10) would be a zero-dimensional array that contains a 10-element vector, but that’s quite cumbersome in many situations. See julia#28866 for more on this.

The simplest guideline is that broadcasting works most seamlessly for functions functions that take scalars and return a single value. To avoid @. from affecting functions like rand (or ones or zeros or any of the like), you can “protect” them with a $. That is, @. 5 * $rand(10) + 2.

16 Likes

Note also that if you have something else to provide the shape of the array, then you can just use rand.().

For example, with a pre-allocated output x = zeros(10), you can do:

@. x = 5 * rand() + 2

and it will expand out to a separate call to rand() for each element, without ever allocating an array of random numbers. Another example would be where you are combining random number with some other array, e.g.

@. 5 * rand() + (1:10)
5 Likes

into the blog it goes

1 Like

Thank you very much for the insightful answer!

Using another example, I don’t understand why this fails

5 .* zeros.() .+ 2
# MethodError: no method matching +(::Array{Float64, 0}, ::Int64) 
# For element-wise addition, use broadcasting with dot syntax: array .+ scalar

but this works

x = 5 .* zeros.()
x .+ 2
# 2.0

I guess x provided the shape for the broadcasting as in @stevengj answer, but what is the intention of the behaviour of the first example? Even the error message is basically saying I did it correct :sweat_smile:

Think of assigning this to an array — the broadcast means that each element will be computed as 5 * zeros() + 2, but this fails because zeros() returns a 0-dimensional array, and 5 * zeros() (scalar * array = array) is a 0-dimensional array, but Julia doesn’t define a method for array + scalar for any dimensionality of array:

julia> zeros()
0-dimensional Array{Float64, 0}:
0.0

julia> zeros() + 1
ERROR: MethodError: no method matching +(::Array{Float64, 0}, ::Int64)

In contrast, it works with rand.() because rand() returns a scalar.

1 Like