How do I define eltype for custom composite type?

I tried creating my own container type alias:

Forest{Tree} = Union{<: AbstractVector{<: Tree}, <: AbstractSet{<: Tree}}

I want to do, in essence:

Base.eltype(::Type{Forest{T}}) where T = T 
julia> eltype(Forest{Int})
ERROR: UndefVarError: `T` not defined
Stacktrace:
 [1] eltype(::Type{Union{var"#s13", var"#s12"} where {var"#s13"<:(AbstractVector{<:Int64}), var"#s12"<:(AbstractSet{<:Int64})}})

Which I get because the Forest definition is being replaced by what it is aliased to.
Is there a way to define the eltype for Forest specifically?

julia> Base.eltype(::Type{Union{AbstractVector{T}, AbstractSet{T}}}) where T = T
julia> eltype(Annotations.Forest{Int})
Any

So now it doesn’t complain but doesn’t give the right answer either …

julia> Base.eltype(::Type{Union{<: AbstractVector{<: T}, <: AbstractSet{<: T}}}) where T = T
julia> eltype(Annotations.Forest{Int})
ERROR: UndefVarError: `T` not defined
Stacktrace:
 [1] eltype(::Type{Union{var"#s13", var"#s12"} where {var"#s13"<:(AbstractVector{<:Int64}), var"#s12"<:(AbstractSet{<:Int64})}})

Now we’re back to the start.

1 Like

This is what you want, I guess:

julia> const Forest = Union{AbstractVector{Tree},AbstractSet{Tree}} where {Tree}
Union{AbstractSet{Tree}, AbstractVector{Tree}} where Tree

julia> Base.eltype(::Type{<:Forest{T}}) where {T} = T

julia> eltype(Forest{Int})
Int64

However, the eltype method definition above is what is termed type piracy, meaning that it’s not OK for production code, because neither the function (eltype), nor any of the type signature elements are owned by you, so such a definition wouldn’t behave well in the package ecosystem.

To avoid type piracy, perhaps define Forest as a struct instead of as a type alias.

Thank you.
Yes this is just for an analysis I’m doing - won’t become production code.
Is there a struct version of this you would recommend? I’m not sure how to use structs to represent sum types - like this?

abstract type Forest{T} end 
struct VectorForest{T} <: Forest{T}
  v::Vector{T} 
end
struct SetForest{T} <: Forest{T}
  s::Set{T}
end

I guess this is the “tagged union” approach but I can’t augment behavior of known data types this way. I would also have to reimplement every single function related to Vectors or Sets on my datatype if I wanted to use it as one.

I’m also curious why extending eltype to work with Unions would be considered “type piracy” - the behavior I’m generally creating here is:

given eltype(X{T}) == eltype(Y{T}) == ... == T
eltype(Union{X{T}, Y{T}, ...}) = T

Which seems sound to me.

1 Like

Perhaps use one of the available sum type packages, e.g., SumTypes.jl.

Assume type piracy were not frowned upon. In this case it might happen that variations upon your eltype definition could appear in several independent packages. Then it could happen, upon loading multiple of these packages, that some eltype calls would be ambiguous. Thus basically each package in the ecosystem could only work with a whitelisted set of packages. This would prevent the ecosystem from growing in a unified manner, instead I guess it’d be fractured into many smaller ecosystems.

Just to flesh out why this is type piracy: despite using descriptive labels Forest and Tree you are actually just aliasing AbstractVector{<:Any} and AbstractSet{<:Any}. You aren’t actually defining any types that you own, just renaming existing types. So any methods of existing functions (ie Base.eltype) defined for Forest and Tree will be type piracy.

By defining a new type called Forest you avoid type piracy.

ETA: the reason that you haven’t encountered any issues with this is that your eltype methods are pretty much exactly what Base is doing anyway. Essentially Base defines each case of the type union separately, you’re just combining them.

1 Like