RFC: Language Support for Traits — Yay or Nay?

Oops, for some reason I deleted this from my OP: I think also traits might have to be a special function, in that traits is not called to dispatch on the traits of its argument (to avoid stackoverflow).

It could be similar to that, or new keywords could be reserved; I just wanted to express the idea in terms of existing Julia as much as possible for clarity.

To make this idea “work” as a package—that is to say, to hack the idea so that on a surface syntax level it works with the current dispatch system—I’d make a macro which would wrap function arguments such that whenever calling a function,

f(a, b, c, d)
# is transformed to
f(TTr(a), TTr(b), TTr(c), TTr(d))

where TTr is defined as:

struct TTr{X, XTr}  
    x::X
    TTr(x::X) where X = new{X, traits(X)}(x)  
end

Then, method signatures would be transformed like this:

f(a, b::B, c::::CTr, d::D::DTr) = ...
# is transformed to
f(a::TTr{<:Any, >:Union{}}, b::TTr{<:B, >:Union{}}, c::TTr{<:Any, >:CTr}, d::TTr{<:D, >:DTr}) = ...

It should work quite handsomely with methods defined and called like this.

However, you’re confronted with the fact that this would only work for functions that have already been declared this way; nothing in Base and no other package would be compatible with this function call, so you’d have to know at the callsite not to use this macro on those. It would be an un-composable package, an island of its own. Unless…

Yikes!

I believe this proposal, implemented in the compiler, isn’t breaking (at least if we consider it okay to break code which uses the pattern var::Type1::Type2, which I’d argue is already broken). Code that already implements Holy traits would still work; it just wouldn’t be necessary to do it that way anymore. And code that doesn’t use any traits would work as-is.

Implementing it as a package, however, would require breaking literally everything and a herculean effort to make it work again.

Yes, this is the sort of insight I’m hoping to gain here. What are the most onerous and insidious possibilities you can imagine this proposal would open? Would they be infeasible to address through proper patterns and use of Test.detect_ambiguities? Would we need new tools to understand the interactions? Would such problems be solvable, or would it be a Pandora’s box?

Possibly, but I feel like that problem is better solved by adding better support for wrapper structs. My ideas on that topic aren’t mature enough to be worth sharing though.

This is exactly what inverting the meaning does: when you query whether an object implements a set of traits, increasing the size of the query set decreases the size of the set of objects that match the criterion, since the latter set is an intersection. An object that implements a very large set of traits is a special object, as the set of such objects is very small.

Being able to dispatch on both type hierarchy and traits allows for further intersection.

This is an excellent example of how this proposal would allow greater extensibility. This is probably also the best example of why this idea could open up unexpected dispatch ambiguities, because every object must show :sweat_smile: If this problem is resolvable, then perhaps any such problem would be.

For example, suppose I define MyTrait2 and show(::IO, ::::MyTrait2), and I have an object ::Foo for which traits(Foo) >: Union{MyTrait, MyTrait2}. This creates an ambiguity for ::Foo’s show method, unless we either define show(::IO, ::::Union{MyTrait, MyTrait2}), or we define show(::IO, ::Foo).

Is it possible to develop a set of rules—language features, design patterns, best practices, etc.—by which such an ambiguity could readily either a) be anticipated and avoided, or b) be detected and resolved? Perhaps we’d want some friction in the process of adding traits to a type, because adding a new trait can suddenly introduce dispatch ambiguities? Or maybe we’d want type hierarchy visualization tools? Or maybe we’d want language features to enforce orthogonality of traits? What would trait orthogonality even mean? I’m spitballing here and wide open to ideas.

It seems like maybe traits should continue to be considered an “advanced” feature, a tool with sharp edges that neophytes would be discouraged from using unless there are ways to put safeties in place. I suspect that the extra :: should also add friction to cause a preference toward dispatching on the type hierarchy instead of traits, but maybe I’m wrong.

As @Mason points out, this isn’t just about saving three lines of code. Functions that don’t already dispatch on traits, cannot be extended to. And functions that do, can’t be extended to specialize on new trait categories (groupings of traits that are returned by a GetThisTrait function). By contrast, this proposal makes such trait categories unnecessary.

That curse is also a blessing: it forces trait dispatch to be rather specialized in what trait categories it can dispatch on, which alleviates the potential for ambiguities (the ambiguities for the show method example above are impossible with Holy traits). But it makes further specialization impossible.

Can I interest you in type-parameterizing Core.Box? As shown here, that could result in immediate performance improvements. I also have ideas for how to reduce the number of boxes that are generated, but perhaps that should be for a different post (infact, I already pondered a simple one here that garnered zero attention, and this one only became an argument. I’m also hoping to build support for this one.).