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.