As suggested on discourse,
it would be lighter to replace $Ref(s)
by &s
to declare that s
should be treated as a scalar in broadcasting:
@. f($Ref(s), t)
would become
@. f(&s, t)
Outside @.
macros, &s
would be a shortcut for Ref(s)
.
The error message should be updated accordingly.
This particular syntax proposal has been discussed a bit before and if I recall there was some concern over the operator precedence for &
. Hopefully not intractable though, because I also think it'd be quite nice. :slightly_smiling_face:
Searched again, and only found issue https://github.com/JuliaLang/julia/pull/25322 yet, but it is not related to Ref
.
The issue is not really one of precedence as it is of confusion between unary operators vs. function calls. We'd be making a distinction between (&)(1)
and &1
. The former already exists and works — it calls the one-argument method of the &
function. The latter would presumably lower to something different as a unary operator for Ref
.
Currently we define (&)(x::Integer) = x
, which seems not incredibly useful. Perhaps that could instead be (&)(x::T) where {T} = Ref{T}(x)
? It would take a deprecation cycle but we could then introduce &
for this purpose in 1.0.
I looked at that a while ago and IIRC we had been relying upon that method in base for (&)(args...)
. I don't see such a splat in Base anymore, but there are some packages that do it. It's along the same lines as *(::Number)
.
that is a fairly useless method but sticking Ref functionality into the bitwise and operator is distinctly punny. The unary operator syntax does not need to be related to the binary and operator, however.
Maybe a good reason to deprecate the bitwise operators to something more readable (e.g bitwise_and, bitwise_or, bitwise_xor)? The don't really need to used enough to warrant having syntax, and it would remove the precedence question (|
vs ||
)
Maybe a good reason to deprecate the bitwise operators to something more readable
That was already discussed and decided against.
that is a fairly useless method but sticking Ref functionality into the bitwise and operator is distinctly punny
That is a fair point. So is that a "no" on this proposal then? (FWIW I remain in favor.)
deprecate the bitwise operators to something more readable
We'd definitely need to simultaneously introduce the .||
syntax to make that anywhere near tenable for the data folk. The bitwise arithmetic folks got all up in arms just considering changing the precedence of |
in #5187, so I'd foresee quite a bit of resistance. E.g., https://github.com/JuliaLang/julia/issues/27563#issuecomment-397463384.
I don't really see a problem with having a special &x
syntax for Ref
, just so long as it's not a method of &
. It is a bit of parser-special-case-y-ness — not ideal, but probably just fine.
I don't really see a problem with having a special
&x
syntax forRef
, just so long as it's not a method of&
.
Yes, ☝️
Note that &x
is already special-cased in the parser—it parses as Expr(:&, :x)
, i.e. a special &
node. The only new thing would be a new lowering step that lowers &
expressions to Ref
. I pushed a sample implementation to #27608.
Another nice thing about this is that Ref(x)
doesn't work if x
is an array. You have to type Ref{typeof(x)}(x)
or Base.RefValue(x)
, and it is a lot easier to type &x
.
As I mentioned in #27608, if we do this it would seem valuable to consider uses of references beyond the @.
macro... (perhaps this was discussed elsewhere but not github).
I can see that this will work nicely for ccall
, and similarly passing mutable references to Julia functions, but will this also work out nicely for the other reference syntax discussed in e.g. #21912. In that PR, Keno suggests using @
but I can see because of using &
here it might be better to use &
for that purpose as well?
For example, can we use &a[i]
or, a&[i]
(or whatever) to get a reference to the i
th value of a::Array
? Can we use &a.b
or a&.b
(or whatever) to get a reference to a field of a mutable struct? Can this be extended to "mutating parts of immutables"?
I like @andyferris's idea of getting more out of this syntax by using it for various kinds of references.
In 1.0 the error is cryptic
ERROR: MethodError: no method matching length(::Material)
and there is no workaround.
chi1 = @. chi(&m1, energy)
ERROR: syntax: invalid syntax &m1
This is probably going to scare off some new users.
I prefer '&', but if you want to buy time until the &
syntax is fully worked out,
here is a proposition: implement #27608 with the '♮' symbol.
"♮" can be typed by \natural<tab>
In music it means that all alterations are removed for the following note.
Not far from the actual concept here (it removes the broadcasting magic).
That would be backwards-incompatible because variable names can start with ♮
— in Julia 1.0, ♮x
is a variable with the name ♮x
, distinct from the application of unary ♮
to x
which would be written as ♮(x)
.
&
also is a utf-8 character. It is treated differently. Why couldn't ♮
?
The difference is that &
is already treated differently:
julia> x = 10
10
julia> &x
ERROR: syntax: invalid syntax &x
&x
is an error in Julia 1.0, whereas ♮x
already means something — namely, the variable with that name.
julia> ♮x = "hello"
"hello"
julia> ♮x
"hello"
julia> &x = "hello"
ERROR: syntax: invalid assignment location "&x"
This is important because the version 1.0 comes with a stability guarantee — code written for 1.0 should continue to work on 1.x. There are a few other characters that are already parsed as unary operators, and those can be used now:
julia> ~ = Ref
Ref
julia> println.(~[1, 2, 3])
[1, 2, 3]
julia> println.([1, 2, 3])
1
2
3
3-element Array{Nothing,1}:
nothing
nothing
nothing
@yurivish &
is specially parsed, and is not unary.
Woudn't any unary operator such as ~
raise even worse ambiguity concerns ?
The release candidate of 1.0 lasted few hours only before the official release.
And 0.7 never had Plots working before that. So normal users probably did not try it thoroughly.
From the release ceremony, it was clear that the urge was not a disregard for users,
but rather a well deserved celebration for the current state of julia.
version 1.0 comes with a stability guarantee — code written for 1.0 should continue to work on 1.x
In my opinion, the lack of feedback implies that _this rule can be relaxed somewhat,
if it helps significantly_.
(although, again, I prefer an &
syntax, and sometimes creativity comes out of constraints)
The current situation is bad, because octave and python users trying julia-1.0 after the holidays
will quickly face this issue, and get a wrong impression.
This is why stealing the ♮
symbol, perhaps temporarily, or going ahead with the &
syntax,
seems better than status quo.
How would Python and Octave users encounter corner cases of a syntax that's unique to Julia immediately upon trying out the language?
Because we use broadcast extensively, so this is one of the first thing we try.
[Actually the @.
stuff was what brought me back to julia.]
Then mixing struct and iterables comes naturally.
[At least three independent reports already surfaced saying that]
"Immediately", surely not, but less than one month of serious programming
before encountering the issue seems a good estimate. Hence my kind heads-up.
Starting from a concept that the notation &x
is for "shielding" x
from the broadcasting mechanism, how about generalizing it to shielding from broadcasting _and materializing_ mechanisms (i.e., #19198)? What I mean is that how about
sum(&(sin.(x .+ π)))
being lowered to
%1 = (Base.broadcasted)(+, x, π)
%2 = (Base.broadcasted)(sin, %1)
%3 = sum(%2) # w/o materialize(%2)
The motivation here is to fuse the mapping _and_ reduction.
To make it more extensible, this can probably go through an additional indirection of lowering &expr
to, in broadcasting context, (say) Broadcast.shield(lowered_expr)
which has the default definition
shield(bc::Broadcasted) = bc
shield(x) = Ref(x)
Further extending the concept, how about "indexing by .
" to mean "penetrate the shield", i.e.:
y .= sum(&(sin.(M .+ π))[:, .]) .+ v
to do (without allocation)
for i in 1:size(M, 2)
y[i] = sum(sin.(M[:, i] .+ π)) + v[i]
end
(note that the loop fusion happens even though sum(...)
is not a dotted call)
Writing this even for non-dotted expression (i.e., "array reference") inside &(...)
seems to be useful:
y .= sum(&M[:, .]) .+ v
Also, indexing by symbol other than :
and .
, say, &
, can be used to indicate "reduce but don't squeeze" (https://github.com/JuliaLang/julia/pull/27608#issuecomment-397927443). That is to say, you use &
if you want to have a "reference" to that dimension in the outer broadcasting.
I like the way you're thinking, @tkf. Have to ponder it for a while. @mbauman, any reactions?
Interesting; see also the @lazy
proposal in #19198.
However, note that @tkf's proposal is not so much a generalization as a conflicting proposal. In particular, what does f.(x, &g.(x))
do? Is it equivalent to f.(x, Ref(g.(x))
or f.(x, @lazy g.(x))
or f.(x, Ref(@lazy g.(x)))
?
not so much a generalization as a conflicting proposal
Good point. Since what I proposed has to "shield" against two mechanisms, &
has to do a half of its job when only one mechanism is in place (in the first variant I discuss below). It may or may not become a source of pitfalls, but I guess I need to think about the exact rules and how they play together.
In particular, what does
f.(x, &g.(x))
do? Is it equivalent tof.(x, Ref(g.(x))
orf.(x, @lazy g.(x))
orf.(x, Ref(@lazy g.(x)))
?
I was thinking f.(x, Ref(@lazy g.(x)))
, based on the concept "shield against both broadcasting and materializing." I can think of at least two variants:
Suppose &
shields against things "only when necessary." The rules would be:
&x
becomes Ref(x)
if x
is not a dot call.
&f.(args...)
turns into...
broadcasted(f, args...)
if &
node itself is as an argument of a dot call.Ref(broadcasted(f, args...))
otherwise.Let's see how it plays in different contexts:
# Proposed | In Julia 1.1
1: x .+ f(y, &identity(z)) | x .+ f(y, Ref(z)) # maybe disallow?
2: x .+ f(y, &identity.(z)) | x .+ f(y, broadcasted(identity, z))
3: x .+ f.(y, &identity(z)) | x .+ f.(y, Ref(z))
4: x .+ f.(y, &identity.(z)) | x .+ f.(y, Ref(broadcasted(identity, z)))
The result of expressions 3 and 4 would be the same while expressions 1 and 2 probably have different results. This shows that you have to look at both .
and &
to understand the whole expression. In particular, &identity.(x)
and &identity(x)
are different _unless_ they are the arguments to the dot call. This is unfortunate as identity
has been and the identity despite broadcasted or not (i.e., x .+ identity.(y .+ z)
is x .+ identity(y .+ z)
, modulo allocation). So, I wonder if expression 1 should be disallowed (but using Ref
outside broadcasting is also useful...).
Suppose more "uniform" rule:
Ref(x)
if x
is not a dot call.Ref(broadcasted(f, args...))
if the argument of &
is a dot call.The same examples now become
# Proposed | In Julia 1.1
1: x .+ f(y, &identity(z)) | x .+ f(y, Ref(z))
2: x .+ f(y, &identity.(z)) | x .+ f(y, Ref(broadcasted(identity, z))) # changed
3: x .+ f.(y, &identity(z)) | x .+ f.(y, Ref(z))
4: x .+ f.(y, &identity.(z)) | x .+ f.(y, Ref(broadcasted(identity, z)))
Now &identity.(x)
and &identity(x)
are equivalent, although f(y, Ref(broadcasted(identity, z)))
is probably not much useful when f
is expecting an array in arguments. Of course, one can immediately dereference Ref
-of-broadcasted
:
2': x .+ f(y, &identity.(z)[]) | x .+ f(y, broadcasted(identity, z))
But this pattern seems to be more useful than single &
. Also, the extension like y .= sum(&M[:, .]) .+ v
mentioned above would not be straightforward. So, I tend to think "minimal shield" is a better approach, even though expression 1 becomes a (possibly) tricky case.
Thanks so much for enumerating those examples. Unfortunately they also make the prospect of doubling up the behaviors (to serve as both Ref & no-materialize) here much less exciting to me. I don't really like the behaviors of either variant.
The part that is quite exciting to me, however, is the idea of using a .
to enable non-scalar fusion through to _particular dimensions_ of a non-scalar indexing expression. This idea can actually stand completely on its own and isn't simply an extension of this particular &
proposal. This is clever and interesting, but I haven't had a chance to think through all the ramifications yet. Perhaps we can continue the discussion of that idea over in https://github.com/JuliaLang/julia/issues/2591.
Yeah, I guess adding multiple responsibilities to one syntax was not a good direction. I was thinking .
and &
to be somewhat like quasiquote/unquote inspired by Guy Steele's "vector notation talk". But I now learned stretching the analogy when multiple mechanisms are in action is tricky.
I didn't realize [.]
can be considered separately. That's a very good point. I added a quick comment in https://github.com/JuliaLang/julia/issues/2591#issuecomment-452476140
Most helpful comment
Maybe a good reason to deprecate the bitwise operators to something more readable (e.g bitwise_and, bitwise_or, bitwise_xor)? The don't really need to used enough to warrant having syntax, and it would remove the precedence question (
|
vs||
)