-
-
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
Deprecate defaulting to scalar in broadcast (take 2) #26435
Conversation
Does wrapping all scalars in a |
Numbers have both
|
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.
I like this direction. Automatically unwrapping Ref
is technically a breaking change, but I don't see a clean way of deprecating this. Maybe add a deprecation warning to broadcastable(::Ref)
?
base/broadcast.jl
Outdated
""" | ||
struct DefaultArrayStyle{N} <: AbstractArrayStyle{N} end | ||
(::Type{<:DefaultArrayStyle})(::Val{N}) where N = DefaultArrayStyle{N}() | ||
const DefaultVectorStyle = DefaultArrayStyle{1} | ||
const DefaultMatrixStyle = DefaultArrayStyle{2} | ||
BroadcastStyle(::Type{<:AbstractArray{T,N}}) where {T,N} = DefaultArrayStyle{N}() | ||
BroadcastStyle(::Type{<:Ref}) = DefaultArrayStyle{0}() | ||
BroadcastStyle(::Type{<:Union{Base.RefValue,Number}}) = DefaultArrayStyle{0}() |
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.
This also changes RefArray
(which is a good thing). Should there be a BroadcastStyle
for RefArray
?
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.
Why is that a good thing (I don't actually know what this return value means)? For context, RefArray
is a pointer to precisely one element of an array. It also changes Ptr
, if that matters?
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.
All arguments' BroadcastStyle
s are combined with a promote-like mechanism in order to select a broadcasting implementation. For the most part, it's largely used as a kludge around the lack of a "at least one vararg is of type T
" dispatch, but it's more complicated to deal with cases that would otherwise be ambiguities.
Yes, Ptr
has neither axes or indexing defined, so it cannot be — by itself — considered like a 0-dimensional array. I don't think RefArray
is common enough to add special broadcasting support for it.
Before:
julia> identity.(Ref([1,2,3],1))
0-dimensional Array{Int64,0}:
1
After (edit: now out of date):
julia> identity.(Ref([1,2,3],1))
┌ Warning: broadcast will default to iterating over its arguments in the future. Wrap arguments of
│ type `x::Base.RefArray{Int64,Array{Int64,1},Nothing}` with `Ref(x)` to ensure they broadcast as "scalar" elements.
│ caller = top-level scope
└ @ Core :0
Base.RefArray{Int64,Array{Int64,1},Nothing}([1, 2, 3], 1, nothing)
In the future, it will be an error because:
julia> collect(Ref([1,2,3],1))
ERROR: MethodError: no method matching length(::Base.RefArray{Int64,Array{Int64,1},Nothing})
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.
I don't think RefArray is common enough to add special broadcasting support for it.
However, RefValue
is an implementation detail, and should never be mentioned explicitly either: RefArray
is simply an alternative implementation of it. We know that length(::Ref) == 1
forall Ref subtypes, we've just hesitated to add that definition to the code until needed to keep the API small.
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.
This is largely a historical accident due to how I ended up here… and trying to work around Ptr
. As it stands, I can easily remove all uses of RefValue
. I just pushed a change.
New after:
julia> identity.(Ref([1,2,3],1))
1
base/deprecated.jl
Outdated
if Base.Broadcast.BroadcastStyle(typeof(x)) isa Broadcast.Unknown | ||
depwarn(""" | ||
broadcast will default to iterating over its arguments in the future. Wrap arguments of | ||
type `x::$(typeof(x))` with `Ref(x)` to ensure they broadcast as "scalar" elements. |
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.
Maybe add:
or define
broadcastable(x::MyType) = Ref(x)
for the conversion to be automatic.
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.
I'm guessing that the most common reader of this deprecation will be an end-user.
base/broadcast.jl
Outdated
broadcastable(x::AbstractArray) = x | ||
# In the future, default to collecting arguments. TODO: uncomment once deprecations are removed | ||
# broadcastable(x) = BroadcastStyle(typeof(x)) isa Unknown ? collect(x) : x | ||
# broadcastable(::Dict) = error("intentionally unimplemented to allow development in 1.x") |
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.
Will this be a different PR? We should deprecate this behaviour in 0.7
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.
AbstractDict
hits the fallback ::Any
deprecation. Next to that deprecation is a task-item to un-comment these definitions, so when that happens it will go from a warning to an error.
@inline broadcast(f, A, Bs...) = | ||
broadcast(f, combine_styles(A, Bs...), nothing, nothing, A, Bs...) | ||
@inline function broadcast(f, A, Bs...) | ||
A′ = broadcastable(A) |
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.
It's unfortunate that the prime looks so similar to the adjoint...
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.
¯\_(ツ)_/¯
There's more than one of us using this style in base to represent roughly-the-same-value-but-maybe-slightly-different.
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.
You could also use Ã
, but I agree that the \prime
style is pretty common.
base/broadcast.jl
Outdated
broadcastable(x::AbstractArray) = x | ||
# In the future, default to collecting arguments. TODO: uncomment once deprecations are removed | ||
# broadcastable(x) = BroadcastStyle(typeof(x)) isa Unknown ? collect(x) : x | ||
# broadcastable(::Dict) = error("intentionally unimplemented to allow development in 1.x") |
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.
::AbstractDict
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.
Also we might consider if NamedTuple
should maybe broadcast together with a AbstractDict{Symbol}
rather than by iteration index.
@nanosoldier |
Nanosoldier is going to fail without that commit I just pushed. |
Well it didn't listen to me anyway... @nanosoldier (Put I am just being overly cautious) |
base/deprecated.jl
Outdated
@@ -687,6 +687,21 @@ end | |||
# After deprecation is removed, enable the @testset "indexing by Bool values" in test/arrayops.jl | |||
# Also un-comment the new definition in base/indices.jl | |||
|
|||
# Broadcast no longer defaults to treating its arguments as scalar (#) | |||
function Broadcast.broadcastable(x) |
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.
should have @noinline
to ensure depwarn tracking works
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.
Thanks for your efforts to improve broadcast
!
Cc: @timholy
base/broadcast.jl
Outdated
|
||
Return either `x` or an object like `x` such that it supports `axes` and indexing. | ||
""" | ||
broadcastable(x::Union{Symbol,String,Type,Function,UndefInitializer,Nothing,RoundingMode}) = Ref(x) |
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.
I guess you can add Missing
to this list.
base/broadcast.jl
Outdated
@@ -457,13 +460,13 @@ as in `broadcast!(f, A, A, B)` to perform `A[:] = broadcast(f, A, B)`. | |||
end | |||
|
|||
# Optimization for the all-Scalar case. |
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.
Is this still the "all scalar case"?
base/broadcast.jl
Outdated
arguments and the scalars themselves in `As`. Singleton and missing dimensions | ||
are expanded to match the extents of the other arguments by virtually repeating | ||
the value. By default, only a limited number of types are considered scalars, | ||
including `Number`s, `Type`s and `Function`s; all other arguments are iterated |
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.
Could give the full list (in particular strings).
base/broadcast.jl
Outdated
|
||
- If all the arguments are scalars, it returns a scalar. |
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.
I imagine this rule still applies? Maybe worth keeping for clarity even if it's covered by more general rules below.
@@ -905,6 +905,11 @@ Deprecated or removed | |||
* `map` on dictionaries previously operated on `key=>value` pairs. This behavior is deprecated, | |||
and in the future `map` will operate only on values ([#5794]). | |||
|
|||
* Previously, broadcast defaulted to treating its arguments as scalars if they were not |
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.
It would be useful to add a sentence for package developers mentioning broadcastable
, just like the deprecation does.
base/broadcast.jl
Outdated
|
||
Return either `x` or an object like `x` such that it supports `axes` and indexing. | ||
""" | ||
broadcastable(x::Union{Symbol,AbstractString,Type,Function,UndefInitializer,Nothing,RoundingMode,Missing}) = Ref(x) |
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.
Can remove Type
here since it has a method below.
4f2c4fe
to
fb59643
Compare
@nanosoldier |
base/broadcast.jl
Outdated
broadcastable(x) | ||
|
||
Return either `x` or an object like `x` such that it supports `axes` and indexing. | ||
""" |
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.
Perhaps add an example?
# Examples
```jldoctest
julia> Broadcast.broadcastable([1, 2, 3])
3-element Array{Int64,1}:
1
2
3
julia> Broadcast.broadcastable("Hello, world!")
Base.RefValue{String}("Hello, world!")
```
base/broadcast.jl
Outdated
@@ -423,6 +423,23 @@ end | |||
broadcastable(x) | |||
|
|||
Return either `x` or an object like `x` such that it supports `axes` and indexing. | |||
|
|||
If `x` supports iteration, the returned value should match [`collect(x)`](@ref). |
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.
This isn't true. e.g. collect(3)
returns an Array{Int,0}
, but broadcastable
just returns 3
.
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.
Fair. How about I add with respect to axes and indexing
?
Edit: or If `x` supports iteration, the returned value should have the same `axes` and indexing behaviors as [`collect(x)`](@ref)
3272ab2
to
37bd43e
Compare
@nanosoldier |
Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @ararslan |
That does not look too bad – all these regressions seem like they could be in the noise. |
The slowdowns for tuple broadcasting seem like they might be real. They are testing:
for various |
Well that was a remarkably simple fix. Once CI passes again I'd like to merge. |
While we wait on CI. |
Except that Nanosoldier takes about 3 times longer than our longest running CI build. 😉 |
I had to restart the server. @nanosoldier |
Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @ararslan |
There is still a regression for |
I fought with that remaining allocation for quite some time with no success. It's the Given that this regression is small and the optimizer is getting revamped, I'm inclined to merge. |
Broadcast now calls a special helper function, `broadcastable`, on each argument. It ensures that the arguments support indexing and have a shape, or otherwise have defined a custom `BroadcastStyle`.
6c76d2b
to
54f54a5
Compare
Would be interesting to see if the new optimizer can eliminate that allocation. |
The manual section on the broadcasting interface still uses
when building the docs. |
And Lines 593 to 596 in 7eb5d44
julia> broadcast(+, 1.0, (0, -2.0), Ref(1))
(2.0, 0.0) That example seem rather pointless now that |
Amazing what a great exercise writing documentation is. This is a followup to #26435, and more clearly lays out the requirements for defining a custom (non-AbstractArray) type that can participate in broadcasting. The biggest functional change here is that types that define their own BroadcastStyle must now also implement `broadcastable` to be a no-op.
Thank you! See #26601. |
* Make LinAlg fess up to using Base._return_type * Simplify broadcastable a bit further and add docs Amazing what a great exercise writing documentation is. This is a followup to #26435, and more clearly lays out the requirements for defining a custom (non-AbstractArray) type that can participate in broadcasting. The biggest functional change here is that types that define their own BroadcastStyle must now also implement `broadcastable` to be a no-op. * Trailing comma, doc fixes from review; more tests
Thanks for the design of The only problem is defining If |
There is a default, which is |
@fredrikekre https://discourse.julialang.org/t/broadcasting-over-structs-in-1-0-what-am-i-supposed-to-do/13408 We have to implement the interface like julia> Broadcast.broadcastable(m::M) = Ref(m) to make a type work as a scalar. A minimal working example: julia> struct A end
julia> println.(A())
ERROR: MethodError: no method matching length(::A)
Closest candidates are:
length(::Core.SimpleVector) at essentials.jl:571
length(::Base.MethodList) at reflection.jl:728
length(::Core.MethodTable) at reflection.jl:802
...
Stacktrace:
[1] _similar_for(::UnitRange{Int64}, ::Type, ::A, ::Base.HasLength) at ./array.jl:532
[2] _collect(::UnitRange{Int64}, ::A, ::Base.HasEltype, ::Base.HasLength) at ./array.jl:563
[3] collect(::A) at ./array.jl:557
[4] broadcastable(::A) at ./broadcast.jl:609
[5] broadcasted(::Function, ::A) at ./broadcast.jl:1134
[6] top-level scope at none:0 |
This PR replaces #26212. The end result is the same, but I like this implementation much better.
This PR consists of two commits — both pass tests locally and I'd like to keep them separate. The first commit changes the default behavior of broadcast to collect its arguments instead of treating them as scalars. Broadcast now calls a special helper function,
broadcastable
, on each argument. It ensures that the arguments support indexing and have a shape, or otherwise have defined a customBroadcastStyle
. The second commit layers on top a deprecation mechanism that makes this — almost entirely — non-breaking.I believe this to be a compromise that will fix #18618 once deprecations are removed. Note that I kept all the types that are currently tested to behave as scalars as specializing
broadcastable
to return something that supports both indexing andaxes
, or otherwise has its ownBroadcastStyle
and internal implementation.This also changes the behavior of broadcast such that if it ever wants to return a 0-dimensional array, it'll just "unwrap" that 0-dimensional array and return the contents instead.
Reasons I like this approach better than #26212:
Ref
to make them broadcast as scalars. In RFC: Deprecate defaulting to scalar in broadcast #26212, adding aRef
to an otherwise scalar expression would result in a 0-dimensional array as a result. Yes, I generally have a very strong aversion to these kinds of auto-wrapping/unwrapping, but in this case I believe it is warranted and ends up with fewer special cases. 0-dimensional arrays and "scalar" types are treated identically as far as broadcast is concerned.broadcastable
psuedo-conversion method makes it easier for other code to "work-around" broadcast and know how it'll behave. Callbroadcastable
on a thing and then you'll be able to know whatbroadcast
is going to do on the basis of itsaxes
. No digging around inBroadcastStyle
s or other internals — this one function makes sure that iterables and everything else can just work. (This isn't quite true, yet, asRef
isn't fully conforming with the iterable andaxes
spec, but I'd like to implement that separately.)Broadcast.Scalar()
styles.