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

Would a tail-recursive function like they write in Elixir/Erlang be efficient in Julia?

1 Like

Julia doesn’t have tail call elimination so if you recurse too far you will overflow the stack.

5 Likes

Despite the fancy name, the expression problem is definitely a stumbling block you’ve encountered many, many times in object-oriented programming, wether you knew what it was or not. From the OOP side (as opposed to the functional language perspective, where it is also a problem but looks quite different), it is simply the problem of trying to add a new operation to existing types. For what it’s worth, I go through this in depth in the video I linked above, but some people prefer written explanations. The core problem with class-based OOP is that methods live inside of classes and everyone has to agree on what methods make sense to add to each class. You may think, “What is the problem? I have added methods to classes all the time?” In your own code that you fully control, sure, if you want a new method, just open up the class definition and add the method. The problem occurs when, as happens in the real world, there are many people sharing and reusing code. Which, you will note, is one of the main problems that OOP is intended to solve.

Suppose, for example, that I maintain a package like ColorTypes but in a class-based language like Python or Java. Now further suppose that you want to treat each color object like a 3-vector (or however many channels it has) and do vector operations like addition, subtraction, and taking norms. You don’t even insist on using operators like + and -, you’ll happily write color1.add(color2), color1.sub(color2) and color.norm(). You try to convince me to add these methods to the Colorant abstract base class. However, I don’t want to add these methods: I don’t want to take on more features to maintain and it’s not entirely clear to me if/how colors should behave like vectors because perceptual color space isn’t linear in terms of various representations like RGB, CMYK, HSV, Lab, XYZ, etc.

Given my refusal to add methods to ColorTypes, what can you do? Traditional OOP would tell you to define your own subclasses. Problem solved! Right? Well, let’s think it through. The implementation is not as simple as it sounds. You want to add methods to Colorant so that all the color types inherit the new methods you add and any generic operations you define work across all color types. So you subclass VectorColorant <: Colorant. But now how do you get all the corresponding vector versions of specific concrete subtypes like RGB, HSV, CMYK, etc.? You have to define your own parallel type hierarchy that mirrors the one in ColorTypes. If your language supports multiple inheritance (Java does not, Python does), you can define VectorRGB <: (VectorColorant, RGB), but you have to do that for every possible subclass — even ones defined outside of ColorTypes. This isn’t great: the number of definitions you have to write scales with the number of concrete subtypes, but at least each definition is trivial.

If your language does not support multiple inheritance, then you’re pretty much stuck: you have to copy the definition of each subtype but inheriting from the Vector-prefixed variant of what the original type inherited from. At this point, you’ve copied the entire class hierarchy aside from the topmost abstract base class, which probably doesn’t have much in it anyway, so you might as well just make your own copy of the whole thing into your own separate VectorColorTypes package. You also fix a few annoyances with the original code while you’re at it. After a while the upstream ColorTypes package makes a few changes that you haven’t kept up with, and the ColorTypes and VectorColorTypes packages start to diverge. They’re still similar, but unless you both try really hard to keep them in sync, they gradually become more and more incompatible. OOPs! :grimacing:

That’s not even the extent of the problems. Suppose you’re lucky enough to be in a language that supports multiple inheritance and you created a mirror hierarchy of trivial Vector-prefixed types without completely parting ways with the original ColorTypes package. Now you encounter some really neat palette method that someone wrote using ColorTypes that takes a single RGB value and produces an entire palette of harmonious colors based on it. But you want to use it with your VectorRGB type. You can’t use it directly, however, since the methods are defined specifically for the RGB type. Even though VectorRGB has the exact same structure as RGB and even supports all the same methods that do all the same things (remember, I only wanted to add methods, not change the existing stuff), they are different types and not interchangeable: if some method returns RGB values, you cannot apply methods for VectorRGB objects to them. You have three options:

  1. You can petition the author of the code to change it to handle VectorRGB as well as RGB; they many not want to do this, since it’s an unnecessary complication.

  2. You can create a shim method for VectorRGB that converts the VectorRGB value to RGB, calls palette on it and then converts the returned colors back to VectorRGB. This works, but you have to do it for every function like palette.

  3. You can copy the code and duplicated it for the VectorRGB type and part ways with the original author.

None of these is that bad, but you have to do this for every bit of ColorTypes functionality that you might want to use. It’s also funny how OOP, which is supposed to be this great enabler of code reuse and sharing, keeps pushing us towards just making our own copy of someone else’s code and modifying it :thinking:.

The palette method is actually quite simple, so adding support for VectorColorant subtypes isn’t that complicated. In more complex situations where color types are consumed or constructed, adding support for different subtypes leads to a proliferation of complex OOP patterns in real world code bases, like the visitor pattern, the abstract factory pattern, the factory method pattern, the prototype pattern, etc.

How does all of this work out in Julia with multiple dispatch? All you have to do is define methods like this in your own code:

add(c1::RGB, c2::RGB) = RGB(c1.r + c2.r, c1.g + c2.g, c1.b + c2.b)
norm(c::RGB) = norm((c.r, c.g, c.b))

That’s it. You don’t have to convince anyone of anything. You don’t have to copy their code. You can add methods at whatever level of the abstraction hierarchy is appropriate. It doesn’t even matter if other people want to use the same names for methods because if you define an add function in your code and they define a different add function in their code, these are separate function objects in different namespaces and there’s no issue at all. In OOP, the namespace of methods is global and shared, so method name collisions are a problem. The key reason all this is better with multiple dispatch is that, unlike OOP where methods are defined inside of classes, methods belong to generic functions and are defined outside of the types they apply to — potentially not even in the same code base. Each user of a type can add methods as they please, which other users can reuse or ignore as they prefer.

So even if you never realized you encountered the expression problem because you didn’t have a name for it, you almost certainly have. That time you wanted to just add one teensy tiny method to someone else’s class but couldn’t. That was the expression problem. That time you discovered that your favorite OOP language’s ecosystem has two or more non-interoperable packages for some subject and you need some features from both of them but can’t use them together. If not for the expression problem, they could share a core set of simple types while extending them with different methods on top of that shared core. That time you were tearing your hair out trying to understand what the SimpleBeanFactoryAwareAspectInstanceFactory class is and how the heck you’re supposed to use it. That was also the expression problem: if people weren’t forced to subclass just to add a few methods, they could just share concrete types instead of needing so many layers of abstraction. Despite seeming so different, these are all manifestations of the same core problem in OOP languages, a problem which multiple dispatch solves simply and elegantly, thereby eliminating all these issues at once. The effectiveness of this solution is borne out by the unusually high degree of code reuse and sharing we actually see in the Julia ecosystem.

91 Likes

Lisp is the original functional language and allows both mutation and rebinding of variables. Lisp and Julia are both functional languages, just not purely functional. The criterion for a functional programming language is good support for functions as first-class objects — you need to be able to pass them as arguments and return them from functions. That allows patterns like map, filter, etc. Some would also require support for lexical closures, which Julia also has.

16 Likes

This wikipedia page is a good illustration of how hard it is for a language to add multiple dispatch afterwards. I have to say I’ve seen some of those examples in real life.

2 Likes

What if Julia would officially support Multiple dispatch way and OOP way officially?
Both pros and cons would be solved… :slight_smile:

(Although I found power of multiple dispatch reading materials and easier to read write and collaborate but … )

OOP is just single dispatch. Just dispatch on the first argument. Done.

14 Likes

What benefits does the OOP offer that multiple dispatch doesn’t already solve?

7 Likes

OOP from my point of view means that you can add properties to classes and inherit them… But you can do this with Julia already…

3 Likes

I have nothing problem with it :slight_smile: .

I’ve one silly problem that i am looking for , like in classes we use like class. and hitting tab gives all methods interactively but in Multiple dispatch way.
I’ve to use a function which prints all methods at once and not interactive. As we have in julia,
methods(...) I am thinking a way to get interactively those methods like auto completer .
This is Is there Way to know methods/functions of classes(modules/functions) - #5 by vedasulo and I hope that alot of innovation have done, this would come in future :slight_smile: .

16 Likes

Exactly this I was talking :slight_smile: .
It seems these have been merged. I hope i will see this using some extensions in ide’s .

When could i see it in action? :slight_smile:

a = [1]

giving a all autocompletion interactively or tab . and it was added 12 days ago:) , I am waiting.

Thanks…

No, it’s not merged yet — that’s why the PR is listed as “open”. Even when it is merged, you won’t see it until Julia 1.7.

1 Like

:slight_smile: I hope will see soon.
Thank you @stevengj :slight_smile:

Two words: Type Piracy.

Those are indeed two words.

20 Likes

While it’s often useful to be as terse as possible when making a point, I think using just ‘two words’ didn’t convey what you wanted it to. Could you explain why you think those two words are relevant?

3 Likes

That example was not type piracy. It defined a new function which would be scoped to that module. For example SomeModule.somefunction() and OtherModule.somefunction() are two different functions which happen to share the same name, not type piracy.

Ah, just realised that the eBook is on sale for 5 Euro :see_no_evil: thanks for mentioning it!

4 Likes

Won’t it look basically the same in languages such as python? As I understand, the power of multiple dispatch comes not from just the ability to define new functions operating on existing types - this is possible in the majority of popular languages. In fact, your example doesn’t need dispatch at all, if it doesn’t do piracy:

def add(c1, c2):
    return RGB(c1.r + c2.r, c1.g + c2.g, c1.b + c2.b)

def norm(c):
    return np.norm([c.r, c.g, c.b])
2 Likes