I can not make object.property .+= value work where property is implemented in setproperty!. Here is example code:
struct MyStruct
an_array::Array{Int,1}
end
function Base.getproperty(s::MyStruct, name::Symbol)
if name==:subarray
s.an_array[2:end]
else
getfield(s,name)
end
end
function Base.setproperty!(s::MyStruct, name::Symbol, x)
if name==:subarray
s.an_array[2:end] = x
else
setfield!(s,name,x)
end
end
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.
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]
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 .=.
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.
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!.