Setproperty!/getproperty and broadcasting do not seem to work together



I can not make .+= value work where property is implemented in setproperty!. Here is example code:

struct MyStruct

function Base.getproperty(s::MyStruct, name::Symbol)
    if name==:subarray

function Base.setproperty!(s::MyStruct, name::Symbol, x)
    if name==:subarray
        s.an_array[2:end] = x

s = MyStruct([1,2,3])
s.subarray .+= [20,30]

This results in MyStruct([1, 2, 3]) when I would expect the result to be MyStruct([1, 22, 33]).

I tried making it a mutable struct but that did not work either.

What am I doing wrong here? Is this a Julia bug?


This is essentially syntactic sugar for broadcast!(+, s.subarray, s.subarray, [20,30]), so it never calls your setproperty! method.

Probably you just want to change your getproperty method to return @view s.an_array[2:end] rather than a copy.


Thanks for the explanation!

For completeness, here is the (misunderstood) example code that led me to (wrongly) believe a @view is not necessary:

a = [1,2,3]
a[2:3] .+= [20,30] # no copying, because the indexing is on the left side 
b = a[2:3] # this is a copy, as the indexing is on the right side
b .+= [200,300]

Now a is [1,22,33], while b is [222,333].


It might be helpful to think of the .= as the “broadcast into” operation.

  • a .= ... broadcasts into a itself.
  • a[1] .= ... broadcasts into the object that a[1] returns.
  • a.prop .= ... broadcasts into the object that a.prop returns.
  • a[2:3] .= ... effectively broadcasts into a[2:3]. It is indeed a little special because — unlike the other three forms — if we just modified the thing that a[2:3] normally returns we wouldn’t change a at all!

You can even do funny things that you cannot do with the normal (non-broadcast) =, like f(1) .= ..., which broadcasts into whatever f(1) returns.

So the point is that you’re not really doing setproperty! at all when you use .=.


Could you elaborate on the “special” case?

Is this still just rewritten into a broadcast! call? I do not get how this can be true without creating a copy…

Especially given that a[1] .= ... does not have the same “special” behavior.


The point is that — at a high level — LHS .= RHS broadcasts into whatever the left hand side returns. We make an exception for non-scalar indexing because it simply wouldn’t be useful to modify the new container that non-scalar indexing would normally return.

Now if you’re curious about its implementation, the syntax x[i] .= y is essentially lowered to broadcast!(identity, @views(x[i]), y). The Julia parser identifies that there was an indexing expression on the LHS and inserts an extra transform in those cases. The @views macro will return an element if all indices are scalar numbers, and a view otherwise. It’s more complicated than that, though, due to broadcast fusion and such, but the behavior is the same.


No, see the comment here.

a[foo...] .= ... is rewritten into broadcast!(identity, Base.dotview(a, foo...), ...). The Base.dotview function, which in turn calls Base.maybeview (which is used for the @views macro), defaults to getindex, but returns a view for slicing operations like a[1:2] where a is an AbstractArray.


We don’t actually lower to broadcast! directly anymore due to the run-time representation of fusion, but the end result is the same. It’s easier to think about it as though it still did, but this mental model will break if you try to overload broadcast!.