-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Dot-broadcasting for short-circuiting ops .&& and .|| #39594
Conversation
I have long wanted a proper fix for issue #5187. It was the very first Julia issue I filed. This is a shot at such a fix. This PR: * Enables parsing for `.&&` and `.||`. They are parsed into `Expr(:call, :.&&, ...)` expressions at the same precedence as their respective `&&` and `||`: ```julia-repl julia> Meta.show_sexpr(:(a .&& b)) (:call, :.&&, :a, :b) ``` * Unlike all other dotted operators `.op` (like `.+`), the `op`-alone part (`var"&&"`) is not an exported name from Base. As such, this effectively lowers to `broadcasted((x,y)->x && y, ...)`, but instead of using an anonymous function I've named it `Base.andand` and `Base.oror`: ```julia-repl julia> Meta.@lower a .&& b :($(Expr(:thunk, CodeInfo( @ none within `top-level scope' 1 ─ %1 = Base.broadcasted(Base.andand, a, b) │ %2 = Base.materialize(%1) └── return %2 )))) ``` * I've used a named function to enable short-circuiting behavior _within the broadcast kernel itself_. In the case that the second argument is a part of the same fused broadcast kernel, it will only evaluate if required: ```julia-repl julia> mutable struct F5187; x; end julia> (f::F5187)(x) = (f.x += x) julia> (iseven.(1:4) .|| (F5187(0)).(ones(4))) 4-element Vector{Real}: 1.0 true 2.0 true ``` * This also enables support for standalone `.&&` and `.||` as `BroadcastFunction`s, but of course they are not able to short-circuit when used as functions themselves. Request for feedback -------------------- * [ ] A good bikeshed could be had over the names themselves. We could actually use `var"&&"` and `var"||"` for these names — and it'd _almost_ simplify the implementation, but in order to do so we'd have to actually _export_ them from Base, too. It seems like it might just be confusing. * [ ] Is this the implementation we want? This uses `Broadcast.flatten` to create the lazy function needed for short-circuiting the second argument. This could alternatively be done directly within the parser — perhaps by resurrecting the old 0.5 broadcast parsing behavior. Someone else would have to do that work if they wanted it. * [ ] Do we want to support the stand-alone `.&&` and `.||` `BroadcastFunction`s if they cannot possibly short circuit?
Forgive me if I'm misunderstanding, but I think this doesn't correctly preserve the semantics of For instance: julia> @eval Base.Broadcast begin
andand(a, b) = a && b
function broadcasted(::typeof(andand), a, bc::Broadcasted)
bcf = flatten(bc)
broadcasted((a, args...) -> a && bcf.f(args...), a, bcf.args...)
end
oror(a, b) = a || b
function broadcasted(::typeof(oror), a, bc::Broadcasted)
bcf = flatten(bc)
broadcasted((a, args...) -> a || bcf.f(args...), a, bcf.args...)
end
end
broadcasted (generic function with 56 methods)
julia> using Base.Broadcast: oror
julia> oror.(iseven.(1:4), (F5187(0)).(ones(4)))
4-element Vector{Real}:
1.0
true
2.0
true
julia> oror.(isfinite.(1:4), throw("nonfinite number!"))
ERROR: "nonfinite number!"
Stacktrace:
[1] top-level scope
@ REPL[8]:1 That is, this appears to not really short circuit since the both the function arguments get evaluated. Edit: Okay, so it will work if I also broadcast the julia> oror.(isfinite.(1:4), throw.("nonfinite number!"))
4-element BitVector:
1
1
1
1 I'm not sure how to feel about this. Is there a way to make the first one work without So it seems I was just misunderstanding and this was intended design all along. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this makes sense to me, but I am a bit worried about how this will play together with some custom broadcast implementations. Not really sure how to test that though.
@MasonProtter Non-broadcasted calls are always evaluated before broadcasted ones and I don't really see any way around this. To me at least, this behavior for |
What do you think about using callable objects |
That's also what I did at first... but for their |
Yeah, perhaps we don't even want to make them callable. It seems to me like anywhere those are called directly is probably likely to be a bug. |
@simeonschaub Yeah, I've warmed considerably to this, but I will note that the the standard behaviour of executing calls is built around assuming that the objects being broadcasted are functions, and I still can't help but feel this is semantically a bit different. |
I see this a little differently: I see a fused broadcast as being a simple syntax for a complicated |
On the topic of how broadcast implementors would see this: I intentionally intercept this very early on in the call chain. There are two forms of Broadcasteds this uses:
This really isn't any different from what they deal with regularly. |
I would say probably yes just for consistency.
From triage: changing it would be the right thing but breaking, so keep it as-is for now. |
Thanks Simeon! I've updated the first post to reflect the current status quo. This is ready as far as I'm concerned. |
I think I would still lean towards |
If they're not callable, we still need to implement this method. Here's a potential diff (of course, diff --git a/base/broadcast.jl b/base/broadcast.jl
index 60ea22805f..cbd7cc9c86 100644
--- a/base/broadcast.jl
+++ b/base/broadcast.jl
@@ -179,12 +179,16 @@ function Broadcasted{Style}(f::F, args::Args, axes=nothing) where {Style, F, Arg
Broadcasted{Style, typeof(axes), Core.Typeof(f), Args}(f, args, axes)
end
-andand(a, b) = a && b
+struct AndAnd end
+const andand = AndAnd()
+broadcasted(::typeof(andand), a, b) = broadcasted((a, b) -> a && b, a, b)
function broadcasted(::typeof(andand), a, bc::Broadcasted)
bcf = flatten(bc)
broadcasted((a, args...) -> a && bcf.f(args...), a, bcf.args...)
end
-oror(a, b) = a || b
+struct OrOr end
+const oror = OrOr()
+broadcasted(::typeof(oror), a, b) = broadcasted((a, b) -> a || b, a, b)
function broadcasted(::typeof(oror), a, bc::Broadcasted)
bcf = flatten(bc)
broadcasted((a, args...) -> a || bcf.f(args...), a, bcf.args...) I actually started out this direction, but I changed to this because it's simpler (4 fewer LOC, and it lets Julia handle the singleton for us) and we are effectively defining a function method anyway. And it enabled the |
My worry is that these leaking out is likely to indicate a bug, since treating them just like regular functions would ignore their short-circuiting behavior and could lead to subtle mistakes. |
Co-authored-by: Matt Bauman <[email protected]>
I went ahead with this change for now, but feel completely free to revert if you'd prefer the previous approach. |
@mbauman Apparently, I can't request approval from you for your own PR, but please let me know what you think. |
I have long wanted a proper fix for issue JuliaLang#5187. It was the very first Julia issue I filed. This is a shot at such a fix. This PR: * Enables parsing for `.&&` and `.||`. They are parsed into `Expr(:call, :.&&, ...)` expressions at the same precedence as their respective `&&` and `||`: ```julia-repl julia> Meta.show_sexpr(:(a .&& b)) (:call, :.&&, :a, :b) ``` * Unlike all other dotted operators `.op` (like `.+`), the `op`-alone part (`var"&&"`) is not an exported name from Base. As such, this effectively lowers to `broadcasted((x,y)->x && y, ...)`, but instead of using an anonymous function I've named it `Base.andand` and `Base.oror`: ```julia-repl julia> Meta.@lower a .&& b :($(Expr(:thunk, CodeInfo( @ none within `top-level scope' 1 ─ %1 = Base.broadcasted(Base.andand, a, b) │ %2 = Base.materialize(%1) └── return %2 )))) ``` * I've used a named function to enable short-circuiting behavior _within the broadcast kernel itself_. In the case that the second argument is a part of the same fused broadcast kernel, it will only evaluate if required: ```julia-repl julia> mutable struct F5187; x; end julia> (f::F5187)(x) = (f.x += x) julia> (iseven.(1:4) .|| (F5187(0)).(ones(4))) 4-element Vector{Real}: 1.0 true 2.0 true ``` Co-authored-by: Simeon Schaub <[email protected]>
I have long wanted a proper fix for issue JuliaLang#5187. It was the very first Julia issue I filed. This is a shot at such a fix. This PR: * Enables parsing for `.&&` and `.||`. They are parsed into `Expr(:call, :.&&, ...)` expressions at the same precedence as their respective `&&` and `||`: ```julia-repl julia> Meta.show_sexpr(:(a .&& b)) (:call, :.&&, :a, :b) ``` * Unlike all other dotted operators `.op` (like `.+`), the `op`-alone part (`var"&&"`) is not an exported name from Base. As such, this effectively lowers to `broadcasted((x,y)->x && y, ...)`, but instead of using an anonymous function I've named it `Base.andand` and `Base.oror`: ```julia-repl julia> Meta.@lower a .&& b :($(Expr(:thunk, CodeInfo( @ none within `top-level scope' 1 ─ %1 = Base.broadcasted(Base.andand, a, b) │ %2 = Base.materialize(%1) └── return %2 )))) ``` * I've used a named function to enable short-circuiting behavior _within the broadcast kernel itself_. In the case that the second argument is a part of the same fused broadcast kernel, it will only evaluate if required: ```julia-repl julia> mutable struct F5187; x; end julia> (f::F5187)(x) = (f.x += x) julia> (iseven.(1:4) .|| (F5187(0)).(ones(4))) 4-element Vector{Real}: 1.0 true 2.0 true ``` Co-authored-by: Simeon Schaub <[email protected]>
I have long wanted a proper fix for issue JuliaLang#5187. It was the very first Julia issue I filed. This is a shot at such a fix. This PR: * Enables parsing for `.&&` and `.||`. They are parsed into `Expr(:call, :.&&, ...)` expressions at the same precedence as their respective `&&` and `||`: ```julia-repl julia> Meta.show_sexpr(:(a .&& b)) (:call, :.&&, :a, :b) ``` * Unlike all other dotted operators `.op` (like `.+`), the `op`-alone part (`var"&&"`) is not an exported name from Base. As such, this effectively lowers to `broadcasted((x,y)->x && y, ...)`, but instead of using an anonymous function I've named it `Base.andand` and `Base.oror`: ```julia-repl julia> Meta.@lower a .&& b :($(Expr(:thunk, CodeInfo( @ none within `top-level scope' 1 ─ %1 = Base.broadcasted(Base.andand, a, b) │ %2 = Base.materialize(%1) └── return %2 )))) ``` * I've used a named function to enable short-circuiting behavior _within the broadcast kernel itself_. In the case that the second argument is a part of the same fused broadcast kernel, it will only evaluate if required: ```julia-repl julia> mutable struct F5187; x; end julia> (f::F5187)(x) = (f.x += x) julia> (iseven.(1:4) .|| (F5187(0)).(ones(4))) 4-element Vector{Real}: 1.0 true 2.0 true ``` Co-authored-by: Simeon Schaub <[email protected]>
In JuliaLang/julia#39594 it was considered non-breaking enough to change the tokenization of ".&&" from `.&`,`&` to `.&&`. For JuliaSyntax it seems simplest to just use the newer tokenization for this and detect the use of the syntax in the parser, emitting an error on older versions. This leads to the most neat predictable parser errors.
…. Replace with regular vectorised and. Also, because it's over bitvectors, it's likely that it's not any faster: https://discourse.julialang.org/t/vectorized-short-circuit-operator/80336 See vectorised and pull request for 1.7: JuliaLang/julia#39594
I have long wanted a proper fix for issue #5187. It was the very first Julia issue I filed.
This is a shot at such a fix. This PR:
Enables parsing for
.&&
and.||
. They are parsed into syntacticExpr(:.&&, ...)
expressions at the same precedence as their respective&&
and||
:Unlike all other dotted operators
.op
(like.+
), theop
-alone part (var"&&"
) is not an exported name from Base. As such, this effectively lowers tobroadcasted((x,y)->x && y, ...)
, but instead of using an anonymous function I've named itBase.andand
andBase.oror
:I've used a named function to enable short-circuiting behavior within the broadcast kernel itself. In the case that the second argument is a part of the same fused broadcast kernel, it will only evaluate if required:
This also enables support for standalone.&&
and.||
asBroadcastFunction
s, but of course they are not able to short-circuit when used as functions themselves.Request for feedback
andand
andoror
are cutesy placeholders. We could actually usevar"&&"
andvar"||"
for these names — and it'd almost simplify the implementation, but in order to do so we'd have to actually export them from Base, too. It seems like it might just be confusing.The names are fine
Broadcast.flatten
to create the lazy function needed for short-circuiting the second argument. This could alternatively be done directly within the parser — perhaps by resurrecting the old 0.5 broadcast parsing behavior. Someone else would have to do that work if they wanted it.Yes
.&&
and.||
BroadcastFunction
s if they cannot possibly short circuit?Maybe, but deferred
1 .< A .< 2
produce(1 .< A) .&& (A .< 2)
? It currently uses.&
. I'm leaning towards keeping the status quo, even though this would represent a departure from the scalar lowering. I'd bet that changing this would be breaking for folks that are effectively working around Lower chained inequalities in an extendable way #39104 (knowingly or not).Maybe, but breaking and deferred
(See also #26792 which made space for this feature.)