Trait definitions: type level vs instance level

Hi there,

I’ve been thinking about trait-based dispatch, particularly from this book excerpt. The example there defines traits at the type level as follows

# Default behavior is illiquid
LiquidityStyle(::Type) = IsIlliquid()
# Cash is always liquid
LiquidityStyle(::Type{<:Cash}) = IsLiquid()
# Any subtype of Investment is liquid
LiquidityStyle(::Type{<:Investment}) = IsLiquid()

tradable(x::T) where {T} = tradable(LiquidityStyle(T), x)
tradable(::IsLiquid, x) = true
tradable(::IsIlliquid, x) = false

This made me wonder why we don’t define the traits at the instance level, like this

# Default behavior is illiquid
LiquidityStyle(::Any) = IsIlliquid()
# Cash is always liquid
LiquidityStyle(::Cash) = IsLiquid()
# Any subtype of Investment is liquid
LiquidityStyle(::Investment) = IsLiquid()

tradable(x) = tradable(LiquidityStyle(x), x)
tradable(::IsLiquid, x) = true
tradable(::IsIlliquid, x) = false

From what I understand, both approaches are type-stable and would compile to efficient code. Is this more a matter of common practice within the community, or are there advantages to defining traits at the type level?

Thanks in advance for your insights!

1 Like

I don’t know for sure, but my guess is that you can always get a type from an instance with typeof but it could be potentially quite expensive to get an instance from a type.

This also allows generated functions that only know about the types of the inputs know about the traits too.

3 Likes

I think this is a matter of taste. Related thread:

Getting the instance of a singleton type is free. T.instance currently works, though it’s not documented or part of the public interface as far as I know. A guaranteed-to-work way is by relying on incomplete initialization:

struct Helper{T}
  v::T
  Helper{T}() where {T} = new{T}()
end
uninitialized_instance(::Type{T}) where {T} = Helper{T}().v
julia> uninitialized_instance(Nothing) == nothing
true

This is a bit roundabout, but could be abstracted into a tiny package easily. I actually wanted to register such a package, but then gave up because the functionality seemed overly trivial so I wasn’t sure that anyone would use the package.

Its not a matter of taste, there are clear reasons to define traits on types or on objects.

Traits on types
We often want traits to work inside generated function (where we only have types), or on the eltype of an empty vector. Our target object we want to know the trait of is rarely a singleton - its the trait itself that is a singleton. So T.instance is not useful (its also touching internals). So we need to have a trait on the type.

Traits on objects
We cant define the trait on the type if we only know the trait at run time. So the type is useless. For example, we need this in GeoInterface.jl because some objects, like vector points or gdal objects pased in from C, can be 2d or 3d depending on runtime size, So is3d has to work on objects rather than types. Nearly always its a compile-time operation (e.g. for a Tuple or GeometryBasics.jl geometry), but having it work on the object means the interface still works when its not, its just slower.

8 Likes