why doesn't `@.` always broadcast?

some examples:

julia> @. x->x[1]
#1 (generic function with 1 method)

julia> ans( [[1,2,3],[9,8,7]] )
3-element Vector{Int64}:
 1
 2
 3

julia> @. x->getindex(x,1)
#3 (generic function with 1 method)

julia> ans( [[1,2,3],[9,8,7]] )
2-element Vector{Int64}:
 1
 9

julia> @. x->getindex.(x,1)
#5 (generic function with 1 method)

julia> ans( [ [[1,2,3],[9,8,7]] ] )
1-element Vector{Vector{Int64}}:
 [1, 2, 3]

I was expecting #1 to behave like #3, and for #6 to return [[1,9]]. is there a way to make @. work all the time?

1 Like

@. doesn’t introduce broadcasting to everything that isn’t broadcasting. In this case, indexing syntax in the macro’s input expression does not count as a function call, even though it will later lower to a getindex call. The docstring for @. is pretty clear on what it’s supposed to do, and the @macroexpand result is very readable if you need to check:

julia> @macroexpand @. x->x[1] # doesn't touch indexing syntax
:(x->begin
          #= REPL[1]:1 =#
          x[1]
      end)

julia> @macroexpand @. x->getindex(x,1) # affects function call
:(x->begin
          #= REPL[3]:1 =#
          getindex.(x, 1)
      end)

julia> @macroexpand @. x->getindex.(x,1) # leaves broadcasted calls alone
:(x->begin
          #= REPL[4]:1 =#
          getindex.(x, 1)
      end)

help?> @.
  @. expr

  Convert every function call or operator in expr into a "dot call" (e.g. convert f(x) to f.(x)), and convert every
  assignment in expr to a "dot assignment" (e.g. convert += to .+=).

  If you want to avoid adding dots for selected function calls in expr, splice those function calls in with $. For
  example, @. sqrt(abs($sort(x))) is equivalent to sqrt.(abs.(sort(x))) (no dot for sort).
7 Likes

I was more fishing for an answer to the question at the end of the post; if maybe a more thorough broadcasting macro exists in a package somewhere, or a place to start for writing some @.. which does distribute everything.

the @macroexpand call does help me though, thanks

It’s not clear what exactly this question means. @. is working as intended. Conceptually it is just adding .s everywhere where they make sense. E.g. not in x[1]

julia> x = [ [[1,2,3],[9,8,7]] ]; x.[1]
ERROR: syntax: invalid syntax "x.[1]" around REPL[...]:1

but it’s fine in getindex

julia> getindex.(x, 1)
1-element Vector{Vector{Int64}}:
 [1, 2, 3]

though not in getindex.

julia> getindex..(x, 1)
ERROR: UndefVarError: `..` not defined in `Main`

I’m sure there exist counter-examples to this intuitive idea, so check the source code or Benny`s reply to see what the macro is formally doing.


If you want your #6 to return [[1, 9]] you need something like

f1(x) = (a -> getindex.(a, 1)).(x)

i.e.

f2(x) = broadcast(a -> broadcast(b -> getindex(b, 1), a), x)
julia> x = [ [[1,2,3],[9,8,7]] ]; f1(x)
1-element Vector{Vector{Int64}}:
 [1, 9]

julia> 1-element Vector{Vector{Int64}}:
 [1, 9]

Is it possible to write a macro @dotdot so that @dotdot getindex..(x, 1) gets converted into one of these? Sure. But what for x = [[ [[1,2,3],[9,8,7]] ]]? You’d need getindex...(x, 1), which presumably will start conflicting with the splat operator .... If you want to keep broadcasting as deep as possible, note that you can broadcast over scalars (and e.g. x = 5; getindex(x, 1) works fine), so how do you know when to stop? In my opinion this would quickly become a convoluted mess which would be difficult to interpret.


I’m already not too fond of the @. macro itself, as to some extent it hides what’s really happening. E.g. that @. x[1] + y resolves to x[1] .+ y and not x.[1] .+ y or getindex.(x, 1) .+ y. It’s not a lot of effort to just manually add some dots yourself, and doing this manually forces you to check whether it makes sense. Also note that leaving out select broadcasts can improve performance.

2 Likes

ah, sorry; I guess I wasn’t very clear as to what I was looking for. I understand that @. can’t be changed and that it’s working as described in its docs. what I wanted was a macro which broadcasts things exactly one time, and does so everywhere. so if say there was a x[1], you would really be saying ā€˜the first element, of all elements of x’. or if there was a getindex.(x, 1), it would be saying getindex.(i, 1) for i in x or however that syntax should look.

I agree that the @. macro isn’t very useful on its own, since the syntax it expands out to is not all that different from what you give it. but if there were a macro which meant broadcast everything once, I would have plenty of use for it, since there are many cases where broadcasting is more than just adding a dot

maybe a for loop would be the ''correct answer'', but I was hoping for a macro since the original source of this question was already part of a for loop:

for (a, b) in blackbox.(data) .|> (x->(getindex.(x,1), getindex.(x,2)))
...

would be much nicer as

for (a, b) in @.. blackbox(data) |> x->(x[1], x[2])
...

If I understand correctly, the only problem with @. here is then that it does not support the bracket notation for indexing (and not that it doesn’t support double broadcasting (Ć  la ā€˜getindex..’))? If so, that can be easily fixed:

using MacroTools: @capture, postwalk

function bracket_to_getindex(ex)
    postwalk(ex) do x
        @capture(x, A_[i__]) ? :(getindex($A, $(i...))) : x
    end
end

macro var".."(ex)
    return Broadcast.__dot__(bracket_to_getindex(ex))
end


# Testing MWE
using StaticArrays

data = rand(SVector{2, SVector{3, SVector{4, Int}}})
blackbox(d) = d

for (a, b) in blackbox.(data) .|> (x->(getindex.(x,1), getindex.(x,2)))
    println("$a - $b")
end
# Int8[-82, 106, 68] - Int8[83, 62, 117]
# Int8[87, 78, -26] - Int8[-75, 119, -71]

for (a, b) in @.. blackbox(data) |> x->(x[1], x[2])
    println("$a - $b")
end
# Int8[-82, 106, 68] - Int8[83, 62, 117]
# Int8[87, 78, -26] - Int8[-75, 119, -71]

Edit: I do think an explicit for loop in the style of

for d in data
    bbd = blackbox(d)
    a = getindex.(bbd, 1)
    b = getindex.(bbd, 2)
    println("$a - $b")
end
# Int8[-82, 106, 68] - Int8[83, 62, 117]
# Int8[87, 78, -26] - Int8[-75, 119, -71]

is much easier to understand, though.

1 Like

yes, that works beautifully for my current use case; will need to take a hot second to break down and digest exactly how it’s working though.

what I like about the double broadcasting aspect and such is the generality of it; the idea that the functions that you give it don’t matter because it preforms the same transformation over everything. if I was more well-versed in monads and such I’m sure I’d have a word for what I’m trying to get across

for my brain at least, the last for loop you include is a larger cognitive load since data, a, and b are all things which it makes sense to give a name to. creating an extra d and bbd which don’t have any logical interpretation other than ā€˜an entry of data’ or ā€˜a list of pairs of a and b’ means you have to keep bouncing around the logic to keep track of where everything is going

Well, without the necessary context, none of the variables have any logical interpretation for me :wink:
But it might definitely be a fair point. I’m just giving my limited perspective here, based on my personal coding style and experience. Of course feel free to use a different style.

I was more giving an explanation as to why it looks nicer structured that way in my code; in another context bringing it out into a bigger loop might be nicer as well ^^

1 Like

If you want a reason why A[i] isn’t affected by @., it’s because it’s really common to slice arrays for inputs in array operations A[1:10] .+ B[3:13], maybe even lazily @views A[1:10] .+ B[3:13]. That’s why it has the bracket syntax instead of just being getindex/view calls in the first place. You could make a macro that affects bracket syntax as getindex calls, but why do that when you already have the option to write getindex calls? That macro would also have to interact with other macros like @views and adapt the $ opt-out for bracket syntax somehow.

This is a common opinion. Manual dots was the more typical practice that Julia’s own dots emulated, and @. was always just a convenient macro for simpler array operations (which is exactly where bracket syntax is intended to remain as unbroadcasted getindex). For more complicated expressions, like those here involving anonymous functions, it’s bad form to use indiscriminate macros. @. was never intended as a go-to tool for adding a layer of broadcasting to everything in an arbitrary expression, nor was that something anybody wanted to do; things turned out the way it did for the preexisting context of array operations.

1 Like

There’s no technical reason why this can’t be supported, and we’ve talked about it from the early days of dot-call broadcasting, but it never got implemented because we haven’t been able to decide on the exact semantics.

See several github issues: Broadcast Array Indexing Ā· Issue #19169 Ā· JuliaLang/julia Ā· GitHub, make indexing expressions participate in dot syntax fusion Ā· Issue #22858 Ā· JuliaLang/julia Ā· GitHub, Broadcasting pointwise indexing operator Ā· Issue #2591 Ā· JuliaLang/julia Ā· GitHub

Just to add my two cents, from my perspective it’s not that clear. You quote the docs as:

Convert every function call or operator in expr into a "dot call"

At least in my experience, [] notation is usually referred to as the index operator, so I would have assumed this to be included here. Maybe the docs could add a note for these exceptions.
Another exception is the . operator, as in the getfield operator.

I’m probably going a bit off-topic here, but I do sometimes wish there was a .. operator to broadcast a getfield call, since using getfield. usually results in type instability:

julia> struct A x::Int; y::Float64 end

julia> v =  [A(1, 1.0), A(2, 2.0), A(3, 3.0)]
3-element Vector{A}:
 A(1, 1.0)
 A(2, 2.0)
 A(3, 3.0)

julia> @code_warntype getfield.(v, :x)
MethodInstance for (::var"##dotfunction#230#1")(::Vector{A}, ::Symbol)
  from (::var"##dotfunction#230#1")(x1, x2) @ Main none:0
Arguments
  #self#::Core.Const(var"##dotfunction#230#1"())
  x1::Vector{A}
  x2::Symbol
Body::Union{Vector{Float64}, Vector{Int64}, Vector{Real}}
1 ─ %1 = Base.broadcasted(Main.getfield, x1, x2)::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}, Nothing, typeof(getfield), Tuple{Vector{A}, Base.RefValue{Symbol}}}
│   %2 = Base.materialize(%1)::Union{Vector{Float64}, Vector{Int64}, Vector{Real}}
└──      return %2

Unless you wrap it in an extra function, but that is kinda tedious:

julia> f(x::A) = x.x # works the same with getfield(x, :x) too
f (generic function with 1 method)

julia> @code_warntype f.(v)
MethodInstance for (::var"##dotfunction#231#2")(::Vector{A})
  from (::var"##dotfunction#231#2")(x1) @ Main none:0
Arguments
  #self#::Core.Const(var"##dotfunction#231#2"())
  x1::Vector{A}
Body::Vector{Int64}
1 ─ %1 = Base.broadcasted(Main.f, x1)::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}, Nothing, typeof(f), Tuple{Vector{A}}}
│   %2 = Base.materialize(%1)::Vector{Int64}
└──      return %2

It seems like for some reason, the normal getfield can constant propagate the Symbol, but the getfield. call can’t.

Definitely opening up another can of worms here, but whether those count as operators or just syntax/notation elements is much more inconsistent than for +,-,*,/. C/C++ puts them among member access operators, but Python/MATLAB/Julia doesn’t. Julia does document lists of operators, but for specific purposes rather than an exhaustive list, and neither . for getproperty (slight correction there, though it usually inlines getfield anyway) nor [] brackets are described on that page. Despite deliberately using ā€œdot operatorā€ for dotted versions of binary operators however, the symbol :. is in fact on the ā€œoperator precedenceā€ list and accepted by Base.operator_precedence. On the other hand, there is no possible way to make a symbol for brackets and its precedence is as obvious as () parentheses, which also aren’t on that list.

That’s because getfield. isn’t a derivative function to be called. getfield is just an input for a higher order function in a series of calls lowered from the dot syntax. By keeping the symbol :x as a separate input to those calls instead of a constant in a nested Core.getfield call like (a -> a.x).(v), we don’t have constant propagation privileges anymore; the :x is currently stored in a Base.RefValue{Symbol}(:x) element, which is how the compiler ends up only seeing Symbol.

@code_warntype won’t show constant propagation even if it would occur for nested calls, this is what happens for dots:

julia> @code_warntype A(1,2.3).re
MethodInstance for getproperty(::A, ::Symbol)
...
Body::Union{Float64, Int64}
...

I doubt any improvement to constant propagation would need a separate .. notation, and it’s very far removed from anything suggested for it so far.

1 Like