Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

&<var> syntax to declare a scalar to broadcast with #27563

Open
ederag opened this issue Jun 13, 2018 · 28 comments
Open

&<var> syntax to declare a scalar to broadcast with #27563

ederag opened this issue Jun 13, 2018 · 28 comments
Labels
broadcast Applying a function over a collection parser Language parsing and surface syntax

Comments

@ederag
Copy link
Contributor

ederag commented Jun 13, 2018

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.

@mbauman mbauman added broadcast Applying a function over a collection parser Language parsing and surface syntax labels Jun 13, 2018
@ararslan
Copy link
Member

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. 🙂

@ederag
Copy link
Contributor Author

ederag commented Jun 14, 2018

Searched again, and only found issue #25322 yet, but it is not related to Ref.

@mbauman
Copy link
Sponsor Member

mbauman commented Jun 14, 2018

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.

@ararslan
Copy link
Member

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.

@mbauman
Copy link
Sponsor Member

mbauman commented Jun 14, 2018

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).

@StefanKarpinski
Copy link
Sponsor Member

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.

@vtjnash
Copy link
Sponsor Member

vtjnash commented Jun 14, 2018

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 ||)

@ararslan
Copy link
Member

ararslan commented Jun 14, 2018

Maybe a good reason to deprecate the bitwise operators to something more readable

That was already discussed and decided against.

@ararslan
Copy link
Member

ararslan commented Jun 14, 2018

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.)

@mbauman
Copy link
Sponsor Member

mbauman commented Jun 14, 2018

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., #27563 (comment).

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.

@StefanKarpinski
Copy link
Sponsor Member

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 &.

Yes, ☝️

@stevengj
Copy link
Member

stevengj commented Jun 16, 2018

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.

@stevengj
Copy link
Member

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.

@andyferris
Copy link
Member

andyferris commented Jun 17, 2018

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 ith 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"?

@StefanKarpinski
Copy link
Sponsor Member

StefanKarpinski commented Jun 18, 2018

I like @andyferris's idea of getting more out of this syntax by using it for various kinds of references.

@ederag
Copy link
Contributor Author

ederag commented Aug 23, 2018

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).

@yurivish
Copy link
Contributor

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).

@ederag
Copy link
Contributor Author

ederag commented Aug 23, 2018

& also is a utf-8 character. It is treated differently. Why couldn't ?

@yurivish
Copy link
Contributor

yurivish commented Aug 24, 2018

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

@ederag
Copy link
Contributor Author

ederag commented Aug 25, 2018

@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.

@StefanKarpinski
Copy link
Sponsor Member

How would Python and Octave users encounter corner cases of a syntax that's unique to Julia immediately upon trying out the language?

@ederag
Copy link
Contributor Author

ederag commented Aug 26, 2018

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.

@tkf
Copy link
Member

tkf commented Jan 7, 2019

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" (#27608 (comment)). That is to say, you use & if you want to have a "reference" to that dimension in the outer broadcasting.

@StefanKarpinski
Copy link
Sponsor Member

I like the way you're thinking, @tkf. Have to ponder it for a while. @mbauman, any reactions?

@stevengj
Copy link
Member

stevengj commented Jan 7, 2019

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)))?

@tkf
Copy link
Member

tkf commented Jan 8, 2019

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 to f.(x, Ref(g.(x)) or f.(x, @lazy g.(x)) or f.(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:

"Minimal shield"

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...).

"Uniform shield"

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.

@mbauman
Copy link
Sponsor Member

mbauman commented Jan 8, 2019

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 #2591.

@tkf
Copy link
Member

tkf commented Jan 8, 2019

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 #2591 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
broadcast Applying a function over a collection parser Language parsing and surface syntax
Projects
None yet
Development

No branches or pull requests

9 participants