Announcing Traits.jl - a revival of Julia traits

To illustrate the ambiguity problem more concretely, let’s take the example of two orthogonal axes from the Traitor README. Let’s say I’m a “traits-naive” author, and in my package I write

foo(x) = 1

Everyone just loves this awesome new foo method, but they need to specialize it. Person A does the following:

# Steal `foo` and create a traits version
expr = steal_method_body(which(foo, Tuple{Any}))  # grabs the current method body then deletes the method
foo(x) = foo(Traits(x, Size(x)))
foo(x::Traits{<:Any, Big}) = 2   # a version highly specialized for Big things
@eval foo((x::Traits{<:Any, <:Size}) = $expr

But person B does this:

# Steal `foo` and create a traits version
expr = steal_method_body(which(foo, Tuple{Any}))  # grabs the current method body then deletes the method
foo(x) = foo(Traits(x, Odor(x)))
foo(x::Traits{<:Any, Smelly}) = 3   # a version highly specialized for Smelly things
@eval foo((x::Traits{<:Any, <:Odor}) = $expr

Now boom Person A’s package is completely broken just by loading Person B’s package. Ah, but wait you say, maybe we could be smart and generate the depot to do this:

foo(x) = foo(Traits(x, Size(x), Odor(x)))

And to be helpful I’ll also go through and redefine all their trait-dispatch methods to account for the pair of traits. But you’ve still broken both packages, because now you’ll get situations where you end up needing to dispatch on Traits(<:Any, Big, Smelly} and no one has prepared for this possibility or even knew it could happen.

In contrast, if the trait depot is defined from the beginning then at least everyone knows what might happen. We agree as a community on which traits we need and then everyone has to dispatch on them. Thanks to version controlled Pkg and CompatHelper updating with a new trait-axis is not the nightmare scenario it would otherwise be.

This is a very real-world issue. In the bad old days, getindex used dispatch to implement all sorts of fancy indexing behaviors (vector indexing, trailing 1s, etc.) To avoid package conflicts we had to work hard to make sure that the default getindex method was the most generic (least-specialized) version in existence, and also give people a way to write just the specialization they needed. This is where Vararg{Int,N} came from, so you can write Base.getindex(A::MyArrayType{T,N}, i::Vararg{Int,N}). That’s basically the foundation of Julia’s array-awesomeness, and the most difficult problem was figuring out how we were going to support fancy generic behaviors while also allowing people to specialize for their specific array types. What we needed to do was engineer an absence of methods (by requiring high specificity from the methods that would be implemented) so that generic fallbacks with low precedence nevertheless end up being called.

9 Likes