Currently, A .= B
lowers to effectively broadcast!(identity, A, B)
. That typically returns the mutated object — A
. But this isn't enforced… or even documented.
If we want to support chained .=
expressions, it seems like we really need this to always evaluate to the RHS through a lowering transformation.
So you want to fuse the loops in A .= (B.= C .+ D) .* E
, in order to get multiple-output broadcasts? Or am I understanding you wrong here?
What do you do if an L-value appears multiple time, e.g.A .= (A .= A.+C) .+ A
? Do you want, post this expression, A= 2*A+2*C
or A = 2*A + 3*C
? The first one, I presume.
It should be consistent with regular assignment, i.e. lower to the RHS.
Note that doing this as a lowering transform is not without precedent. setindex!
returns the mutated object, but a[] = b
returns b
.
Yup, it's not so much about supporting fusion through multiple .=
expressions as it is getting the semantics consistent.
Triage was unanimous. Let's do this.
Triage accepts.
I'm not sure what the proposed semantics are.
Suppose you do
result = (floatarray .= 2 .* intarray)
What is "the RHS" in this case? The RHS expression2 .* intarray
by itself would create another integer array, but because of fusion this array is never actually allocated here. The only place the result is stored is in the LHS floatarray
, at which point the element type has changed and some rounding may have occurred.
Returning the LHS, on the other hand, would be straightforward. At minimum, we could document this property of broadcast!
, but of course that couldn't be enforced. So we could lower to an expression let tmp = floatarray; broadcast!(...); tmp; end
.
Is there a way in which we can expand x .= f.(y)
as itself just a call to (lazy) broadcast (roughly along the lines of lazy(.=, x, lazy(f, y))
)?
Very good point. Apparently we have the technology in lowering to wrap expressions in "unnecessary" heads, and then they can get elided if they're never used. That's still a little unsatisfactory, since it'd mean that we'd run the broadcasted function(s) through the arguments twice — once in the fused assignment and once in allocation of the RHS.
The goal here would be to try to lower to _something_ that would detect if it's needed.
Would it be so terrible to just have .=
return the LHS, not the RHS, since the LHS is always materialized? It would be different from =
, but .=
is already different from =
in lots of ways.
Would it be so terrible to just have .= return the LHS
Yes, it's inconsistent with every other appearance of =
in the language.
The lowering passes (julia-syntax.scm) have a feature where you can wrap an expression in (unnecessary ...)
and it will be removed if the value is not used.
.=
is already very different from =
... if you are mutating an object in-place, doesn’t it make sense to return the mutated object?
a[i] = b
is also a mutation though.
I am not a computer scientist, so excuse my ignorance. What happens to codes that follow this pattern:
```
struct MyType{T,N}<: AbstractArray {T,N}
A::Array{T,N}
Other fields...
end
Base.setindex!(m::MyType,I,v)=setindex!(m.A,I,v)....
f(m::MyType)=...
m=MyType(...)
m.=rand(10)
f(m)
```
Will
m` become an array and the function call not work anymore? Is this pattern not common enough, or wrong?
Thanks
No, this will be nearly unobservable in most everyone's code. This is only considering what the entire expression (m.=rand(10))
should evaluate to. In most code, you don't even ask for this value. You'll only observe this change if your code asks for ret = (m.=rand(10))
or ret .= (m.=rand(10))
… and it'll determine what gets returned at the REPL, too.
The .=
syntax will still continue assigning into and mutating objects as you expect.
I've been using something like A_mul_B!(y, A, (x .= x.^2))
to avoid allocation. Does it mean I need to rewrite it in two lines in Julia 1.0? (I do respect consistency so the rewrite is fine, but it was a very handy notation...)
Could we see some more examples where this change matters?
Stevegj's example:
result = (floatarray .= 2 .* intarray)
LHS lowering: one loop, no temp copy, result===floatarray
RHS lowering: What should this mean? What code should get run? Do we store a temporary or compute twice?
result .= (floatarray .= 2 .* intarray)
LHS lowering: two loops, perform compute once, no temp copy, result == floatarray
(mod inexact conversions), result !== floatarray
.
RHS lowering: What should this mean? What code should get run? Do we run all computations in the (ultimately) RHS multiple times? Do we store them in a temporary?
Is there any example where trying for RHS lowering is better than returning nothing
? What are the examples where returning the LHS is better than returning nothing
?
Long-term, fusing into multi-output broadcasts might be cool; in this case we'd best have something now that returns the same result as the eventual solution (since multi-output broadcast that can store temporaries on the way has no syntax now).
To give an example where we currently have a difference:
a=100;c=1;a= (a + 0) + (a=a+c); a
#201
a=[100];c=[1]; a= (a + 0) + (a=a+c); a
#[201]
a=[100];c=[1]; @. a= (a + 0) + (a=a+c); a
#[202]
Otoh
a=100;c=1;a= a + (a=a+c); a
#202
a=[100];c=[1]; a= a + (a=a+c); a
#[202]
a=[100];c=[1]; @. a= a + (a=a+c); a
#[202]
I'm getting false positives in https://github.com/JuliaLang/julia/pull/24368 with code like this:
$ ./julia --depwarn=error -q
julia> module TestFoo
x .= y
nothing
end
ERROR: syntax: Deprecated syntax `using the value of `.=``.
Edit: whoops, I meant to post this in the PR that fixed this issue.
I keep encountering cases where this change makes it impossible to chain in-place operations. e.g. I just ran into a case where I wanted shuffle!(a .= foo.(b))
, and instead you have to split it into two statements. I really think returning the LHS is the desirable behavior here.
Or you can use shuffle!(map!(foo, a, b))
.
@mbauman, in this case I actually had foo.(bar.(b))
, but yes, I could have used shuffle!(map!(x -> foo(bar(x)), a, b))
. Or I could split it into two lines. I'm just saying that chaining a sequence of in-place operations is extremely common, and it is a shame to prohibit using .=
in this context.
I have to say I'm fairly convinced by @stevengj's examples. Is there some way we can return the LHS and still avoid the spooky type annotation action at a distance?
How about we return the rhs broadcasted to the shape of the LHS?
On Mar 7, 2018 13:41, "Stefan Karpinski" notifications@github.com wrote:
I have to say I'm fairly convinced by @stevengj
https://github.com/stevengj's examples. Is there some way we can return
the LHS and still avoid the spooky type annotation action at a distance?—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/JuliaLang/julia/issues/25954#issuecomment-371295027,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABO1l0RlDV8KZa7r72dXyMsYcL1QAQmJks5tcFQigaJpZM4R-ksT
.
Returning the rhs in any form loses the advantage of working in-place with the lhs, which is the whole point of .=
. I really don't see the problem here with .=
being different from =
in yet another way.
And now we can't use .=
in the REPL without getting a deprecation warning...
Most helpful comment
No, this will be nearly unobservable in most everyone's code. This is only considering what the entire expression
(m.=rand(10))
should evaluate to. In most code, you don't even ask for this value. You'll only observe this change if your code asks forret = (m.=rand(10))
orret .= (m.=rand(10))
… and it'll determine what gets returned at the REPL, too.The
.=
syntax will still continue assigning into and mutating objects as you expect.