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

support dots for unary operators #20249

Merged
merged 3 commits into from
Feb 2, 2017
Merged

Conversation

stevengj
Copy link
Member

@stevengj stevengj commented Jan 26, 2017

This PR implements dotted versions of all the unary operators, which as usual convert to fusing broadcast operations. (Closes #14161.)

@stevengj stevengj added the domain:broadcast Applying a function over a collection label Jan 26, 2017
@StefanKarpinski StefanKarpinski added this to the 0.6.0 milestone Jan 26, 2017
@Sacha0
Copy link
Member

Sacha0 commented Jan 26, 2017

Would also close #20039.

@Sacha0
Copy link
Member

Sacha0 commented Jan 26, 2017

Unary operators being prefix like normal function calls, having the dot precede rather than follow the operator seems a bit jarring. Being able to say "dots follow identifiers in prefix calls, and precede identifiers in infix calls" seems nice (rather than "dots follow identifiers in prefix calls --- except for a privileged set of unary operators where the dot precedes the operator --- and precede infix identifiers".) If anything, changing the position of dots attached to infix operators to match dot position elsewhere seems a better step, the rule then simply being "dots follow identifiers in dot calls". Thoughts? Thanks!

@stevengj
Copy link
Member Author

stevengj commented Jan 26, 2017

@Sacha0, for f.(x) we think of . as modifying the "function-call operator" (. Because unary operators don't require parentheses, putting the dot after a unary operator would create a new parsing ambiguity for something +.2. Also, since something like - is both unary and binary, it makes more sense to me to use .- for both cases.

stevengj added a commit to stevengj/julia that referenced this pull request Jan 26, 2017
Nullable(1) === Nullable(6)
@test Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable{Int}() .+
Nullable(1) .+ Nullable(1) |> isnull_oftype(Int)
@test (Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable(1) .+
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this change related?

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevengj
Copy link
Member Author

Should be good to merge?

@Sacha0
Copy link
Member

Sacha0 commented Jan 27, 2017

Should be good to merge?

I would appreciate a bit more time to think this through and write a response. Best!

@StefanKarpinski
Copy link
Sponsor Member

I'm not clear what there is to consider – this should definitely work, for the sake of consistency. Sure, we may want another, nicer syntax for it, but this seems uncontroversial.

@Sacha0
Copy link
Member

Sacha0 commented Jan 27, 2017

for f.(x) we think of . as modifying the "function-call operator" (.

We should mention this interpretation in the documentation if it is now official. (I remember this interpretation being provided by @nalimilan in #8450 and downstream threads [with e.g. a little dissent], but could not find it in the documentation. The existing documentation never separates f, ., and ( in e.g. f.(...).)

While that interpretation makes f.(...) consistent with a .+ b, how does one reconcile that interpretation with prefix calls of infix operators, e.g. .+([1, 2, 3], [4, 5, 6]) or .+([1,2], [3,4], [5,6])?

Do you think a user familiar with the f.(...) syntax attempting to apply ! element-wise over A would be more likely to reach for !.A or .!A?

Because unary operators don't require parentheses, putting the dot after a unary operator would create a new parsing ambiguity for something +.2.

How is this ambiguity different from e.g. the former 2.+1 ambiguity of dot-terminated numeric literal juxtaposition with dot operators? What would be the problem with requiring disambiguation with whitespace or parens in this case as well? (That seems like it might help code readability in any case?)

Also, since something like - is both unary and binary, it makes more sense to me to use .- for both cases.

Is that not an equally strong argument for using -. in both cases (status quo aside)? (And is that not an even stronger argument for making all use of dot syntax consistent?)

this should definitely work, for the sake of consistency

Could you expand?

this seems uncontroversial

The apparent inconsistency between f.(...) and a .+ b was a major (perhaps even central) point of contention in #8450 and downstream threads. This is an extension of that controversial syntax to additional cases. Poking at it from all angles a bit seems reasonable? Best!

@StefanKarpinski
Copy link
Sponsor Member

This kind of relates to an issue I stumbled upon yesterday. I needed to pass .+ to reduce as the reduction function. When .+ was its own operator, this was fine since you could write reduce(.+, ...) but now you can't do that and you have to write reduce((a,b)->a.+b, ...) which is not awful but kind of annoying. One simple option would be to curry broadcast so that broadcast(f) = (args...)->broadcast(f, args...); then you could write reduce(broadcast(+), ...) which is not as terse as reduce(.+, ...) but easier than writing out the anonymous function.

@stevengj
Copy link
Member Author

stevengj commented Jan 27, 2017

@Sacha0, I'm not sure what you mean by an "official interpretation". The syntax is documented, but how you want to describe it is up to you. Technically, of course, ( is not an operator, nor is .(. Of course, it wouldn't hurt to mention that rationale for the syntax somewhere; maybe that's what you mean?

It sounds like you are arguing to rename .+ to +. even for binary operators? That would be a much more disruptive change, and is way outside the scope of this PR. I agree that this was a point of contention in #8450, but I thought it was settled, and is now water under the bridge; I'm skeptical that we should re-open that can of worms now.

Given that .+ is broadcasting as a binary operator, it seems clear that it should work as a broadcasting unary operator as well, and hence similarly for other unary operators. If the unary operator is followed by parentheses (which are not required for unary operators!), I agree that we may also want to support .( call syntax, but that can be a future PR.

The 2.+ ambiguity is indeed not that much different from the +.2 ambiguity, but the difference is that the latter would be a new ambiguity that does not currently exist. I'd rather not have both of them at the same time.

@stevengj
Copy link
Member Author

stevengj commented Jan 27, 2017

@StefanKarpinski, I think that in 0.7 or 1.0 or whatever we could easily make .+ work as a function argument again. Right now, however, the .+ symbol (when treated as a function) seems like it is needed for deprecation purposes.

@stevengj
Copy link
Member Author

stevengj commented Jan 27, 2017

To be precise, in #8450 we settled on f.(args...) rather than .f(args...) or some other syntax. Whether we want to now rename .+ to +. is a separate question that wasn't really discussed, which I guess we could argue about now (though hopefully not in this PR). Personally, I don't think it's worth the disruption (and it would take at least two release cycles, because +. is not even parsed as an operator yet so it would be impossible to implement backwards compatibility if you switched in the same release that implemented the parsing). Nor am I thrilled at the prospect of yet another long bike-shed argument about spelling.

@stevengj
Copy link
Member Author

(Note that broadcast(f) already means f(), so we would need a different syntax for currying.)

@stevengj
Copy link
Member Author

stevengj commented Jan 30, 2017

It's pretty frustrating not to be able to write e.g. exp.(.-x.^2). Even if we eventually decide to rename every .⨳ into ⨳., allowing unary .- etc now won't make the deprecation process any harder.

@GlenHertz
Copy link
Contributor

@stevenj where is the rational for func. vs .func explained from a readability point of view? I could find out how it was decided. I find the syntax very difficult to grok: x .= ∆.(.-3 .* √.(.x)). A dot always before to be so much easier and MATLAB friendly: x .= .∆(.-3 .* .√(.x)) as the dots disappear instead of disjoin the expression. Would you consider this going to a vote or is it a done deal?

@stevengj
Copy link
Member Author

stevengj commented Jan 30, 2017

@GlenHertz, it was discussed at length in #8450 and #15032. I don't think we're looking to re-open the debate, nor do we decide these things by votes. Regarding readability, there is the possibility of an @. or @dots macro that would just add dots to every operator and function call in an expression.

@GlenHertz
Copy link
Contributor

@stevenj I don't mean to be rude as I'm not a developer. This is an amazing feature. From what I could tell the main issue was parsing ambiguities like .sin(x).*.cos(x) -- in practice spaces are recommended to improve readability (even if it isn't ambiguous -- func(a,b) vs func(a, b)). At the start .func seemed most liked. This is the first syntax of Julia that I cannot easily guess: .*(a, b) vs *.(a, b). When writing a macro is there a way to know which side the dot should go on?

@stevengj
Copy link
Member Author

Not sure what you mean about "when writing a macro". But can we confine this PR to discussion about what to do with unary operators in 0.6?

Copy link
Sponsor Member

@StefanKarpinski StefanKarpinski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approve of this it seems like a logical step given everything else. If we want to discuss bigger changes elsewhere, that's fine but I don't really think that affects whether we go ahead with this PR or not.

@Sacha0
Copy link
Member

Sacha0 commented Jan 30, 2017

Separate from the discussion above, how do operators that are both unary and binary behave with this PR? That is, what does .+(1,2,3) do? Is .+(1,2,3) a broadcast of n-ary + over scalar arguments 1, 2, and 3 yielding 6, or instead unary + broadcast over the tuple (1, 2, 3) yielding (1, 2, 3)? Another example, is .-(1, 2) a broadcast of binary - with scalar arguments 1 and 2 yielding -1, or instead unary - broadcast over the tuple (1, 2) yielding (-1, -2)? Best! (Apologies for slow responses at the moment.)

@wsshin
Copy link
Contributor

wsshin commented Jan 30, 2017

Not sure if it is good time to add a comment to this interesting discussion, but I would like to report my practice of broadcasting unary operators over a tuple, which I believe this PR tries to implement. Just in case it helps future discussion.

Even before this PR, I have been already broadcasting unary operators over a tuple. The trick is to do (op).(tuple). For example,

julia> (-).((1,2,3))
(-1,-2,-3)

julia> (!).((true,false,true))
(false,true,false)

Because of the formal similarity with this already working (op).(tuple), I would prefer op.tuple to .op tuple for broadcasting unary operators. It would feel a bit weird if .op tuple produces the same result as (op).(tuple) but op.tuple doesn't.

@stevengj
Copy link
Member Author

@wsshin, the reason that (op).(args...) works is that parenthesizing operators always lets you access them as ordinary function calls.

But the whole point of defining something as a unary operator is that the caller is not forced to use parentheses. If you require parentheses, it might as well not be an operator at all.

@wsshin
Copy link
Contributor

wsshin commented Jan 31, 2017

@stevengj, thanks for explaining why (op).(tuple) works! I didn't know parenthesizing an operator makes it a function.

However, I think the point I wanted to raise is still valid: (op).(tuple) produces the intended result, and it looks closer to op.tuple than .op tuple. So I think op.tuple could be more appealing than .op tuple to some users.

Let me ask this question. Suppose we have the dot syntax for broadcasting a function as implemented now, but don't have any element-wise operators defined. In other words, we have f.(tuple1, tuple2, ...), but don't have things like tuple1 .+ tuple2. Now, if you are to define an element-wise unary minus operator, which one would be your choice: -.tuple or .-tuple? Because of the formal similarity with the function broadcasting syntax, I would choose the former, but different people may think differently...

A related question is this. Which syntax is more fundamental syntax to Julia: the dot syntax for broadcasting a function, or the element-wise operators inherited from MATLAB? (Because I think the dot syntax for broadcasting is more fundamental to Julia, in the previous paragraph I considered the case where we have the dot syntax but don't have MATLAB-like element-wise operators.) If one thinks the dot syntax for broadcasting is more fundamental, appending a dot to operators/functions it belongs to seems like a design to follow. (I know there is an interpretation that the dot in f.(args...) belongs to the function call operator (...) rather than to the function f, and therefore the dot is actually prepended to (...) rather than appended to f. I think that is a beautiful interpretation. However at the same time I feel that it is not a natural interpretation, but somewhat made up to interpret the broadcasting syntax as consistent with the existing MATLAB-like element-wise operators...)

Maybe I prefer op.tuple because I'm so used to the a.b notation in object-oriented programming. For some reason, the form a.b is recognized as a single unit, so when I see op.tuple I know that op must be applied to x. However, from .op tuple such binding between op and tuple does not register to me. For example, when I see sin(-.x), I know that -.x is one unit, so I need to apply - to x first and then apply sin to -.x. However, if I see sin(.-x), I feel that -x is one unit that needs to be applied to sin(.※), which is not a correct interpretation.

If we choose op.tuple over .op tuple for unary operators, for consistency I think it would be better to use the same form for binary operators as well like @Sacha0 suggested. Such a decision would be beyond this thread. I totally agree with @stevengj and @StefanKarpinski that we need a separate thread for this!

(And I don't mind having .op tuple instead of op.tuple for now. Having the element-wise unary operators will be very convenient in any case!)

@stevengj
Copy link
Member Author

stevengj commented Jan 31, 2017

The bottom line (for this PR) is this:

  • We aren't going to change the spelling of binary operators in 0.6: .+ must remain .+ for now. (We can debate whether we want to make this change elsewhere, but the earliest it could happen would be two releases from now, in order to have a proper deprecation cycle due to the required parser changes.)

  • Given that 0.6 has .+ as a fusing-broadcast binary operator, shouldn't it also have .+ as a similar unary operator? And hence for the other unary operators as well?

Supporting . prefixes for unary operators now won't make it any harder to switch to +. in the future if we decide on that, because the big pain will come from the change in binary operators anyway. And I really don't think that this PR is the place to argue for changing the spelling of binary operators; please open a separate issue for that debate if you must.

@martinholters
Copy link
Member

@Sacha0 has a valid point there. We have:

julia> (1,4).-(1,2)
(0,2)

This would strongly suggest we want:

julia> .-(1,2)
(-1,-2)

That is, the (unary) - is broadcast over the tuple (1,2). Yet presently:

julia> .-(1,2)
-1

That is, 1 and 2 are separate arguments to a broadcast (binary) -. But we do want to change this, don't we?

@stevengj
Copy link
Member Author

@martinholters, that is a parsing ambiguity, but the behavior is not changed by this PR.

(Note that #20321 will somewhat lessen the need for unary dotted operators, since you will simply be able to do @. exp(-x^2) to make a fused broadcasting version of this.)

@martinholters
Copy link
Member

that is a parsing ambiguity, but the behavior is not changed by this PR

But should it?

If (a,b).-(c,d) is equivalent to broadcast(-, (a,b), (c,d)), then why is .-(c, d) equivalent to broadcast(-, c, d) instead of broadcast(-, (c,d)). Further, .- (c,d) will likely mean the latter with this PR (instead of giving an error as on master)? Then .-(c,d) and .- (c,d) would mean different things which strikes me as rather unfortunate.

@stevengj
Copy link
Member Author

@martinholters, it's ambiguous; either one could be correct, like several other space-sensitive parsing ambiguities in Julia. My feeling is that the current behavior is fine, and I don't think this usage will be common anyway. Leaving the behavior as-is also has the advantage of not making this PR a breaking change.

@martinholters
Copy link
Member

Leaving the behavior as-is also has the advantage of not making this PR a breaking change.

True.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:broadcast Applying a function over a collection
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants