Skip to content

Commit f147aaa

Browse files
pabloferztkelman
authored andcommitted
Limit broadcast mechanism over Nullables (#19787)
* Limit broadcast mechanism over Nullables * Test that broadcast doesn't propagate too much * Update manual section on Nullables * Address TotalVerb comments * Address Sacha0 comments
1 parent b2ae5a5 commit f147aaa

File tree

7 files changed

+43
-145
lines changed

7 files changed

+43
-145
lines changed

NEWS.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,10 @@ 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+
5859
* `broadcast` now handles missing data (`Nullable`s) allowing operations to
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)`.
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]).
6562

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

base/broadcast.jl

Lines changed: 29 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ 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, is_nullable_array
8+
nullable_returntype, null_safe_eltype_op, hasvalue
99
import Base: broadcast, broadcast!
1010
export broadcast_getindex, broadcast_setindex!, dotview
1111

12+
typealias ScalarType Union{Type{Any}, Type{Nullable}}
13+
1214
## Broadcasting utilities ##
1315
# fallbacks for some special cases
1416
@inline broadcast(f, x::Number...) = f(x...)
@@ -28,37 +30,28 @@ containertype(::Type) = Any
2830
containertype{T<:Ptr}(::Type{T}) = Any
2931
containertype{T<:Tuple}(::Type{T}) = Tuple
3032
containertype{T<:Ref}(::Type{T}) = Array
31-
containertype{T<:AbstractArray}(::Type{T}) =
32-
is_nullable_array(T) ? Array{Nullable} : Array
33+
containertype{T<:AbstractArray}(::Type{T}) = Array
3334
containertype{T<:Nullable}(::Type{T}) = Nullable
3435
containertype(ct1, ct2) = promote_containertype(containertype(ct1), containertype(ct2))
3536
@inline containertype(ct1, ct2, cts...) = promote_containertype(containertype(ct1), containertype(ct2, cts...))
3637

3738
promote_containertype(::Type{Array}, ::Type{Array}) = Array
3839
promote_containertype(::Type{Array}, ct) = Array
3940
promote_containertype(ct, ::Type{Array}) = Array
40-
promote_containertype(::Type{Tuple}, ::Type{Any}) = Tuple
41-
promote_containertype(::Type{Any}, ::Type{Tuple}) = Tuple
41+
promote_containertype(::Type{Tuple}, ::ScalarType) = Tuple
42+
promote_containertype(::ScalarType, ::Type{Tuple}) = Tuple
4243
promote_containertype(::Type{Any}, ::Type{Nullable}) = Nullable
4344
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}
5245
promote_containertype{T}(::Type{T}, ::Type{T}) = T
5346

5447
## Calculate the broadcast indices of the arguments, or error if incompatible
5548
# array inputs
5649
broadcast_indices() = ()
5750
broadcast_indices(A) = broadcast_indices(containertype(A), A)
58-
broadcast_indices(::Union{Type{Any}, Type{Nullable}}, A) = ()
51+
broadcast_indices(::ScalarType, A) = ()
5952
broadcast_indices(::Type{Tuple}, A) = (OneTo(length(A)),)
6053
broadcast_indices(::Type{Array}, A::Ref) = ()
61-
broadcast_indices{T<:Array}(::Type{T}, A) = indices(A)
54+
broadcast_indices(::Type{Array}, A) = indices(A)
6255
@inline broadcast_indices(A, B...) = broadcast_shape((), broadcast_indices(A), map(broadcast_indices, B)...)
6356
# shape (i.e., tuple-of-indices) inputs
6457
broadcast_shape(shape::Tuple) = shape
@@ -132,9 +125,7 @@ end
132125

133126
Base.@propagate_inbounds _broadcast_getindex(A, I) = _broadcast_getindex(containertype(A), A, I)
134127
Base.@propagate_inbounds _broadcast_getindex(::Type{Array}, A::Ref, 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
128+
Base.@propagate_inbounds _broadcast_getindex(::ScalarType, A, I) = A
138129
Base.@propagate_inbounds _broadcast_getindex(::Any, A, I) = A[I]
139130

140131
## Broadcasting core
@@ -285,28 +276,20 @@ ftype(f, A) = typeof(f)
285276
ftype(f, A...) = typeof(a -> f(a...))
286277
ftype(T::Type, A...) = Type{T}
287278

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

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

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

307290
# broadcast methods that dispatch on the type of the final container
308291
@inline function broadcast_c(f, ::Type{Array}, A, Bs...)
309-
T = _broadcast_type(Any, f, A, Bs...)
292+
T = _broadcast_type(f, A, Bs...)
310293
shape = broadcast_indices(A, Bs...)
311294
iter = CartesianRange(shape)
312295
if isleaftype(T)
@@ -317,21 +300,14 @@ _broadcast_type{T}(::Type{T}, f, A, Bs...) = Base._default_eltype(Base.Generator
317300
end
318301
return broadcast_t(f, Any, shape, iter, A, Bs...)
319302
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
327303
function broadcast_c(f, ::Type{Tuple}, As...)
328304
shape = broadcast_indices(As...)
329305
n = length(shape[1])
330306
return ntuple(k->f((_broadcast_getindex(A, k) for A in As)...), n)
331307
end
332308
@inline function broadcast_c(f, ::Type{Nullable}, a...)
333309
nonnull = all(hasvalue, a)
334-
S = _broadcast_type(Array, f, a...)
310+
S = _broadcast_type(f, a...)
335311
if isleaftype(S) && null_safe_eltype_op(f, a...)
336312
Nullable{S}(f(map(unsafe_get, a)...), nonnull)
337313
else
@@ -347,28 +323,21 @@ end
347323
"""
348324
broadcast(f, As...)
349325
350-
Broadcasts the arrays, tuples, `Ref`, nullables, and/or scalars `As` to a
326+
Broadcasts the arrays, tuples, `Ref`s, nullables, and/or scalars `As` to a
351327
container of the appropriate type and dimensions. In this context, anything
352-
that is not a subtype of `AbstractArray`, `Ref` (except for `Ptr`s) or `Tuple`,
328+
that is not a subtype of `AbstractArray`, `Ref` (except for `Ptr`s), `Tuple`,
353329
or `Nullable` is considered a scalar. The resulting container is established by
354330
the following rules:
355331
356332
- If all the arguments are scalars, it returns a scalar.
357333
- If the arguments are tuples and zero or more scalars, it returns a tuple.
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.
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.
361337
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.
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".
372341
373342
A special syntax exists for broadcasting: `f.(args...)` is equivalent to
374343
`broadcast(f, args...)`, and nested `f.(g.(args...))` calls are fused into a
@@ -433,21 +402,8 @@ Nullable{String}("XY")
433402
julia> broadcast(/, 1.0, Nullable(2.0))
434403
Nullable{Float64}(0.5)
435404
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
405+
julia> (1 + im) ./ Nullable{Int}()
406+
Nullable{Complex{Float64}}()
451407
```
452408
"""
453409
@inline broadcast(f, A, Bs...) = broadcast_c(f, containertype(A, Bs...), A, Bs...)

base/nullable.jl

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,6 @@ 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-
309305
# Overloads of null_safe_op
310306
# Unary operators
311307

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(Any, f, A, Bs...)
76+
entrytypeC = Base.Broadcast._broadcast_type(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(Any, f, A, Bs...)
104+
entrytypeC = Base.Broadcast._broadcast_type(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: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,15 +1426,3 @@ 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: 8 additions & 1 deletion
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(Any, x -> x + a, 1.0))
366+
g() = (a = 1; Base.Broadcast._broadcast_type(x -> x + a, 1.0))
367367
@test @inferred(g()) === Float64
368368
end
369369

@@ -411,3 +411,10 @@ 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: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -507,48 +507,6 @@ 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-
552510
# test fast path taken
553511
for op in (+, *, -)
554512
for b1 in (false, true)
@@ -557,11 +515,6 @@ for op in (+, *, -)
557515
@inferred(broadcast(op, Nullable{Int}(1, b1),
558516
Nullable{Int}(2, b2)))
559517
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)
565518
end
566519
end
567520

0 commit comments

Comments
 (0)