Why does Julia not support multiple traits?

Yes, this is essentially how it works in Java/c#: have interfaces which are fully abstract and implement against them. Without being able to implement multiple interfaces this useless though.

You can search “protocols” in Guthaben which Jeff envisioned to be a more generic “trait like” thing.

If you want to move this forward, I think a Concrete proposal (julep) Would be best

I think it makes sense to think about traits and interfaces/protocols as separate things.

A trait, I understand as a set of types, which can be used for dispatch. Using this definition, an abstract type indeed encodes such a set. One thing traits need to make them useful is that types can be added to them after creation; and this is where multiple inheritance would fall short (as the supertypes of a type are fixed at its creation/definition). Holy-traits are a pattern to create trait, which is extensible after its creation. Things which need to be figured out concerning traits are inheritance, intersections and such and what that means for method resolution; presumably set operations will do.

Now, as I see it, interfaces/protocols are a way specify which types belong to a certain trait: say the trait of all types supporting iteration means that each of its elements has to have methods start, next and done defined. The specification of these interfaces can be a bit intricate, this was discussed in #6975.

I think it is useful to keep a mental distinction of above two concepts even though they are quite intertwined.

2 Likes

Having learned a lot about traits, Holy Traits, Traitor.jl, multiple abstract inheritance, and protocols, I know clearer now, what I want. I see, that this discussion is very old and has not come to a conclusion yet. So I want to add my list of wishes. I don’t want to spoil the discussion, but be constructive.

My principles for the subject:

  • simplicity
  • orthogonality
  • liberal policy in type strictness
  1. The Julia type system with leaf types, abstract types, type unions, type parameters is close to perfect; together with the unique multiple dispatch an ideal working concept, I would not like to miss. Main purpose of abstract types is method dispatch and transport of type information. Whether the expected methods are missing for an object, is detected at runtime - no problem in a testing-oriented development approach. I love duck-typing!

  2. I want a (small) extension, though, that is multiple abstract inheritance. That would allow a leaf-type or abstract type to be contained in a set of super types, which is not restricted by the linear chain of super-types. To maintain method dispatch, a kind of linearisation of the supertype-set would be fine.

  3. It is not necessary to introduce much new syntax or a new feature. I think it is acceptable, that author of the base type has to decide, which supertypes his type belongs to. If remote extensibility is required, other concepts like (Holy-) traits or delegation, are still available.

  4. I do not see Jeff’s protocols as a replacement for abstract types, but as an optional additional property, the purpose of which is documentation, control (early error messages for undefined methods). There is a canonical order relation on the set of protocols (the inclusion of their methods sets), which must be compatible with the subtype relation on the set of types. (The map `{t | t is a type} → {p | p is a protocol} assigning a protocol to each type (default protocol does not have any methods) should be an order-homomorphism).

  5. I see multiple inheritance from leaf types problematic, infeasible, and unnecessary. Allow for extension of leaf-types by abstract types (singular inheritance from leaf-types).

  6. I would like to see automatically generated getter- (and setter) methods for some or all fields of a (mutable) struct.

  7. I would like to see automatically generated delegate methods for a field of a data type, if the field is typed with a protocol attached for all the methods mentioned in the protocol.

That needs more of an explanation than a name. I’m not entirely sure how that could be expected to be determined and easily understandable.

One thing I’ve been wondering is to what extend the base library would be designed differently if the language supported traits along the lines that @mauro3 describes. For example, would it then make sense to make something like Number a trait rather than an abstract type that one needs to inherit from? Same say for AbstractString, could that be a trait, and a concrete string implementation would not have to inherit from AbstractString?

My understanding is that the current plan is to deal with traits after 1.0 because one could probably just add them to the language without breaking anything. While that is true, the story seems less clear and simple in terms of the design of base, i.e. one could probably not move base from a mostly inheritance based design to a traits based design until 2.0. Or maybe one could? I haven’t thought much about it yet and would be quite interested to hear what others think.

2 Likes

I understand the term linearization as defined in the Scala Reference manual. In the case singular inheritance it returns the chain of supertypes starting with the type itself and ending with Any. In the case of multiple inheritance, the order of the list respects all defined subtype relationships.

Scala 2.11 Reference Chapter 5

5.1.2 Class Linearization

The classes reachable through transitive closure of the direct inheritance relation from a class C
are called the base classes of C. Because of mixins, the inheritance relationship on base classes forms in general a directed acyclic graph. A linearization of this graph is defined as follows.
Definition: linearization

Let C be a class with template C1 with … with Cn { stats }. The linearization of C, L( C)
is defined as follows:

L(C) = C, L(Cn) +⃗ … +⃗ L(C1)

Here +⃗ denotes concatenation where elements of the right operand replace identical elements of the left operand:

a, A +⃗ B == a, (A+⃗ B) if a ∉ B  A+⃗ B if a ∈ B

Example

Consider the following class definitions.

abstract class AbsIterator extends AnyRef { ... }
trait RichIterator extends AbsIterator { ... }
class StringIterator extends AbsIterator { ... }
class Iter extends StringIterator with RichIterator { ... }

Then the linearization of class Iter is
{ Iter, RichIterator, StringIterator, AbsIterator, AnyRef, Any }

Note that the linearization of a class refines the inheritance relation: if C
is a subclass of D, then C precedes D in any linearization where both C and D
occur. Linearization also satisfies the property that a linearization of a class
always contains the linearization of its direct superclass as a suffix.

For instance, the linearization of StringIterator is
{ StringIterator, AbsIterator, AnyRef, Any }
which is a suffix of the linearization of its subclass Iter. The same is not true for the linearization of mixins. For instance, the linearization of RichIterator is
{ RichIterator, AbsIterator, AnyRef, Any }
which is not a suffix of the linearization of Iter.

1 Like

My thesis is: we don’t need traits, if we have multiple abstract inheritance. For the standard library that would mean, some new abstract types (like Iterable) would be defined in Base and attached to the applicable existing classes. Minor effects to existing user code.

@tim.holy the image universary seems to use traits. Is this something that would we possible with protocols?

I personally find traits to be quite difficult to understand. Maybe someone could give an example how “Number” “Signed” … would be expresses in terms of traits.

One nice property of traits is a nice symmetry: I can define a new type and apply an existing (e.g. Base) trait to it, and I can also define a new trait and apply it to existing (e.g. Base) types. Abstract multiple inheritance does not give me that symmetry: I can define a new type that inherits from an existing abstract, but I can’t retroactively apply a new abstract base to an existing type. That seems like a pretty important limitation.

6 Likes

@rdeits I see your point. The problem is, that Julia does currently not allow to inherit from leaf types. Otherwise you could extend an existing class (leaf type) with an abstract type just by sub-classing.
So I should modify my wish-list with

  1. Allow for extension of leaf-types by abstract types (singular inheritance from leaf-types).

After that adaptation, I see no big differences between the “abstract type” and the “traits” approach. I want to preserve the other property of the abstract types, which is the possibility of type parameters.

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