StructArrays + custom broadcasting + Makie

I’d like to customize broadcasting for a type, such that Julia will allocate the result using a StructArray from the StructArrays.jl package instead of a plain array. This is actually very easy to do:

julia> struct Foo{Ta,Tb,Tc}
         a::Ta
         b::Tb
         c::Tc
       end

julia> using StructArrays: StructArray

julia> function Base.similar(bc::Broadcast.Broadcasted{Broadcast.DefaultArrayStyle{N}}, ::Type{ElType}) where {N,ElType<:Foo}
           return StructArray{ElType}(undef, size(bc))
       end


julia> foos = Foo.(1:10, 2:11, 3:12)
10-element StructArray(::Vector{Int64}, ::Vector{Int64}, ::Vector{Int64}) with eltype Foo{Int64, Int64, Int64}:
 Foo{Int64, Int64, Int64}(1, 2, 3)
 Foo{Int64, Int64, Int64}(2, 3, 4)
 Foo{Int64, Int64, Int64}(3, 4, 5)
 Foo{Int64, Int64, Int64}(4, 5, 6)
 Foo{Int64, Int64, Int64}(5, 6, 7)
 Foo{Int64, Int64, Int64}(6, 7, 8)
 Foo{Int64, Int64, Int64}(7, 8, 9)
 Foo{Int64, Int64, Int64}(8, 9, 10)
 Foo{Int64, Int64, Int64}(9, 10, 11)
 Foo{Int64, Int64, Int64}(10, 11, 12)

julia> foos[1]
Foo{Int64, Int64, Int64}(1, 2, 3)

julia> foos.b
10-element Vector{Int64}:
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11

julia>

The method for similar takes inspiration from this line in broadcast.jl for Bool: julia/base/broadcast.jl at 860188fb617a2a26042a5d6b64b2e40c960c3ccb · JuliaLang/julia · GitHub

But, what if I feel like creating a plot afterword with Makie?

julia> using GLMakie

julia> fig = Figure()
ERROR: UndefVarError: `T` not defined in static parameter matching
Suggestion: run Test.detect_unbound_args to detect method arguments that do not fully constrain a type parameter.
Stacktrace:
  [1] buildfromschema(f::Function, ::Core.TypeofBottom)
    @ StructArrays ~/.julia/packages/StructArrays/n5wxA/src/lazy.jl:55
  [2] #_#25
    @ ~/.julia/packages/StructArrays/n5wxA/src/structarray.jl:206 [inlined]
  [3] StructArray
    @ ~/.julia/packages/StructArrays/n5wxA/src/structarray.jl:205 [inlined]
  [4] similar
    @ ./REPL[7]:2 [inlined]
  [5] copy
    @ ./broadcast.jl:902 [inlined]
  [6] materialize
    @ ./broadcast.jl:867 [inlined]
  [7] Attributes(pairs::Vector{Pair{Symbol, Union{}}})
    @ MakieCore ~/.julia/packages/MakieCore/yRxVU/src/attributes.jl:31
  [8] Attributes
    @ ~/.julia/packages/MakieCore/yRxVU/src/attributes.jl:32 [inlined]
  [9] Scene(; viewport::Nothing, events::Events, clear::Bool, transform_func::Function, camera::typeof(campixel!), camera_controls::EmptyCamera, transformation::Transformation, plots::Vector{…}, children::Vector{…}, current_screens::Vector{…}, parent::Nothing, visible::Observable{…
}, ssao::SSAO, lights::MakieCore.Automatic, theme::Attributes, deregister_callbacks::Vector{…}, theme_kw::@Kwargs{})
    @ Makie ~/.julia/packages/Makie/6KcTF/src/scenes.jl:231
 [10] Figure(; kwargs::@Kwargs{})
    @ Makie ~/.julia/packages/Makie/6KcTF/src/figures.jl:112
 [11] Figure()
    @ Makie ~/.julia/packages/Makie/6KcTF/src/figures.jl:108
 [12] top-level scope
    @ REPL[13]:1
Some type information was truncated. Use `show(err)` to see complete types.

julia> 

Not great!

But if I change the definition of similar slightly:

julia> struct Foo{Ta,Tb,Tc}
         a::Ta
         b::Tb
         c::Tc
       end

julia> using StructArrays: StructArray

julia> function Base.similar(bc::Broadcast.Broadcasted{Broadcast.DefaultArrayStyle{N}}, ::Type{Foo{Ta,Tb,Tc}}) where {N,Ta,Tb,Tc}
                  return StructArray{Foo{Ta,Tb,Tc}}(undef, size(bc))
              end

julia> foos = Foo.(1:10, 2:11, 3:12)
10-element StructArray(::Vector{Int64}, ::Vector{Int64}, ::Vector{Int64}) with eltype Foo{Int64, Int64, Int64}:
 Foo{Int64, Int64, Int64}(1, 2, 3)
 Foo{Int64, Int64, Int64}(2, 3, 4)
 Foo{Int64, Int64, Int64}(3, 4, 5)
 Foo{Int64, Int64, Int64}(4, 5, 6)
 Foo{Int64, Int64, Int64}(5, 6, 7)
 Foo{Int64, Int64, Int64}(6, 7, 8)
 Foo{Int64, Int64, Int64}(7, 8, 9)
 Foo{Int64, Int64, Int64}(8, 9, 10)
 Foo{Int64, Int64, Int64}(9, 10, 11)
 Foo{Int64, Int64, Int64}(10, 11, 12)

julia> foos.a
10-element Vector{Int64}:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10

julia>

Makie works again.

Could someone explain what’s going on there?

1 Like

ElType<:Foo includes the bottom of the type lattice Union{} because that’s a subtype of everything, like Any is the supertype of everything. The stacktrace shows that there are Union{}s in the mix. Your other definition doesn’t have this problem. It’s an annoying footgun of the type system.

1 Like

Excellent, thanks! The thing I don’t understand is why Union{} is apparently “below” Foo but not Foo{Ta,Tb,Tc}:

julia> Union{} <: Foo
true

julia> Union{} <: Foo{Int64,Int64,Int64}
true

julia> Union{} <: Bool
true

julia> 

Isn’t Union{} below everything, including concrete types?

The second version has no <: so you’re only letting the type parameters of Foo vary but Foo itself is fixed. In Julia X{Y} is not a subtype of X{Z} if Y is a subtype of Z. The only exception is if X === Tuple

1 Like

I see now, thanks again. This helped convince me:

julia> struct Foo{Ta,Tb,Tc}
                a::Ta
                b::Tb
                c::Tc
              end

julia> f(T::Type{<:Foo}) = println("hello from f1")
f (generic function with 1 method)

julia> f(T::Type{Foo{Ta,Tb,Tc}}) where {Ta,Tb,Tc} = println("hello from f2")
f (generic function with 2 methods)

julia> foo = Foo(1,2,3)
Foo{Int64, Int64, Int64}(1, 2, 3)

julia> f(typeof(foo))
hello from f2

julia> f(Union{})
hello from f1

julia>