TraitWrapper - an idea for developing a traits based system

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.

6 Likes

I think this makes sense as a thing to do.

Note: your example traits are both dynamic, so do not compile away.
(At least for now. See also Tricks.jl)

1 Like

Worth trying; seems closer to a CharacteristicWrapper which would be helpful in its own right and somewhat orthogonal to the machinery and algorithmic specialization that Holy Traits embody.

@maruo3 do you see a clean and helpful way to package this approach or some variant stemming from this approach?

1 Like

Tidied up my thinking and put it in a package https://github.com/xiaodaigh/TraitWrappers.jl

5 Likes

And what if you need to dispatch not on a single object trait, but a combination of several traits of a single object?

4 Likes

That is a very good question. Must admit I haven’t thought too much about that. But I think you would define a new Trait which is the trait that satisfies all those traits. Maybe providing convenience functions to make it easy to to define such a trait would do the trick.

Been reading into Traitor.jl, I think their system is quite nice and maybe TraitWrappers.jl isn’t really necessarily. But it’s an interesting intellectual exercise for me.

Probably something like TraitWrappers could be useful in reviving / mimicking Traitor.

https://github.com/mauro3/SimpleTraits.jl is alive. Seems to have adopted some Traitor.jl like syntax.

Yes, SimpleTraits.jl is alive, and I’m planning to keep it that way. But I will probably not have time to extend its functionality. I think it can be attractive to use in “normal” cases, like your example above. It helps to reduce boiler-plate, keep the code consistent (there are several ways to implement THT), as well as clearly marking places where you use traits. But it’s not as powerful as hand-coding, so you can’t do more complicated cases. (Although, it handles two-type traits just fine.)

I think your trick is potentially nice, moving boiler plate from trait-functions to trait definitions. Although, youR IterableTraitWrapper errors when used with a non-iterator, so maybe in the else clause of the constructor, you should return something like Not(new{T}(t))?

2 Likes

There are many more types than traits, and that is likely to remain so. What if we were to use the machinery of abstract types and singleton structs in an orthogonal manner, providing abstract traits and singleton traitstates? There would be distinct pools; types and traits would be separate in memory.

With that, we could support (at least) single trait augmented abstract single
inheritance; and, probably – as most distinct traits do not create diamonds – several trait augmented abstract single inheritance. Moreover, one trait may be introduced at an earlier level of type abstraction and another associated at a later unfolding of the line of abstract inheritance.