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

Define basic FillArrays types in Base #39184

Closed
wants to merge 10 commits into from
2 changes: 2 additions & 0 deletions base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ include("multidimensional.jl")
include("permuteddimsarray.jl")
using .PermutedDimsArrays

include("fillarrays.jl")

include("broadcast.jl")
using .Broadcast
using .Broadcast: broadcasted, broadcasted_kwsyntax, materialize, materialize!
Expand Down
44 changes: 39 additions & 5 deletions base/broadcast.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ module Broadcast

using .Base.Cartesian
using .Base: Indices, OneTo, tail, to_shape, isoperator, promote_typejoin, @pure,
_msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache, unalias
_msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache, unalias,
AbstractFill, Fill, getindex_value
import .Base: copy, copyto!, axes
export broadcast, broadcast!, BroadcastStyle, broadcast_axes, broadcastable, dotview, @__dot__, broadcast_preserving_zero_d, BroadcastFunction, andand, oror

Expand Down Expand Up @@ -691,14 +692,16 @@ julia> Broadcast.broadcastable([1,2,3]) # like `identity` since arrays already s
3

julia> Broadcast.broadcastable(Int) # Types don't support axes, indexing, or iteration but are commonly used as scalars
Base.RefValue{Type{Int64}}(Int64)
0-dimensional Fill{Type{Int64}, 0, Tuple{}}:
Int64

julia> Broadcast.broadcastable("hello") # Strings break convention of matching iteration and act like a scalar instead
Base.RefValue{String}("hello")
0-dimensional Fill{String, 0, Tuple{}}:
"hello"
```
"""
broadcastable(x::Union{Symbol,AbstractString,Function,UndefInitializer,Nothing,RoundingMode,Missing,Val,Ptr,AbstractPattern,Pair}) = Ref(x)
broadcastable(::Type{T}) where {T} = Ref{Type{T}}(T)
broadcastable(x::Union{Symbol,AbstractString,Function,UndefInitializer,Nothing,RoundingMode,Missing,Val,Ptr,AbstractPattern,Pair}) = Fill(x)
broadcastable(::Type{T}) where {T} = Fill{Type{T}}(T)
broadcastable(x::Union{AbstractArray,Number,Ref,Tuple,Broadcasted}) = x
# Default to collecting iterables — which will error for non-iterables
broadcastable(x) = collect(x)
Expand Down Expand Up @@ -1377,4 +1380,35 @@ function Base.show(io::IO, op::BroadcastFunction)
end
Base.show(io::IO, ::MIME"text/plain", op::BroadcastFunction) = show(io, op)


# Fill Broadcasting
function broadcasted(::DefaultArrayStyle{N}, op, r::AbstractFill{T,N}) where {T,N}
return Fill(op(getindex_value(r)), axes(r))
end

function broadcasted(::DefaultArrayStyle, op, a::AbstractFill, b::AbstractFill)
val = op(getindex_value(a), getindex_value(b))
return Fill(val, broadcast_shape(axes(a), axes(b)))
end

# Need to prevent array-valued fills from broadcasting over entry
_broadcast_getindex_value(a::AbstractFill{<:Number}) = getindex_value(a)
_broadcast_getindex_value(a::AbstractFill) = Ref(getindex_value(a))

function broadcasted(::DefaultArrayStyle{1}, ::typeof(*), a::AbstractFill, b::AbstractRange)
broadcast_shape(axes(a), axes(b)) == axes(b) || throw(ArgumentError("Cannot broadcast $a and $b. Convert $b to a Vector first."))
return broadcasted(*, _broadcast_getindex_value(a), b)
end

function broadcasted(::DefaultArrayStyle{1}, ::typeof(*), a::AbstractRange, b::AbstractFill)
broadcast_shape(axes(a), axes(b)) == axes(a) || throw(ArgumentError("Cannot broadcast $a and $b. Convert $b to a Vector first."))
Copy link
Contributor

Choose a reason for hiding this comment

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

This was carried over from FillArrays.jl but I think was a temporary restriction. I'm not entirely sure this restriction should be kept.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should I just delete that line altogether?

Copy link
Contributor

Choose a reason for hiding this comment

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

The check is necessary because the function may be incorrect in the "degenerate" case where a is length 1 and so the shape comes from b, not a. Perhaps it would be better to be rewritten as

if length(a) == 1 
   broadcasted(*, a[1], b)
else
   broadcasted(*, a, _broadcast_getindex_value(b))
end

but this would not be type stable.

In any case some thought should be put into this.

return broadcasted(*, a, _broadcast_getindex_value(b))
end

broadcasted(::DefaultArrayStyle{N}, op, r::AbstractFill{T,N}, x::Number) where {T,N} = Fill(op(getindex_value(r),x), axes(r))
broadcasted(::DefaultArrayStyle{N}, op, x::Number, r::AbstractFill{T,N}) where {T,N} = Fill(op(x, getindex_value(r)), axes(r))
broadcasted(::DefaultArrayStyle{N}, op, r::AbstractFill{T,N}, x::Ref) where {T,N} = Fill(op(getindex_value(r),x[]), axes(r))
broadcasted(::DefaultArrayStyle{N}, op, x::Ref, r::AbstractFill{T,N}) where {T,N} = Fill(op(x[], getindex_value(r)), axes(r))


end # module
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export
Dims,
Enum,
ExponentialBackOff,
Fill,
IndexCartesian,
IndexLinear,
IndexStyle,
Expand Down
134 changes: 134 additions & 0 deletions base/fillarrays.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@

# Basic structure of a FillArray, the last version of fill.
# These definitions are *intended* to be pirated and extended by FillArrays.jl (which originally defined them)

"""
AbstractFill{T, N, Axes} <: AbstractArray{T, N}
Supertype for lazy array types whose entries are all equal to constant.

For more `Fill` functionality, use the FillArrays.jl package.
"""
abstract type AbstractFill{T, N, Axes} <: AbstractArray{T, N} end

==(a::AbstractFill, b::AbstractFill) = axes(a) == axes(b) && getindex_value(a) == getindex_value(b)

@inline function getindex(F::AbstractFill, k::Integer)
@boundscheck checkbounds(F, k)
getindex_value(F)
end

@inline function getindex(F::AbstractFill{T, N}, kj::Vararg{<:Integer, N}) where {T, N}
@boundscheck checkbounds(F, kj...)
getindex_value(F)
end

@inline function setindex!(F::AbstractFill, v, k::Integer)
@boundscheck checkbounds(F, k)
v == getindex_value(F) || throw(ArgumentError("Cannot setindex! to $v for an AbstractFill with value $(getindex_value(F))."))
F
end

@inline function setindex!(F::AbstractFill{T, N}, v, kj::Vararg{<:Integer, N}) where {T, N}
@boundscheck checkbounds(F, kj...)
v == getindex_value(F) || throw(ArgumentError("Cannot setindex! to $v for an AbstractFill with value $(getindex_value(F))."))
F
end

@inline function fill!(F::AbstractFill, v)
v == getindex_value(F) || throw(ArgumentError("Cannot fill! with $v an AbstractFill with value $(getindex_value(F))."))
F
end

IndexStyle(::Type{<:AbstractFill{<:Any,N,<:NTuple{N,Base.OneTo{Int}}}}) where N = IndexLinear()


"""
Fill{T, N, Axes}
where `Axes <: Tuple{Vararg{AbstractUnitRange,N}}`
A lazy representation of an array of dimension `N`
whose entries are all equal to a constant of type `T`,
with axes of type `Axes`.
Typically created by `Fill` or `Zeros` or `Ones`
# Examples
```jldoctest
julia> Fill(1)
0-dimensional Fill{Int64, 0, Tuple{}}:
1

julia> Fill(7, (2,3))
2×3 Fill{Int64,2,Tuple{Base.OneTo{Int64},Base.OneTo{Int64}}}:
7 7 7
7 7 7

julia> Fill{Float64, 1, Tuple{UnitRange{Int64}}}(7., (1:2,))
2-element Fill{Float64,1,Tuple{UnitRange{Int64}}} with indices 1:2:
7.0
7.0
```

For more `Fill` functionality and efficient routines, use the FillArrays.jl package.
"""
struct Fill{T, N, Axes} <: AbstractFill{T, N, Axes}
value::T
axes::Axes

Fill{T,N,Axes}(x::T, sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
new{T,N,Axes}(x,sz)
Fill{T,0,Tuple{}}(x::T, sz::Tuple{}) where T = new{T,0,Tuple{}}(x, sz)
end

Fill{T,N,Axes}(x, sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
Fill{T,N,Axes}(convert(T, x)::T, sz)

Fill{T,0}(x::T, ::Tuple{}) where T = Fill{T,0,Tuple{}}(x, ()) # ambiguity fix

@inline Fill{T, N}(x::T, sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
Fill{T,N,Axes}(x, sz)
@inline Fill{T, N}(x, sz::Axes) where Axes<:Tuple{Vararg{AbstractUnitRange,N}} where {T, N} =
Fill{T,N}(convert(T, x)::T, sz)

@inline Fill{T, N}(x, sz::SZ) where SZ<:Tuple{Vararg{Integer,N}} where {T, N} =
Fill{T,N}(x, Base.OneTo.(sz))
@inline Fill{T, N}(x, sz::Vararg{Integer, N}) where {T, N} = Fill{T,N}(convert(T, x)::T, sz)


@inline Fill{T}(x, sz::Vararg{<:Integer,N}) where {T, N} = Fill{T, N}(x, sz)
@inline Fill{T}(x, sz::Tuple{Vararg{<:Any,N}}) where {T, N} = Fill{T, N}(x, sz)
""" `Fill(x, dims...)` construct lazy version of `fill(x, dims...)` """
@inline Fill(x::T, sz::Vararg{<:Integer,N}) where {T, N} = Fill{T, N}(x, sz)
""" `Fill(x, dims)` construct lazy version of `fill(x, dims)` """
@inline Fill(x::T, sz::Tuple{Vararg{<:Any,N}}) where {T, N} = Fill{T, N}(x, sz)

# We restrict to when T is specified to avoid ambiguity with a Fill of a Fill
@inline Fill{T}(F::Fill{T}) where T = F
@inline Fill{T,N}(F::Fill{T,N}) where {T,N} = F
@inline Fill{T,N,Axes}(F::Fill{T,N,Axes}) where {T,N,Axes} = F

@inline axes(F::Fill) = F.axes
@inline size(F::Fill) = length.(F.axes)

AbstractArray{T}(F::Fill{T}) where T = F
AbstractArray{T,N}(F::Fill{T,N}) where {T,N} = F
AbstractArray{T}(F::Fill{V,N}) where {T,V,N} = Fill{T}(convert(T, F.value)::T, F.axes)
AbstractArray{T,N}(F::Fill{V,N}) where {T,V,N} = Fill{T}(convert(T, F.value)::T, F.axes)

convert(::Type{AbstractArray{T}}, F::Fill{T}) where T = F
convert(::Type{AbstractArray{T,N}}, F::Fill{T,N}) where {T,N} = F
convert(::Type{AbstractArray{T}}, F::Fill) where {T} = AbstractArray{T}(F)
convert(::Type{AbstractArray{T,N}}, F::Fill) where {T,N} = AbstractArray{T,N}(F)
convert(::Type{AbstractFill}, F::AbstractFill) = F
convert(::Type{AbstractFill{T}}, F::AbstractFill) where T = convert(AbstractArray{T}, F)
convert(::Type{AbstractFill{T,N}}, F::AbstractFill) where {T,N} = convert(AbstractArray{T,N}, F)

copy(F::Fill) = Fill(F.value, F.axes)

getindex(F::Fill{<:Any,0}) = getindex_value(F)

@inline getindex_value(F::Fill) = F.value
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this? Why not just define this in getindex and use F[] everywhere?

Copy link
Contributor

Choose a reason for hiding this comment

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

That seems inconsistent to me unless you meant something else:

julia> F = fill(1,3)
3-element Vector{Int64}:
 1
 1
 1

julia> F[]
ERROR: BoundsError: attempt to access 3-element Vector{Int64} at index []
Stacktrace:
 [1] throw_boundserror(A::Vector{Int64}, I::Tuple{})
   @ Base ./abstractarray.jl:651
 [2] checkbounds
   @ ./abstractarray.jl:616 [inlined]
 [3] _getindex
   @ ./abstractarray.jl:1196 [inlined]
 [4] getindex(::Vector{Int64})
   @ Base ./abstractarray.jl:1170
 [5] top-level scope
   @ REPL[20]:1

Copy link
Member

Choose a reason for hiding this comment

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

By F[] I just meant indexing in general. Seems like an unnecessary indirection/complexity.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's to allow for unified code for Fill, Zeros, and Ones. Using indexing doesn't work here as one would have to add artificial indices.


map(f::Function, r::AbstractFill) = Fill(f(getindex_value(r)), axes(r))

iterate(f::AbstractFill{<:Any, 0}) = (getindex_value(f), nothing)
iterate( ::AbstractFill{<:Any, 0}, s) = nothing

getindex(f::AbstractFill{<:Any, 0}, ::CartesianIndex{0}) = getindex_value(f)
9 changes: 3 additions & 6 deletions doc/src/manual/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -946,13 +946,10 @@ julia> string.(1:3, ". ", ["First", "Second", "Third"])
```

Sometimes, you want a container (like an array) that would normally participate in broadcast to be "protected"
from broadcast's behavior of iterating over all of its elements. By placing it inside another container
(like a single element [`Tuple`](@ref)) broadcast will treat it as a single value.
from broadcast's behavior of iterating over all of its elements. By placing it inside another container broadcast
will treat it as a single value.
```jldoctest
julia> ([1, 2, 3], [4, 5, 6]) .+ ([1, 2, 3],)
([2, 4, 6], [5, 7, 9])

julia> ([1, 2, 3], [4, 5, 6]) .+ tuple([1, 2, 3])
julia> ([1, 2, 3], [4, 5, 6]) .+ Fill([1, 2, 3])
([2, 4, 6], [5, 7, 9])
```

Expand Down
2 changes: 1 addition & 1 deletion test/choosetests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function choosetests(choices = [])
"boundscheck", "error", "ambiguous", "cartesian", "osutils",
"channels", "iostream", "secretbuffer", "specificity",
"reinterpretarray", "syntax", "corelogging", "missing", "asyncmap",
"smallarrayshrink", "opaque_closure", "filesystem", "download"
"smallarrayshrink", "fillarrays", "opaque_closure", "filesystem", "download"
]

tests = []
Expand Down
135 changes: 135 additions & 0 deletions test/fillarrays.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
@testset "fill array constructors and convert" begin

@test Fill(1) ≡ Fill{Int}(1) ≡ Fill{Int,0}(1) ≡ Fill{Int,0,Tuple{}}(1,())
@test Fill(1,(-1,5)) ≡ Fill(1,(0,5))
@test Fill(1.0,5) isa AbstractVector{Float64}
@test Fill(1.0,5,5) isa AbstractMatrix{Float64}
@test Fill(1,5) ≡ Fill(1,(5,))
@test Fill(1,5,5) ≡ Fill(1,(5,5))
@test eltype(Fill(1.0,5,5)) == Float64

@test_throws InexactError Matrix{Float64}(Fill(1.0+1.0im,10,10))

for T in (Int, Float64)
F = Fill{T}(one(T), 5)

@test eltype(F) == T
@test Array(F) == fill(one(T),5)
@test Array{T}(F) == fill(one(T),5)
@test Array{T,1}(F) == fill(one(T),5)

F = Fill{T}(one(T), 5, 5)
@test eltype(F) == T
@test Array(F) == fill(one(T),5,5)
@test Array{T}(F) == fill(one(T),5,5)
@test Array{T,2}(F) == fill(one(T),5,5)

@test convert(AbstractArray,F) ≡ F
@test convert(AbstractArray{T},F) ≡ AbstractArray{T}(F) ≡ F
@test convert(AbstractMatrix{T},F) ≡ AbstractMatrix{T}(F) ≡ F

@test convert(AbstractArray{Float32},F) ≡ AbstractArray{Float32}(F) ≡
Fill{Float32}(one(Float32),5,5)
@test convert(AbstractMatrix{Float32},F) ≡ AbstractMatrix{Float32}(F) ≡
Fill{Float32}(one(Float32),5,5)

@test Fill{T}(F) ≡ Fill{T,2}(F) ≡ typeof(F)(F) ≡ F
end

@testset "copy should return Fill" begin
x = Fill(1.0,10)
@test copy(x) ≡ x
end
end

# @testset "indexing" begin
# A = Fill(3.0,5)
# @test A[1:3] ≡ Fill(3.0,3)
# @test A[1:3,1:1] ≡ Fill(3.0,3,1)
# @test_throws BoundsError A[1:3,2]
# @test_throws BoundsError A[1:26]
# @test A[[true, false, true, false, false]] ≡ Fill(3.0, 2)
# A = Fill(3.0, 2, 2)
# @test A[[true true; true false]] ≡ Fill(3.0, 3)
# @test_throws DimensionMismatch A[[true, false]]

# @testset "colon" begin
# @test Fill(3.0,2)[:] ≡ Fill(3.0,2)[Base.Slice(Base.OneTo(2))] ≡ Fill(3.0,2)

# @test Fill(3.0,2,2)[:,:] ≡ Fill(3.0,2,2)[Base.Slice(Base.OneTo(2)),Base.Slice(Base.OneTo(2))] ≡ Fill(3.0,2,2)
# end

# @testset "mixed integer / vector /colon" begin
# a = Fill(2.0,5)
# z = Zeros(5)
# @test a[1:5] ≡ a[:] ≡ a
# @test z[1:5] ≡ z[:] ≡ z

# A = Fill(2.0,5,6)
# Z = Zeros(5,6)
# @test A[:,1] ≡ A[1:5,1] ≡ Fill(2.0,5)
# @test A[1,:] ≡ A[1,1:6] ≡ Fill(2.0,6)
# @test A[:,:] ≡ A[1:5,1:6] ≡ A[1:5,:] ≡ A[:,1:6] ≡ A
# @test Z[:,1] ≡ Z[1:5,1] ≡ Zeros(5)
# @test Z[1,:] ≡ Z[1,1:6] ≡ Zeros(6)
# @test Z[:,:] ≡ Z[1:5,1:6] ≡ Z[1:5,:] ≡ Z[:,1:6] ≡ Z
# A = Fill(2.0,5,6,7)
# Z = Zeros(5,6,7)
# @test A[:,1,1] ≡ A[1:5,1,1] ≡ Fill(2.0,5)
# @test A[1,:,1] ≡ A[1,1:6,1] ≡ Fill(2.0,6)
# @test A[:,:,:] ≡ A[1:5,1:6,1:7] ≡ A[1:5,:,1:7] ≡ A[:,1:6,1:7] ≡ A
# end
# end


@testset "Broadcast" begin
x = Fill(5,5)
@test (.+)(x) ≡ x
@test (.-)(x) ≡ -x
@test exp.(x) ≡ Fill(exp(5),5)
@test x .+ 1 ≡ Fill(6,5)
@test 1 .+ x ≡ Fill(6,5)
@test x .+ x ≡ Fill(10,5)
f = (x,y) -> cos(x*y)

@testset "support Ref" begin
@test Fill(1,10) .- 1 ≡ Fill(1,10) .- Ref(1) ≡ Fill(1,10) .- Ref(1I)
@test Fill([1 2; 3 4],10) .- Ref(1I) == Fill([0 2; 3 3],10)
@test Ref(1I) .+ Fill([1 2; 3 4],10) == Fill([2 2; 3 5],10)
end
end

@testset "map" begin
x = Fill(2,5,3)
@test map(exp,x) === Fill(exp(2),5,3)
end

@testset "0-dimensional" begin
A = Fill{Int,0,Tuple{}}(3, ())

@test A[] ≡ A[1] ≡ 3
@test A ≡ Fill{Int,0}(3, ()) ≡ Fill(3, ()) ≡ Fill(3)
@test size(A) == ()
@test axes(A) == ()
end

@testset "setindex!/fill!" begin
F = Fill(1,10)
@test (F[1] = 1) == 1
@test_throws BoundsError (F[11] = 1)
@test_throws ArgumentError (F[10] = 2)

F = Fill(1,10,5)
@test (F[1] = 1) == 1
@test (F[3,3] = 1) == 1
@test_throws BoundsError (F[51] = 1)
@test_throws BoundsError (F[1,6] = 1)
@test_throws ArgumentError (F[10] = 2)
@test_throws ArgumentError (F[10,1] = 2)

@test (F[:,1] .= 1) == fill(1,10)
@test_throws ArgumentError (F[:,1] .= 2)

@test fill!(F,1) == F
@test_throws ArgumentError fill!(F,2)
end