I read with interests The Emergent Features of JuliaLang: Part II - Traits and have been thinking about traits lately. I think I understand how Holy Traits work but I feel unsatisfied with it.
The examples and the blogs I have seen are based on using traits on one of the arguments. But in my use-case, I have multiple arguments that I want to use traits with.
The use-case I have is JLBoost.jl’s tree boosting algorithm. At some stage, I want to use a predict
function to score out a bunch of trees on a DataFrame
.
The current function signature looks like this
function predict(jlts::AbstractVector{T}, df::AbstractDataFrame) where T <:AbstractJLBoostTree
mapreduce(x->predict(x, df), +, jlts)
end
The two arguments are jlts
and df
and as you can see it’s just using the mapreduce
function. Actually, I don’t require jlts
to be AbstractVector{T<:AbstractJLBoostTree}
nor df
to be AbstractDataFrame
at all.
I just need jlts
to be an iterable of S<:AbstractJLBoostTree
and df
to be that something supports df[!, column]
syntax. So naturally, I want to use traits. But if I use Holy traits I would end up with functions like this
function predict(jlts, df)
predict(Iterable(jlts), ColumnAccessible(df), jlts, df)
end
function predict(::Iterable{T}, ::ColumnAccessible, jlts, df) where T <:AbstractJLBoostTree
mapreduce(x->predict(x, df), +, jlts)
end
Again, I find that unsatisfying and I was thinking about this concept of a TraitWrapper
where the Trait
type contains the object that you want to use. E.g.
abstract type TraitWrapper{T} end
struct IterableTraitWrapper{T} <: TraitWrapper{T}
object::T
IterableTraitWrapper(t::T) where T = begin
if hasmethod(iterate, Tuple{T})
new{T}(t)
else
throw("This is not iterable")
end
end
end
struct ColumnAccessibleTraitWrapper{T} <: TraitWrapper{T}
object::T
ColumnAccessibleTraitWrapper(t::T) where T = begin
if hasmethod(getindex, Tuple{DataFrame, typeof(!), Symbol})
new{T}(t)
else
throw("This is not ColumnAccessible")
end
end
end
object(t::TraitWrapper) = t.object
Now I can just use my traits signature like so
function predict(jlts, df)
predict(IterableTraitWrapper(jlts), ColumnAccessibleTraitWrapper(df))
end
function predict(jlts::IterableTraitWrapper, df::ColumnAccessible)
mapreduce(x->predict(x, object(df)), +, object(jlts))
end
So that if I need more traits in a function signature I can just do that and not have to double the number of arguments. I also find the TraitWrapper
to be clearer as I can see which trait corresponds to which argument better and I am clearly expressing which argument I expect to have a certain trait.
I am quite new to the traits systems in Julia, but I find this TraitWrapper
idea very appealing and I wonder if it’s already done elsewhere. And I wouldn’t be surprised.
Another approach I thought of is to combine the two arguments into one trait, which might work too. Happy to hear ideas from elsewhere in the Julia-ecosystem.