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

For content management systems, the Zope community discovered that Python’s object oriented model was inadequate – so, they created the “Zope Component Architecture”. Equivalently, the same sort of phenomena occurs among Java community’s “Design Patterns”; even walking a tree can be complex in Java, requiring double-dispatch, giving rise to the “Visitor” pattern. In Julia, programming in this way is just normal way of doing things; they don’t require any special user-land interface definition libraries or design patterns, they just work. Moreover, they are fast because they are supported directly by the compiler rather than being added on. It’s for this reason that I think Julia will eventually break into and start dominating in all sorts of business applications. The next popular content management system (the core of any enterprise application), will have an intuitive and performant mechanism for negotiating the cross product of content produces, observers, and consumers based upon multiple-dispatch.

14 Likes

One thing that I will note is that julia-land is not free of these clunky design patterns either. We just need them less often.

Probably the most famous example of such a ‘design pattern’ in julia is the Holy Trait pattern, which can become important in many circumstances where subtyping is not feasible.

Julia’s multiple dispatch and type system are fantastic, but I do think the presence of this trait pattern points to a deficiency in the language semantics, just like how patterns like ‘double dispatch’ and the ‘visitor pattern’ point to deficiencies in Class-Based-OOP languages.

18 Likes

I’ve used the visitor pattern a few times in production code, and my thoughts on it were not that OOP is deficient somehow. Similarly I would expect that other people who come to Julia from OOP will have the visitor pattern in their toolbox. It would help newcomers who know such patterns if better alternatives in Julia would be easily available (documented).
Multiple dispatch can be better, but bashing OOP does not help people migrating from OOP.

There’s a brief window in the early morning after a small piece of chocolate where I think I understand the Holy Trait pattern. However, by afternoon, I’m back to puzzling… I know the problem exists. I’m not even sure I could describe the problem. That said, I did have some unexpected success lately with package interoperability. Both HypertextLiteral and Hyperscript produce objects that are showable to "text/html" – this is what lets them interoperate seamlessly. Both of them, as a fallback, use show(io, MIME"text/html"(), obj) to print subordinates, so it just works. My code was originally doing look-before-you-leap test via showable, but it seems the Julia way is just to leap, and let a “Method Error” appear when something can’t integrate.

I don’t see pointing out deficiencies of older technologies as bashing. We’ve learned quite a bit in 30 years. The earliest that I remember being introduced to object oriented programming is with “cfront” pre-processor in the late 80s, the precursor to C++. It was, at that time, magic. It solved so many problems. I remember having lots of meetings with procedural programmers on my team telling them why we should use data structures where the first element in the structure was a pointer to a function table of methods, instead of having an enum where each function uses switch to pick the correct method implementation. This was a revolutionary idea that solved so many problems. It was the cats meow!

But, technology moves on. Just like using a pointer to a virtual function table as the first element in the data structure was a huge improvement over existing techniques at the time. Certainly those who stuck with older patterns had programs that worked, but, they spent much more time building their programs than those who used “cfront”. It’s the productivity that came with the change that convinced people. The same will happen here.

Let’s get to your specific point. The reason why the “Visitor Pattern” needed to be documented is that, to someone unaccustomed to OOP with a statically typed system, it’s really unobvious how to implement the traversal of a tree with multiple kinds of visitors. Now, with dynamic languages, like Python (that came along about a decade later), and duck typing, it’s less of a challenge. However, the Design Patterns book was really essential reading when it came out.

It’s not that Julia is perfect. It’s just that a working implementation of multiple dispatch is a huge step forward. That said, we really do need to figure out traits… it is critical to having interoperable libraries that don’t know about each other in advance. Julia seems just so close here… but I can’t even articulate what the challenge is. So, in this regard, a bunch of Julia Design Patterns will emerge, but I think these will be focused on different problems.

10 Likes

I didn’t mean to attach a lot of negative judgment with the word ‘deficiency’. I didn’t mean to imply that people shouldn’t use the visitor pattern or double dispatch or any of these other techniques. I’m also a big fan of Holy Traits and I often advocate their use, they’re not really any different from things like the visitor pattern.

All I meant to say was that these design patterns are sort of clunky ways to do something that the respective dispatch systems aren’t great for. That’s what I meant by deficiency, as in, they’re a little deficient at succicntly and systematically expressing these concepts and instead require the programmer to roll it out by hand (or rely on a package to kinda do it), rather than just being a natural part of the language.

I’m not trying to help people migrate from OOP. I only brought this up to discuss a place where Julia has a problem analogous to what people above claimed plagued class based languages. My comment was about improving julia, not instructing people on migrating from other languages.

4 Likes

I wrote a little about the idea behind traits here, not sure if that’s helpful. Maybe this blogpost by @oxinabox is helpful.

The problem with explaining traits is that it’s really hard to find a small self contained example of why they’re useful. For any small snippet, defining a supertype is just better. Traits really become useful when you have multiple packages interacting that don’t want to all share a supertype, or in cases where you really want a type to have two supertypes.

4 Likes

OMG, I thought I was the only one! :slight_smile:

9 Likes

This is a very close description of my experience with Monads in Haskell. I remember until today that for a had an epiphany and for one afternoon I understood what Monads were really about. Then it was gone, not completely, but a shadow of what it was. Remembered me of some RPG systems, were it is implied that wizards study so much because the act of casting a spell erases it from the wizard’s memory.

7 Likes

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