I think you’re right. I’m trying to express a position in an object type hierarchy and the possession of a set of traits; the solution to my dissatisfaction is to find a more concise way to express this, and the most concise expression is as an intersection—a construct that Julia simply doesn’t have.
I think it could be backwards-compatible. Simply put, instead of x::X
asserting typeof(x)<:X
, and instead of having methods dispatch on typeof
, imagine if we instead had x::X
assert traitsof(x)<:X
and methods dispatch on traitsof
.
Maybe it could work like this:
Intersecting Types and Traits
First, introduce a new type. You called it Meet
, but to keep the spirit of shaven yaks alive I’ll call it Intersection
. In a tree-shaped type hierarchy, the notion of an intersection is useless; Intersection{Signed, Number}
is Signed
, and Intersection{String, Int}
is Union{}
. With such a type system, there’s simply no way that Intersection
could be useful (hence we don’t have it). But let’s introduce it.
Next, introduce the notion of multiple hierarchies. Currently we have only one: the hierarchy of objects with a top of Any
and a bottom of Union{}
, and all types declared in all modules share this hierarchy. A trait such as Growable
doesn’t belong on this hierarchy—growability isn’t a category of thing; it’s a property that belongs on its own plane of existence.
To express this, maybe we could have a “type module” to declare a completely separate hierarchy:
type module Growable
struct Is end
struct IsNot end
end
Growable.Is <: Growable.Any # true
Main.Any != Growable.Any # true
Intersection{Growable.Is, Growable.IsNot} # Growable.Union{}
With orthogonality assumed, the intersection of a type in the Growable
hierarchy with a type in the object hierarchy will be irreducible—i.e., Intersection{AbstractString, Growable.Is}
cannot be reduced to Union{}
, but instead must remain Intersection{AbstractString, Growable.Is}
. In fact, every trait should exist in its own hierarchy, ideally orthogonal to every other trait, so that intersections with multiple traits can be formed.
Let’s finalize the specification of Intersection
. Like Union
and Tuple
, Intersection
types are covariant in their parameters, so that if T1<:T2
and MyTrait.TA<:MyTrait.TB
, then Intersection{T1, MyTrait.TA} <: Intersection{T2, MyTrait.TB}
. Also, like Union
, order doesn’t matter.
Next, imagine if we have these definitions:
traitsof(x) = Intersection{typeof(x), traits(typeof(x))}
traits(::Type{T}) where T = Any
As before, traits
could be a special function, in that it dispatches on its argument’s typeof
instead of traitsof
.
For a type that doesn’t have any traits, you can see how traitsof(x)
reduces to typeof(x)
, and therefore x::X
is backward-compatible. And if x
now becomes traitful, but X
is still just a supertype, x isa X
remains true and x::X
still asserts correctly.
Then, to add traits to a type, we can perform a trick similar to the OP:
add_traits(T, Tr) = let Trs = Intersection{traits(T), Tr}
eval(:( traits(::Type{var"#T"}) where var"#T"<:$T = $Trs ))
end
I think we have all the pieces we need now. With this, function dispatch could work like this:
# function call
f(a, b, c, d)
# method signature
f(::Any, ::B, ::CTrait.Tr, ::Intersection{D, DTrait.Tr})
# this method is chosen if:
traitsof(a) <: Any &&
traitsof(b) <: B &&
traitsof(c) <: CTrait.Tr &&
traitsof(d) <: Intersection{D, DTrait.Tr}
# (and, of course, if this is the most specific method
# that satisfies these constraints.)
Here’s a question: what should typeof(Growable.Is)
be? Should Growable.Is isa Type
? If so, then maybe we can make this shorter with ∩(A::Type, B::Type) = Intersection{A, B}
.
Are there any concerns with this? Would it work? Did I miss something? Can I choose better function names?
Taking a stab at method ambiguities
One thing I’ve noticed about method ambiguities is that anecdotally it seems like they don’t need to exist. Namely, ambiguities are introduced and resolved, but if methods were written in a different order (i.e., starting with the method that solved the ambiguity) the ambiguity would never have existed.
Maybe we can leverage Julia’s dynamic nature—which gets in the way sometimes by forcing method definitions to be sprinkled around type declarations (and vice versa) because of the linear evaluation order—and put it to use here. Namely, every time a method is declared, we could check if it creates any ambiguities with existing methods and throw an error if it does. That would alleviate concerns over method ambiguities by making them strictly impossible to begin with.
Is it generally true that ambiguities can be avoided throughout the course of declaring a function’s methods by properly ordering evaluation? Or am I wrong—would throwing errors on ambiguous method declarations cause some valid method signatures to be unreachable?