So I had the tab open for Issue #5, and I can’t believe I hadn’t read it . It’s a roller coaster, defo recommended reading.
[Yet!] Another Idea:
What if we embrace WhereTraits’ idea of inserting function calls into the where
braces, but instead of giving them access to instances, give them access to typevars? Further, what if we disallow functions that return booleans, and only allow functions that return types? Staying in the type domain would allow traitful method declaration to maintain coherence with traitful type declaration, as well as leveraging the language’s type comparison machinery and never tempting people into runtime dispatch.
Imagine if this were a valid type specification:
T where T<:AbstractArray{E,N} where {E<:Float64,N} where {traits(T) >: Union{IndexCartesian, IsStrided}}
(Yup, I’m reverting to the idea of the OP—to express traits as a union .)
I’m not sure what the internal representation would look like, but somehow it would store λ = T->traits(T)
as something resembling a typevar, and the Union{...}
as a lower bound. And calling <:
or >:
to compare this object against another type would cause λ
to be called.
The method signatures of the OP would then look like this:
show(io::IO, ::T) where T where traits(T)>:HasDuckBill = print(io, "Probably a duck type...")
show(io::IO, ::T) where T<:Mammal where traits(T)>:HasDuckBill = print(io, "Maybe a platypus?")
show(io::IO, ::T) where T<:Mammal where traits(T)>:Union{HasDuckBill, LaysEggs}) = print(io, "Probably a platypus.")
show(io::IO, ::Platypus) = print(io, "Definitely a platypus!")
and
foo(A::AbstractArray, args...) = … # default for arrays
foo(A::T, args...) where T<:AbstractArray where traits(T)>:IndexCartesian = … # trait specialization
foo(A::T, args...) where T<:AbstractArray where traits(T)>:IsStrided = … # trait specialization
foo(A::T, args...) where T<:AbstractArray where traits(T)>:Union{IndexCartesian, IsStrided} = … # deeper trait specialization
foo(A::T, args...) where T<:AbstractMatrix where traits(T)>:Union{IndexCartesian, IsStrided} = … # deeper type specialization
I have an idea for how the mechanics of dispatch could work with this, but I’ll save it for later.
Interestingly, this approach is also pretty well-aligned with using Holy traits:
foo(A::T, args...) where T<:AbstractArray where IndexStyle(T)<:IndexCartesian = …
As a result, transitioning from the Holy traits pattern to a more generic style which calls traits
should be fairly smooth. And for functions without trait methods, there would be no change; the mechanics of non-traitful dispatch would be unchanged.
Using the other example of the OP:
# function call
f(a, b, c, d)
# method signature
f(::A, ::B, ::C, ::TD) where {A,C,TD<:D} where {traits(C) >: CTr, traits(TD) >: DTr}
# this method is chosen if:
typeof(a) <: Any &&
typeof(b) <: B &&
typeof(c) <: Any && traits(typeof(c)) >: CTr &&
typeof(d) <: D && traits(typeof(d)) >: DTr
# (and, of course, if this is the most specific method
# that satisfies these constraints.)
And, to dispatch on the traits of a collection’s elements:
foo(A::AbstractArray{E}) where E<:ElType where traits(E)>:ElTraits = …
There are lots of details to work out, but it seems like this should do what we would want. Thoughts?
Thinking about this again, the potential for packages to create method ambiguities with each other has always existed and isn’t unique to traits. Problems are encountered, solutions are found and the ecosystem moves on. Is there good reason not to be optimistic here?