However, I’ve come across a different approach used by libraries such as Tables.jl, where traits are determined by boolean functions, for example:
Tables.istable(t::T) = true
This method also appears to be used by WhereTraits.jl. I’m curious if there’s a specific term for these “boolean” traits and whether there’s a general preference or consensus in the Julia community regarding which trait implementation is considered more effective or appropriate.
In your example, is traitfunc supposed to be somefunc? Also, the T in the definition of LiquidityStyle is not defined. And what is the purpose of LiquidityStyle? It seems that it is not used later on.
Yes, sorry, it’s late. I just wanted to give an example of what I understand as the conventional implementation for Holy traits, so I copied from Holy Traits Pattern (book excerpt), which seems to be the accepted authority for that. Thanks, I corrected the original post!
The main difference between “boolean” and Holy traits are that Holy traits operate in the type system whereas boolean traits use values.
The definitions of each are roughly equivalent but Holy traits are more extensible and probably more performant (due to leveraging Julia’s dispatch).
Using boolean (actually symbols, but works the same) traits:
traitval(::Any) = :none
traitval(::A) = :A
traitval(::B) = :B
somefunc(x::T) where {T} = somefunc(traitval(x), x)
function somefunc(val, x)
if val == :A
return :a
elseif val == :B
return :b
else
# unsure what to do and can't be extended later
end
end
We could use Val(traitval(...)) and lift this into the type system for dispatch, which is essentially reinventing Holy traits, potentially less efficiently.
Holy traits allow us to easily extend by simply adding something like
This means that traits can be added without changing existing code which makes the code more maintainable.
More usefully, since all these operations are operating on types the compiler can generally infer which path will be taken during compilation, avoiding run-time dispatch (ie the if statement in the boolean trait example).
In practice, even with value-based traits the compiler can usually do constant propagation and know which branch to take anyway. So usually there is no run-time penalty.
Yep, that’s what I worried about. In general, I prefer the “boolean traits” as they are closer to what’s usually called traits in other languges (like Rust).
traits that return results in the value space (eg values of the same type, such as true::Bool or false::Bool, an Enum, a Symbol, etc),
traits that encode this information in the type space, using designated types such as Base.HasShape{N}, a payload in a Val{T}, etc.
Generally, the first option may be more convenient, but can run into problems unless you branch or condition on the trait insider the caller. When you cross a function boundary, the compiler is not guaranteed to propagate the information with constant folding, so the second option is preferable.
Working in the type space is also better if you want to keep things extensible (users can define new traits), and/or use methods instead of branching.
There are quite a few traits packages these days but BinaryTraits.jl allows you to define something that an object can do (positive) or cannot do (negative). Perhaps you can take a look.