Why does Julia not support multiple traits?

With traits, these are so-called associated types. I’m just now prototyping them, inspired by this thread.

Good to hear! What I don’t understand, why you introduce new terminology (traits vs. abstract types, associated types vs. type parameters). I think, that will change the character of the language much more, than the extension of Julian concepts in my thoughts.

That is terminology used elsewhere, for instance Rust. The point is that even though those concepts are similar, they are not equal, thus more names…

1 Like

In case someone is interested:

1 Like

@klacru I think you should feel comforted that there are others that want similar things, and agree that Julia should move to support these things.

The feedback on v1.0 is that it is not to introduce lots of new features but rather to complete any known and necessary breaking changes that would otherwise block improving the language, so that we can get some mileage and stability out of a v1.0, v1.1, etc series of releases.

The thing not mentioned is that trait-based dispatch, multiple inheritance, protocols, mixins, etc are new features and not necessarily breaking. Obviously we may choose to make breaking changes to Base when/if any of these become available but we wouldn’t have to do that immediately (I think we’d want them to exist for some time before “good design” becomes obvious).

From my work on Traitor, it would seem to me that one “computable” way to think of traits is that each leaf type has an “abstract supertype” for each “trait dimension”. To get the object’s place in each type tree we use a function - the current one being typeof (maybe I should say identity if we are already acting from the type domain) but we could also consider eltype, isiterable, etc. We take an abstract type as the intersection of all the sets of types satisfying each trait constraint.

An example with modern where syntax might be something like f(x::T where eltype(T) <: Number). For dispatch to work we need to be able to ask if the input type(s) satisfy a signature (easy) and if one signature is more specific than another signature (also easy if you make the assertion that every trait function is completely orthogonal to the others). Given that traits should be computable, I feel that protocols could easily be traits (but there are subtleties relating to 265 which would need to be dealt with).

However, it would nonetheless require a bunch of work to implement this. Given the interaction between type parameters, varargs, covariance, invariance, and specificity being different to subtyping, it’s already very challenging to get dispatch 100% bug free. In fact, it’s taken a whole bunch of work to even get to where we are where abstract types are sensibly defined in terms of sets of possible concrete types. Adding traits and/or multiple inheritance to the type system would be the logical next step in the journey (from my point of view) but some patience will be required. :slight_smile:

Finally, while multiple inheritance may solve some problems, it doesn’t allow post-hoc annotations of existing types with new traits, which is extremely useful.

4 Likes

I seem to remember that @jeff.bezanson mentioned in his JuliaCon talk that they are still a bit on the fence regarding the choice between multiple inheritance and traits.

To me, it would seem that multiple inheritance could be a better fit for Julia, given that it already dispatches on types. If dispatch on traits also becomes possible, you have to wonder when writing a method if you should define the argument as a trait or a type, and when you should use a trait or a type. I think the distinction between an abstract type and a trait is quite vague and could be a major source for confusion.

What is the concrete use case for this, actually? Thinking abstractly, it would seem unsafe to add traits to a type while leaving the author of the original type unaware of this. What if he modifies the original type in such a way that adhering to the trait becomes impossible?

Finally, another plus for multiple inheritance is that it makes it easier to map types from languages that also support it, such as Python and C++.

4 Likes

To be clear, I’m not providing an argument not to do multiple inheritance. If the type intersection algorithms turn out to be easy, then this may well be implemented, and I’m sure that could be great.

However, it’s quite common for a package to extend
new behaviour including on existing types. As just one example, as a package author I’d like a nice way to organise dispatch on e.g. some subset of AbstractArray. E.g. StaticArrays methods are valid not only for StaticArray but also RowVector{<:Any,<:StaticArray} and similarly for Symmetric and other types. A trait I could annotate these with and add specialisations of Base functions for in a convenient way would make life a lot easier. More importantly, other array wrappers in third party packages I’m not even aware of could opt into StaticArray behaviour in appropriate cases.

Tim Holy traits have been used relatively frequently to control dispatch - because the pattern is useful. Generalising this to multiple traits per function/method which don’t have to be known in advance would provide a strictly more flexible and powerful computational model for dispatch (while still being feasible), and yes it still would totally be well-described by the type system (as an extension of UnionAll) and like multiple inheritance, be based on intersections of properties.

The stylistic decision to use traits or types may continue to be a dilemma either way (a la Tim Holy traits, since this let’s you do some things even multiple inheritance will not…).

1 Like

Could you point to the frequent usage of traits in packages?

Again: how would the Number Type hirarchy expressed using HT? I do not get that.

I further do not see how to attatch an interface to a HT? Isn’t this an orthogonal issue to Type hierarchy vs Traits?

Types are an “is a” relationship. Meaning when you say Tree <: Graph you’re saying that a Tree is like a Graph in some ways but not necessarily all ways.

Traits are a “like a” relationship. Meaning when you say Graph like_a Tree you’re saying that Graph’s are everything that Tree’s are but might be more than just that.

Edit: But I’ve just realized that means that adding a trait is equivalent to adding an abstract type to an existing type so I see where the confusion lies now

I don’t see the semantic difference between these two. “Is a” also means that the Type can be much more than the subtype implies. If one inherits from several abstract types this becomes even more obvious.

Maybe we should simply allow to inherit from an interface after its creation. In this way we have a single concept instead of two. One may directly inherit at type definition time but may also defer it.

1 Like

Not sure if my insight adds much to this discussion but the way I view traits is along the lines of interfaces. A trait can be used to group objects by which interfaces they are compatible with. That can be useful to make extremely generic functions and explicitly mentioning only the “traits” I assume the inputs have because I will use them somewhere in my function. This is better than limiting myself to one abstract type root node in a type tree or enumerating all root nodes of type trees that my functions can be expected to work with. Also the error message when incorrectly using a function on inappropriate input will be semantically more rich. Moreover, if one trait always implies another, then I have the option of changing that to a supertype-subtype relationship, in fact I suppose this is the right thing to do. Doing that will save me the effort of explicitly defining which traits each concrete type has but it is understood that all traits of the parent are inherited by its children. But then I suppose there are ways to work around each one of the use cases I mentioned above, so this traits vs multiple inheritance argument may all be just a matter of stylistic preferences. That’s just my 2 cents!

“is a” and “like a” were bad terms to use. If anything I should probably of used them the other way around

Agreed but each of those things would be orthogonal

What do you mean by interface? Like an abstract type is used today?

I’m starting to think traits are like multiple inheritance where you can also add abstract types to a type after that type has already been defined.

1 Like

Maybe, I suppose a trait can also be viewed as a mini/soft abstract type that is more of an analogy between types than a philosophical isa relationship. And like all analogies, if an analogy turns out to be deeply rooted in the definitions of the analogical entities, you might and probably should start viewing them as mere realizations of the same abstract concept/type :slight_smile:

Currently, the manual chapter on interfaces describes them as “informal”. Examples of these informal interfaces are Iterable and Indexable, but neither of these have an abstract type associated to them. AbstractArray on the other hand does have an abstract type associated with it. I’m not sure what the reason for this is, but it does seem to indicate that an abstract type can be used to indicate expectations about a certain interface being available. Maybe Iterable and Indexable don’t exist as types because they would conflict with the rest of the type hierarchy, e.g. a String is Indexable but also an AbstractString. With multiple inheritance it would be possible to define abstract types for each interface (if deemed desirable, it may be overkill in many cases).

Traits on the other hand appear to be used mainly to hint at which implementation to choose, such as the indexing type for arrays. I think using them to define families of types that adhere to an interface is abusing them somewhat, and could cause confusion between when to use a trait and when to use an abstract type. So maybe for traits it is enough to stick with the existing mechanism?

1 Like

Maybe, but I think at the time being formalizing interfaces is a piece in the puzzle that can only be filled with traits (or maybe multiple inheritance in the future). In fact one can argue that traits do an almost perfect job (to the best I can see) generalizing abstract types and interfaces. Looking back on some of the code I have written, I often started with an extremely generic abstract type, then branched off of it to some slightly less abstract types until I reached all the concrete types at the bottom. But I could have also started with the concrete types and defined certain traits and trait inference rules up what used to be an abstract type tree. But in addition to that, I can also define a trait for all types that are summable, indexable, iterable, differentiable, isbits or even some very local traits as package1-compatible, package2-compatible, etc. I suppose defining each and every one of these as supertypes even in a language supporting multiple inheritance is a bit of an overkill, but something about the word “trait” tells me I can do that!

I guess my slight bias towards traits comes partly from the simple philosophical fact that a parent should not come to existence after its children, but then traits can develop with time. This means that if I notice a trait that different types from different packages have, I can define such a trait in my package and exploit this trait/analogy somehow to provide support for similar types across different packages. Defining a parent in this scenario would contradict this basic philosophical fact.

Disclaimer: I don’t claim deep knowledge of many programming languages, I am merely conveying my perspective.

The situation with AbstractArray and Iterable just shows the limitation of the current system since we do not have multiple inheritance.

The reason traits are used seems to be mainly that they provide a workaround for multiple inheritance. But the concepts are so similar that we really should unify them and come up with a single type system. If extending an existing type is desired then just lets allow it. If it’s called trait or not does not matter IMHO.

3 Likes

Multiple inheritance versus traits which can be added after-the-fact strike me as having a large similarity to the expression problem but in the type domain. One of the most powerful things about multiple dispatch is not the multiple part, but the external part: i.e. that another person can come along and independently add generic functions that operate on existing types and add new types to which existing generic functions apply. That’s extremely powerful and is a huge part of why generic programming “just works” in Julia. While it seems very powerful – perhaps overly powerful – to be able to add supertypes after-the-fact, it does seem necessary for solving the type-level expression problem. It is possible, however, that we can add multiple inheritance for one case and address the other case via a more flexible but somewhat less convenient mechanism like Holy traits.

9 Likes

I like the “can do” attitude, but someone does have to implement it :face_with_monocle:.

1 Like

That’s not what I wanted to imply. The usual answer one gets here when asking for multiple inheritance is: we don’t need that we have holy traits. And from that point traits and abstract types are thought of as independent things. My opinion is that this is not satisfying. And my proposal is to

  1. introduce MI
  2. allow to add an abstract type to an existing type after it has been created
  3. Introduce interfaces that link methods to an abstract type

Point 2. should hopefully address the concerns of people who prefer traits. Before thinking about actual implementation it would be interesting if the above is actually technical feasible. @jeff.bezanson may answer this.

If the above sounds interesting I could come up with a julep for that.

4 Likes