Skip to content

Commit 830d8c9

Browse files
committed
Revert "Limit broadcast mechanism over Nullables (#19787)"
This reverts commit f147aaa.
1 parent 51cedb5 commit 830d8c9

File tree

7 files changed

+145
-43
lines changed

7 files changed

+145
-43
lines changed

NEWS.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,13 @@ This section lists changes that do not have deprecation warnings.
5555

5656
* `broadcast` now treats `Ref` (except for `Ptr`) arguments as 0-dimensional
5757
arrays ([#18965]).
58-
5958
* `broadcast` now handles missing data (`Nullable`s) allowing operations to
60-
be lifted over mixtures of `Nullable`s and scalars, as if the `Nullable`
61-
were like an array with zero or one element. ([#16961], [#19787]).
59+
be lifted over `Nullable`s, as if the `Nullable` were like an array with
60+
zero or one element. ([#16961]). Note that many situations where `Nullable`
61+
types had been treated like scalars before will no longer work. For
62+
example, `get.(xs)` on `xs::Array{T <: Nullable}` will now treat the
63+
nullables as a container, and attempt to operate on the data contained.
64+
This use case will need to be migrated to `map(get, xs)`.
6265

6366
* The runtime now enforces when new method definitions can take effect ([#17057]).
6467
The flip-side of this is that new method definitions should now reliably actually
@@ -801,4 +804,3 @@ Language tooling improvements
801804
[#19543]: https://github.com/JuliaLang/julia/issues/19543
802805
[#19598]: https://github.com/JuliaLang/julia/issues/19598
803806
[#19635]: https://github.com/JuliaLang/julia/issues/19635
804-
[#19787]: https://github.com/JuliaLang/julia/issues/19787

base/broadcast.jl

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ module Broadcast
55
using Base.Cartesian
66
using Base: promote_eltype_op, linearindices, tail, OneTo, to_shape,
77
_msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache,
8-
nullable_returntype, null_safe_eltype_op, hasvalue
8+
nullable_returntype, null_safe_eltype_op, hasvalue, is_nullable_array
99
import Base: broadcast, broadcast!
1010
export broadcast_getindex, broadcast_setindex!, dotview
1111

12-
typealias ScalarType Union{Type{Any}, Type{Nullable}}
13-
1412
## Broadcasting utilities ##
1513
# fallbacks for some special cases
1614
@inline broadcast(f, x::Number...) = f(x...)
@@ -30,28 +28,37 @@ containertype(::Type) = Any
3028
containertype{T<:Ptr}(::Type{T}) = Any
3129
containertype{T<:Tuple}(::Type{T}) = Tuple
3230
containertype{T<:Ref}(::Type{T}) = Array
33-
containertype{T<:AbstractArray}(::Type{T}) = Array
31+
containertype{T<:AbstractArray}(::Type{T}) =
32+
is_nullable_array(T) ? Array{Nullable} : Array
3433
containertype{T<:Nullable}(::Type{T}) = Nullable
3534
containertype(ct1, ct2) = promote_containertype(containertype(ct1), containertype(ct2))
3635
@inline containertype(ct1, ct2, cts...) = promote_containertype(containertype(ct1), containertype(ct2, cts...))
3736

3837
promote_containertype(::Type{Array}, ::Type{Array}) = Array
3938
promote_containertype(::Type{Array}, ct) = Array
4039
promote_containertype(ct, ::Type{Array}) = Array
41-
promote_containertype(::Type{Tuple}, ::ScalarType) = Tuple
42-
promote_containertype(::ScalarType, ::Type{Tuple}) = Tuple
40+
promote_containertype(::Type{Tuple}, ::Type{Any}) = Tuple
41+
promote_containertype(::Type{Any}, ::Type{Tuple}) = Tuple
4342
promote_containertype(::Type{Any}, ::Type{Nullable}) = Nullable
4443
promote_containertype(::Type{Nullable}, ::Type{Any}) = Nullable
44+
promote_containertype(::Type{Nullable}, ::Type{Array}) = Array{Nullable}
45+
promote_containertype(::Type{Array}, ::Type{Nullable}) = Array{Nullable}
46+
promote_containertype(::Type{Array{Nullable}}, ::Type{Array{Nullable}}) =
47+
Array{Nullable}
48+
promote_containertype(::Type{Array{Nullable}}, ::Type{Array}) = Array{Nullable}
49+
promote_containertype(::Type{Array}, ::Type{Array{Nullable}}) = Array{Nullable}
50+
promote_containertype(::Type{Array{Nullable}}, ct) = Array{Nullable}
51+
promote_containertype(ct, ::Type{Array{Nullable}}) = Array{Nullable}
4552
promote_containertype{T}(::Type{T}, ::Type{T}) = T
4653

4754
## Calculate the broadcast indices of the arguments, or error if incompatible
4855
# array inputs
4956
broadcast_indices() = ()
5057
broadcast_indices(A) = broadcast_indices(containertype(A), A)
51-
broadcast_indices(::ScalarType, A) = ()
58+
broadcast_indices(::Union{Type{Any}, Type{Nullable}}, A) = ()
5259
broadcast_indices(::Type{Tuple}, A) = (OneTo(length(A)),)
5360
broadcast_indices(::Type{Array}, A::Ref) = ()
54-
broadcast_indices(::Type{Array}, A) = indices(A)
61+
broadcast_indices{T<:Array}(::Type{T}, A) = indices(A)
5562
@inline broadcast_indices(A, B...) = broadcast_shape((), broadcast_indices(A), map(broadcast_indices, B)...)
5663
# shape (i.e., tuple-of-indices) inputs
5764
broadcast_shape(shape::Tuple) = shape
@@ -125,7 +132,9 @@ end
125132

126133
Base.@propagate_inbounds _broadcast_getindex(A, I) = _broadcast_getindex(containertype(A), A, I)
127134
Base.@propagate_inbounds _broadcast_getindex(::Type{Array}, A::Ref, I) = A[]
128-
Base.@propagate_inbounds _broadcast_getindex(::ScalarType, A, I) = A
135+
Base.@propagate_inbounds _broadcast_getindex(::Type{Any}, A, I) = A
136+
Base.@propagate_inbounds _broadcast_getindex(::Union{Type{Any},
137+
Type{Nullable}}, A, I) = A
129138
Base.@propagate_inbounds _broadcast_getindex(::Any, A, I) = A[I]
130139

131140
## Broadcasting core
@@ -276,20 +285,28 @@ ftype(f, A) = typeof(f)
276285
ftype(f, A...) = typeof(a -> f(a...))
277286
ftype(T::Type, A...) = Type{T}
278287

279-
typestuple(a) = (Base.@_pure_meta; Tuple{eltype(a)})
280-
typestuple(T::Type) = (Base.@_pure_meta; Tuple{Type{T}})
281-
typestuple(a, b...) = (Base.@_pure_meta; Tuple{typestuple(a).types..., typestuple(b...).types...})
288+
# nullables need to be treated like scalars sometimes and like containers
289+
# other times, so there are two variants of typestuple.
290+
291+
# if the first argument is Any, then Nullable should be treated like a
292+
# scalar; if the first argument is Array, then Nullable should be treated
293+
# like a container.
294+
typestuple(::Type, a) = (Base.@_pure_meta; Tuple{eltype(a)})
295+
typestuple(::Type{Any}, a::Nullable) = (Base.@_pure_meta; Tuple{typeof(a)})
296+
typestuple(::Type, T::Type) = (Base.@_pure_meta; Tuple{Type{T}})
297+
typestuple{T}(::Type{T}, a, b...) = (Base.@_pure_meta; Tuple{typestuple(T, a).types..., typestuple(T, b...).types...})
282298

283-
ziptype(A) = typestuple(A)
284-
ziptype(A, B) = (Base.@_pure_meta; Iterators.Zip2{typestuple(A), typestuple(B)})
285-
@inline ziptype(A, B, C, D...) = Iterators.Zip{typestuple(A), ziptype(B, C, D...)}
299+
# these functions take the variant of typestuple to be used as first argument
300+
ziptype{T}(::Type{T}, A) = typestuple(T, A)
301+
ziptype{T}(::Type{T}, A, B) = (Base.@_pure_meta; Iterators.Zip2{typestuple(T, A), typestuple(T, B)})
302+
@inline ziptype{T}(::Type{T}, A, B, C, D...) = Iterators.Zip{typestuple(T, A), ziptype(T, B, C, D...)}
286303

287-
_broadcast_type(f, T::Type, As...) = Base._return_type(f, typestuple(T, As...))
288-
_broadcast_type(f, A, Bs...) = Base._default_eltype(Base.Generator{ziptype(A, Bs...), ftype(f, A, Bs...)})
304+
_broadcast_type{S}(::Type{S}, f, T::Type, As...) = Base._return_type(f, typestuple(S, T, As...))
305+
_broadcast_type{T}(::Type{T}, f, A, Bs...) = Base._default_eltype(Base.Generator{ziptype(T, A, Bs...), ftype(f, A, Bs...)})
289306

290307
# broadcast methods that dispatch on the type of the final container
291308
@inline function broadcast_c(f, ::Type{Array}, A, Bs...)
292-
T = _broadcast_type(f, A, Bs...)
309+
T = _broadcast_type(Any, f, A, Bs...)
293310
shape = broadcast_indices(A, Bs...)
294311
iter = CartesianRange(shape)
295312
if isleaftype(T)
@@ -300,14 +317,21 @@ _broadcast_type(f, A, Bs...) = Base._default_eltype(Base.Generator{ziptype(A, Bs
300317
end
301318
return broadcast_t(f, Any, shape, iter, A, Bs...)
302319
end
320+
@inline function broadcast_c(f, ::Type{Array{Nullable}}, A, Bs...)
321+
@inline rec(x) = broadcast(f, x)
322+
@inline rec(x, y) = broadcast(f, x, y)
323+
@inline rec(x, y, z) = broadcast(f, x, y, z)
324+
@inline rec(xs...) = broadcast(f, xs...)
325+
broadcast_c(rec, Array, A, Bs...)
326+
end
303327
function broadcast_c(f, ::Type{Tuple}, As...)
304328
shape = broadcast_indices(As...)
305329
n = length(shape[1])
306330
return ntuple(k->f((_broadcast_getindex(A, k) for A in As)...), n)
307331
end
308332
@inline function broadcast_c(f, ::Type{Nullable}, a...)
309333
nonnull = all(hasvalue, a)
310-
S = _broadcast_type(f, a...)
334+
S = _broadcast_type(Array, f, a...)
311335
if isleaftype(S) && null_safe_eltype_op(f, a...)
312336
Nullable{S}(f(map(unsafe_get, a)...), nonnull)
313337
else
@@ -323,21 +347,28 @@ end
323347
"""
324348
broadcast(f, As...)
325349
326-
Broadcasts the arrays, tuples, `Ref`s, nullables, and/or scalars `As` to a
350+
Broadcasts the arrays, tuples, `Ref`, nullables, and/or scalars `As` to a
327351
container of the appropriate type and dimensions. In this context, anything
328-
that is not a subtype of `AbstractArray`, `Ref` (except for `Ptr`s), `Tuple`,
352+
that is not a subtype of `AbstractArray`, `Ref` (except for `Ptr`s) or `Tuple`,
329353
or `Nullable` is considered a scalar. The resulting container is established by
330354
the following rules:
331355
332356
- If all the arguments are scalars, it returns a scalar.
333357
- If the arguments are tuples and zero or more scalars, it returns a tuple.
334-
- If the arguments contain at least one array or `Ref`, it returns an array
335-
(expanding singleton dimensions), and treats `Ref`s as 0-dimensional arrays,
336-
and tuples as a 1-dimensional arrays.
358+
- If there is at least an array or a `Ref` in the arguments, it returns an array
359+
(and treats any `Ref` as a 0-dimensional array of its contents and any tuple
360+
as a 1-dimensional array) expanding singleton dimensions.
337361
338-
The following additional rule applies to `Nullable` arguments: If there is at
339-
least one `Nullable`, and all the arguments are scalars or `Nullable`, it
340-
returns a `Nullable` treating `Nullable`s as "containers".
362+
The following additional rules apply to `Nullable` arguments:
363+
364+
- If there is at least a `Nullable`, and all the arguments are scalars or
365+
`Nullable`, it returns a `Nullable`.
366+
- If there is at least an array or a `Ref` with `Nullable` entries, or there
367+
is at least an array or a `Ref` (perhaps with scalar entries instead of
368+
`Nullable` entries) and a nullable, then the result is an array of
369+
`Nullable` entries.
370+
- If there is a tuple and a nullable, the result is an error, as this case is
371+
not currently supported.
341372
342373
A special syntax exists for broadcasting: `f.(args...)` is equivalent to
343374
`broadcast(f, args...)`, and nested `f.(g.(args...))` calls are fused into a
@@ -402,8 +433,21 @@ Nullable{String}("XY")
402433
julia> broadcast(/, 1.0, Nullable(2.0))
403434
Nullable{Float64}(0.5)
404435
405-
julia> (1 + im) ./ Nullable{Int}()
406-
Nullable{Complex{Float64}}()
436+
julia> [Nullable(1), Nullable(2), Nullable()] .* 3
437+
3-element Array{Nullable{Int64},1}:
438+
3
439+
6
440+
#NULL
441+
442+
julia> [1+im, 2+2im, 3+3im] ./ Nullable{Int}()
443+
3-element Array{Nullable{Complex{Float64}},1}:
444+
#NULL
445+
#NULL
446+
#NULL
447+
448+
julia> Ref(7) .+ Nullable(3)
449+
0-dimensional Array{Nullable{Int64},0}:
450+
10
407451
```
408452
"""
409453
@inline broadcast(f, A, Bs...) = broadcast_c(f, containertype(A, Bs...), A, Bs...)

base/nullable.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ hasvalue(x) = true
302302
all(f::typeof(hasvalue), t::Tuple) = f(t[1]) & all(f, tail(t))
303303
all(f::typeof(hasvalue), t::Tuple{}) = true
304304

305+
is_nullable_array(::Any) = false
306+
is_nullable_array{T}(::Type{T}) = eltype(T) <: Nullable
307+
is_nullable_array(A::AbstractArray) = eltype(A) <: Nullable
308+
305309
# Overloads of null_safe_op
306310
# Unary operators
307311

base/sparse/higherorderfns.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function _noshapecheck_map{Tf,N}(f::Tf, A::SparseVecOrMat, Bs::Vararg{SparseVecO
7373
fofzeros = f(_zeros_eltypes(A, Bs...)...)
7474
fpreszeros = _iszero(fofzeros)
7575
maxnnzC = fpreszeros ? min(length(A), _sumnnzs(A, Bs...)) : length(A)
76-
entrytypeC = Base.Broadcast._broadcast_type(f, A, Bs...)
76+
entrytypeC = Base.Broadcast._broadcast_type(Any, f, A, Bs...)
7777
indextypeC = _promote_indtype(A, Bs...)
7878
C = _allocres(size(A), indextypeC, entrytypeC, maxnnzC)
7979
return fpreszeros ? _map_zeropres!(f, C, A, Bs...) :
@@ -101,7 +101,7 @@ function _diffshape_broadcast{Tf,N}(f::Tf, A::SparseVecOrMat, Bs::Vararg{SparseV
101101
fofzeros = f(_zeros_eltypes(A, Bs...)...)
102102
fpreszeros = _iszero(fofzeros)
103103
indextypeC = _promote_indtype(A, Bs...)
104-
entrytypeC = Base.Broadcast._broadcast_type(f, A, Bs...)
104+
entrytypeC = Base.Broadcast._broadcast_type(Any, f, A, Bs...)
105105
shapeC = to_shape(Base.Broadcast.broadcast_indices(A, Bs...))
106106
maxnnzC = fpreszeros ? _checked_maxnnzbcres(shapeC, A, Bs...) : _densennz(shapeC)
107107
C = _allocres(shapeC, indextypeC, entrytypeC, maxnnzC)

doc/src/manual/types.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,3 +1426,15 @@ conveniently using `.`-prefixed operators:
14261426
julia> Nullable(2) ./ Nullable(3) .+ Nullable(1.0)
14271427
Nullable{Float64}(1.66667)
14281428
```
1429+
1430+
[`broadcast()`](@ref) also allows one to work with multiple data at the same
1431+
time, without manually writing for loops. This enables performing the same
1432+
operation to arrays where the data is possibly missing; for example
1433+
1434+
```julia
1435+
julia> [Nullable(2), Nullable(), Nullable(3)] .+ 3
1436+
3-element Array{Nullable{Int64},1}:
1437+
5
1438+
#NULL
1439+
6
1440+
```

test/broadcast.jl

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ StrangeType18623(x,y) = (x,y)
363363
let
364364
f(A, n) = broadcast(x -> +(x, n), A)
365365
@test @inferred(f([1.0], 1)) == [2.0]
366-
g() = (a = 1; Base.Broadcast._broadcast_type(x -> x + a, 1.0))
366+
g() = (a = 1; Base.Broadcast._broadcast_type(Any, x -> x + a, 1.0))
367367
@test @inferred(g()) === Float64
368368
end
369369

@@ -411,10 +411,3 @@ Base.Broadcast.broadcast_c(f, ::Type{Array19745}, A, Bs...) =
411411
@test isa(aa .+ 1, Array19745)
412412
@test isa(aa .* aa', Array19745)
413413
end
414-
415-
# broadcast should only "peel off" one container layer
416-
@test get.([Nullable(1), Nullable(2)]) == [1, 2]
417-
let io = IOBuffer()
418-
broadcast(x -> print(io, x), [Nullable(1.0)])
419-
@test String(take!(io)) == "Nullable{Float64}(1.0)"
420-
end

test/nullable.jl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,48 @@ end
507507
@test Nullable(10.5) ===
508508
@inferred(broadcast(+, 1, 2, Nullable(3), Nullable(4.0), Nullable(1//2)))
509509

510+
# broadcasting for arrays
511+
@test istypeequal(@inferred(broadcast(+, [1, 2, 3], Nullable{Int}(1))),
512+
Nullable{Int}[2, 3, 4])
513+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[1, 2, 3], 1)),
514+
Nullable{Int}[2, 3, 4])
515+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[1, 2, 3], Nullable(1))),
516+
Nullable{Int}[2, 3, 4])
517+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[1, Nullable()], Nullable(1))),
518+
Nullable{Int}[2, Nullable()])
519+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[Nullable(), 1],
520+
Nullable{Int}())),
521+
Nullable{Int}[Nullable(), Nullable()])
522+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[Nullable(), 1],
523+
Nullable{Int}[1, Nullable()])),
524+
Nullable{Int}[Nullable(), Nullable()])
525+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[Nullable(), 1],
526+
Nullable{Int}[Nullable(), 1])),
527+
Nullable{Int}[Nullable(), 2])
528+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[Nullable(), Nullable()],
529+
Nullable{Int}[1, 2])),
530+
Nullable{Int}[Nullable(), Nullable()])
531+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[Nullable(), 1],
532+
Nullable{Int}[1])),
533+
Nullable{Int}[Nullable(), 2])
534+
@test istypeequal(@inferred(broadcast(+, Nullable{Float64}[1.0, 2.0],
535+
Nullable{Float64}[1.0 2.0; 3.0 4.0])),
536+
Nullable{Float64}[2.0 3.0; 5.0 6.0])
537+
@test istypeequal(@inferred(broadcast(+, Nullable{Int}[1, 2], [1, 2], 1)),
538+
Nullable{Int}[3, 5])
539+
540+
@test istypeequal(@inferred(broadcast(/, 1, Nullable{Int}[1, 2, 4])),
541+
Nullable{Float64}[1.0, 0.5, 0.25])
542+
@test istypeequal(@inferred(broadcast(muladd, Nullable(2), 42,
543+
[Nullable(1337), Nullable{Int}()])),
544+
Nullable{Int}[1421, Nullable()])
545+
546+
# heterogenous types (not inferrable)
547+
@test istypeequal(broadcast(+, Any[1, 1.0], Nullable(1//2)),
548+
Any[Nullable(3//2), Nullable(1.5)])
549+
@test istypeequal(broadcast(+, Any[Nullable(1) Nullable(1.0)], Nullable(big"1")),
550+
Any[Nullable(big"2") Nullable(big"2.0")])
551+
510552
# test fast path taken
511553
for op in (+, *, -)
512554
for b1 in (false, true)
@@ -515,6 +557,11 @@ for op in (+, *, -)
515557
@inferred(broadcast(op, Nullable{Int}(1, b1),
516558
Nullable{Int}(2, b2)))
517559
end
560+
A = [1, 2, 3]
561+
res = @inferred(broadcast(op, A, Nullable{Int}(1, b1)))
562+
@test res[1] === Nullable{Int}(op(1, 1), b1)
563+
@test res[2] === Nullable{Int}(op(2, 1), b1)
564+
@test res[3] === Nullable{Int}(op(3, 1), b1)
518565
end
519566
end
520567

0 commit comments

Comments
 (0)