Why does Julia not support multiple traits?

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

@tobias.knopp: Maybe we should join our efforts. Look at this: most desired

Yes, I am currently travelling but can contribute to a document in a week.

Let’s make this a real julep, I.e. a design document that outlines the features. Juleps is just markdown living in a julialang repo.

Note that I have the same vision as you have outlined

4 Likes

No, I don’t think Holy-traits are seen as the “solution”, merely a stop-gap pattern until a time when traits are implemented at a language level.

Yes, I too think that traits and multiple inheritance for abstract types in which types can be added after creation are essentially identical. Of course, the latter is a bit of a mouth-full and would need a name: “trait” is a good one. In particular, if we semantically separate them from “interfaces”/“protocols” as you seem to suggest in point 3 and as I suggested above. Also, once you add interfaces and have a correspondence of them to abstract types, I think point 2 is needed. Or it would be a bit strange to have a type which implements the interface but is not a subtype of the corresponding abstract type.

And, yes, I also agree that in the end there should be one “dispatch system” and not some on types, some on traits, some on …

Last, I still think that traits of several types would be good to have: some interfaces in a multi-dispatch world will involve several types and it would be nice to encode them as a trait. To translate this into MI-speak, it would mean to add a supertype to a type tuple, say Tuple{Int,Float64}<:SomeAbstractType.

8 Likes

I don’t really think a design is required for this – it’s pretty well understood. Multiple inheritance where you can add ancestors to types after-the-fact is what I described in issue #5 (the oldest open issue). This is the maximally powerful design. It is also the hardest design to implement, and makes #265 look like child’s play compared to the kinds of updates you might need to do in order to keep the world coherent. Sometimes it’s a good idea to give people a lot of power in a language feature, but other times it’s better to restrict the power you give them. It’s still unclear which situation this is.

3 Likes

Ok, I you have a plan than I will be patient.

1 Like