Fully lost with custom broadcasting interface

I am trying to implement custom broadcasting for a type, and after spending a few hours with the doc I am well and truly lost.

My type looks like this:

struct XYZ{T}
    data::Matrix{Union{T,Nothing}}
    x::Number
    y::Number
end

I am mostly accessing elements in data using Cartesian indexing, but the very important point is that I would like the values with nothing to be omitted (it’s not a sparse array because 99% of the time the degree of sparsity makes them less efficient, and because T can be an arbitrary type).

I have tried to define

Base.size(R::XYZ) = size(R.data)
Base.length(R::XYZ) = length(R.data)
Base.getindex(R::XYZ{T}, inds::Vararg{Int,2}) where {T} = R.data[inds...]
Base.setindex!(R::XYZ{T}, val, inds::Vararg{Int,2}) where {T} = R.data[inds...] = val
Base.broadcastable(R::XYZ) = R
Base.BroadcastStyle(::Type{XYZ}) = Broadcast.Style{XYZ}()
Base.axes(R::XYZ) = tuple(findall(!isnothing, R.data))

and then

function Base.similar(bc::Broadcast.Broadcasted{Broadcast.Style{XYZ}}, ::Type{ElType}) where ElType
    A = find_entry(bc)
    XYZ(similar(Array{Union{Nothing,ElType}}, size(bc)), A.x, A.y)
end

where find_entry is inspired by the docs:

find_entry(bc::Base.Broadcast.Broadcasted) = find_entry(bc.args)
find_entry(args::Tuple) = find_entry(find_entry(args[1]), Base.tail(args))
find_entry(x) = x
find_entry(::Tuple{}) = nothing
find_entry(r::XYZ, rest) = r
find_entry(::Any, rest) = find_entry(rest)

But then I get the following error message:

julia> x .+ 1
ERROR: ArgumentError: broadcasting requires an assigned BroadcastStyle
Stacktrace:
 [1] copy(bc::Base.Broadcast.Broadcasted{Base.Broadcast.Unknown, Tuple{Vector{CartesianIndex{2}}}, typeof(+), Tuple{XYZ{Int64}, Int64}})
   @ Base.Broadcast ./broadcast.jl:899
 [2] materialize(bc::Base.Broadcast.Broadcasted{Base.Broadcast.Unknown, Nothing, typeof(+), Tuple{XYZ{Int64}, Int64}})
   @ Base.Broadcast ./broadcast.jl:883
 [3] top-level scope
   @ REPL[146]:1

There’s probably something obvious I am missing, but I’m 100% lost on this one…

The following modification works for me

struct XYZ{T}
    data::Matrix{Union{T,Nothing}}
    x::Number
    y::Number
end

Base.size(R::XYZ) = size(R.data)
Base.length(R::XYZ) = length(R.data)
Base.getindex(R::XYZ{T}, inds::Vararg{Int,2}) where {T} = R.data[inds...]
Base.setindex!(R::XYZ{T}, val, inds::Vararg{Int,2}) where {T} = R.data[inds...] = val
Base.broadcastable(R::XYZ) = R
Base.BroadcastStyle(::Type{XYZ}) = Broadcast.Style{XYZ}()
Base.axes(R::XYZ) = tuple(findall(!isnothing, R.data))

function Base.similar(bc::Broadcast.Broadcasted{Broadcast.Style{XYZ}}, ::Type{ElType}) where ElType
    A = find_entry(bc)
    XYZ(similar(Array{Union{Nothing,ElType}}, size(bc)), A.x, A.y)
end

x = XYZ{Float64}(ones(1, 1), 1, 1)
println(x.data .+ 1)

Is this what you intended?

I was looking for something like x .+ 1 (I got this one to work), or even x .+= 2.0 (which I am still struggling with)

if you want to broadcast like an array, you better make it that way:

julia> struct XYZ{T} <: AbstractArray{T, 2}
           data::Matrix{Union{T,Nothing}}
           x::Number
           y::Number
       end

julia> Base.BroadcastStyle(::Type{<:XYZ}) = Broadcast.ArrayStyle{XYZ}()

julia> Base.size(A::XYZ) = size(A.data)

julia> Base.length(A::XYZ) = length(A.data)

julia> Base.getindex(A::XYZ{T}, inds::Vararg{Int,N}) where {T,N} = A.data[inds...]

julia> Base.setindex!(R::XYZ{T}, val, inds::Vararg{Int,2}) where {T} = R.data[inds...] = val

julia> function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{XYZ}}, ::Type{ElType}) where ElType
           A = find_entry(bc)
           XYZ(similar(Array{Union{Nothing,ElType}}, size(bc)), A.x, A.y)
       end

julia> find_entry(x) = x
julia> find_entry(r::XYZ, rest) = r
julia> find_entry(bc::Base.Broadcast.Broadcasted) = find_entry(bc.args)
julia> find_entry(args::Tuple) = find_entry(find_entry(args[1]), Base.tail(args))

julia> XYZ(Matrix{Union{Float64, Nothing}}(ones(2,2)), 1, 2) .+ 1
2×2 XYZ{Float64}:
 2.0  2.0
 2.0  2.0
4 Likes