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.