Is Julia's way of OOP superior to C++/Python? Why Julia doesn't use class-based OOP?

One thing that I would be really interested in is if we could make a language that only had structs, functions, and traits (and keep multiple dispatch). If you can keep multiple dispatch without subtyping (only using traits), it might be a really interesting way of structuring a language.

5 Likes

I believe that the current language design would allow someone to experiment with this. All of these facilities are available. Would less be more? Or would less be less?

Yeah, I had been thinking of trying to make a type class system for a while. I think in theory, it wouldn’t be too hard to do if it was meant to be it’s own separate ‘typeclass based ecosystem’, but if it has to interface with regular julia code things could get pretty hairy.

I’ve been meaning to learn about typeclasses for a while, I only know the basics, so implementing them in julia would be an interesting way to approach that. I know @HarrisonGrodin has thought a fair amount about this.

4 Likes

I think you have it:
“Traits become really useful when you really want a type to have two supertypes”.

The very useful feature missing in Julia is the “interface”: you declare an object to have a property,
or a bunch of closely related properties, which are the condition for a bunch of methods. Sometimes
these properties are associated to the presence of certain data field in the object. Then you can use composition, embedding in your object a struct with the field to which you associate the corresponding methods. Sometimes you cannot, because the property depends on a data field in your object which is already used in some abstract type hierarchy. If you could have a struct be a subtype of two different abstract supertypes, that would be a solution. But “interface” could be a more limited feature and still work.

The trait pattern is a kind of double dispatching: for each method concerned with the trait, you first add an argument, whose sole purpose is to call again the method with one more argument (the trait) so that the desirable dispatching occurs. I consider it thus as a kludge (even though very necessary in the current state of Julia). It has for me a big inconvenience: even classes which logically need not be aware of a trait must become aware of it. For instance if you have an abstract type foo which defines addition + on its instances and find that certain objects in some subclasses can use a more efficient algorithm for that, and decide to define a trait for that, then all these subclasses as well as the class foo must become aware of that (by having their ‘+’ method traitified) while there is no logical need for that.

9 Likes

Ok. It’s morning, I had a bite of chocolate, and I read the materials you presented, again.

  1. Traits are a way to categorize datatypes at compile time to choose a method implementation.
  2. Traits can be declared independently of the datatypes that it categorizes.
  3. Additional trait categories can be added retrospectively; but, a type can only have one category, so some coordination between the owner of the type and the owner of the trait should be negotiated.

A somewhat related question, is showable(mime, obj) compiled away? If not, perhaps it could be rewritten to be compiled away? This definition seems to look like it’s deliberately runtime.

showable(::MIME{mime}, @nospecialize x) where {mime} = 
   hasmethod(show, Tuple{IO, MIME{mime}, typeof(x)})

I was wondering the other day, why showable wasn’t defined…

showable(::MIME{mime}, ::T) where {mime, T} =
   hasmethod(show, Tuple{IO, MIME{mime}, T})

Anyway. If the Holy Trait pattern is really settled law, perhaps it should be given specialized syntax so that those wishing to using traits could do so without worrying about the exact technical implementation? I’m reminded of original beginnings of C++. You don’t need C++ to be object oriented, you can do this by having the 1st element of a struct point to a table of function pointers. Moreover, the types of the functions in that dispatch table can be declared statically. It’s just that class keyword and the compiler does this magic for you, by maintaining the function table (and mangling the struct name); the pointer of which is, by convention stored at *(ptr - sizeof(void *)), sort of like strings that store their length before the string starts. But, there’s mental overhead to remembering how it all works and you have to look carefully to ensure that someone didn’t do something… clever. By having a specialized “syntax sugar” that is handled by the compiler, you reduce the cognitive burden of using the pattern, and make the usage/understanding of traits more broadly accessible to Julia programmers.

Is it premature to formalize traits into a syntax?

Perhaps the explanations make it seem more complicated than it is. All the pattern does is compute a new argument from one of the arguments of a function and then dispatch on the type of that new argument. The “trick” is that even though you can’t dispatch on values in Julia, you can derive a type from a value and then dispatch on that type. In a static language you wouldn’t be able to do this but it’s not a problem in Julia.

10 Likes

Yes, traits are just an application of the technique of double dispatching. Similarly to the fact that double dispatching can emulate (poorly) multiple dispatching in languages which have only single dispatching, traits can emulate (poorly) interfaces in a language which has only single inheritance from abstract types.
Why don’t we fix the language instead of being satisfied with a poor emulation?

3 Likes

I’m not sure that multiple inheritance is a feature (in my experience, it introduces more issues than it solves). Moreover, a characteristic of traits is that they can be added by 3rd parties. It might be that the Holy Trait pattern is actually what’s needed. What might be lacking is just a bit of sugar & documentation?

5 Likes

I said what’s missing is the feature “Interface”. The most radical way to have this would be allow multiple inheritance from abstract types; but perhaps this is too complicated and difficult and would
introduce some other can of worms. It could be that a much more restricted feature is sufficient.
But traits as they are do not fit the bill.

To come back to my example, suppose some objects could have a more efficient algorithm for addition. For example, suppose in these objects the field coeff used in addition is already sorted, so there is no need to sort it prior to addition as in other objects. To express this as a trait you do

abstract type CoeffSortedStyle end
struct CoeffSorted<:CoeffSortedStyle end
struct CoeffNotSorted<:CoeffSortedStyle end

Then comes the bad step: you need to make your supertype foo aware that there is a trait below it (of which there is no logical need):

CoeffSortedStyle(X::foo)=CoeffNotSorted
Base.:+(x::foo,y::foo)=+(::CoeffSortedStyle(x),x,y)

and only then define what you really want:

Base.:+(::CoeffSorted,x::foo,y::foo)=# routine which takes in account that coeff is already sorted

In a language with interfaces, you would do something like

Interface CoeffSorted end
Base.:+(x::{foo,CoeffSorted},y::{foo,CoeffSorted})=# routine which takes in account coeff is sorted

and that’s all. No need to interfere with addition for other subtypes of foo. I do not think any clever macros can bring us there without some change to the language. But I would be happy to be contradicted on that.

2 Likes

That is a very good explanation. I have used the trait pattern in designing an interface, so I can use it. The problem for me is that it is hard(er) to form a visual picture of what is going on in the code. It is the indirection that obfuscates things, I think.

1 Like

I would not say that traits a “bad emulation” of interfaces. I think they are distinct things. Interfaces care about a type conflating multiple concepts (it is both a vehicle and a weapon, for example). The traits care about allowing some decisions to be lifted to the type space (and the trade-off between compilation time and run time).

Julia already has interfaces (at least the type of interfaces I know). You can define a new module with many empty (or fallback) functions inside it and just say that for a concrete type to implement that interface the module functions (or a subset of them) must be extended for the specific type. Any type can implement any number of interfaces this way. Code that works over such interfaces only need to know the interface module, use its functions inside it, and do not restrict the type of its parameters (allowing any type, regardless of type hierarchy, to work if it has specialized the right functions).

Maybe I am missing something, or the terms are using another definition that I am not familiar with, but does not seem to me that Julia has a problem with implementing interfaces.

2 Likes

I tried to give a specific example to show what I meant by the terms I used. In your notion of “interface module”, how do I know that my object has sorted coeff so the corresponding module is applicable?

2 Likes

Traits can do this, and a whole bunch of other things, so they are more general than “interfaces” as they allow a lot of general computations to happen in type space.

You would have to pry traits from my cold, dead fingers. :wink:

5 Likes

Tamas Papp. To be completely clear, I should not (I do not) imply that traits can only be used to
make a poor emulation of interfaces. Once you think (cleverly like Tim Holy ) of such a trick of course you should use it as far as you can. But the fact is that a poor emulation of interfaces is the main use of traits in Julia at present.

2 Likes

What’s “poor” about it?

I spelled that out in detail in the specific example I detailed above. My longest post in the thread.

If you are referring to

I consider this an advantage: given the trait and a generic API, I could do other things, eg reuse the method by abstracting out what I need from foo.

In any case, I don’t think this is a big inconvenience — which is why probably no one bothered with implementing interfaces since traits became widely used.

I do not recognize this feature as part of interfaces, but ok. You can have it three ways:

  1. If this distinction was already foreseen at the time the interface module was created, then you can just create a function that asks for this bit of information.
  2. If this trait was not foreseen, but the interface module author decided to have some precautions, then a generic function may be created just to answer questions about possible traits. Other modules/packages can create new types representing the specific traits, for which the generic trait-answering function will be specialized. Now to know about some trait you need to know about the original interface module and the extension module that defines the trait. The fallback of the trait-answering function is returning false or some other sensible value (that will mean the type is not aware of the trait).
  3. If the original interface author has took no precautions (the worst case), the only change is that the generic trait-answering function will need a package/module of its own, maybe already populated with some trait-types (the specializations of the trait-answering function better left to the modules/packages that have created new concrete types that implement the interface, so there is no type piracy).
1 Like

Well it is a big inconvenience. Suppose you are not the author of the package defining foo. Then
it is clearly a problem to have to interfere with + in all subtypes of foo.

I think the type piracy rules apply here — if you don’t own a type, don’t define traits for it. Why would you even do that?

1 Like