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

I believe the standard answer is that Julia is not an object oriented language but it does have multiple dispatch. The problems most people are trying to solve in Julia are better suited to a functional language than an object oriented language.

That said you can organize your code to look like a basic object oriented language, but you can’t easily do the inheritance tricks that a full OOP language can do.

Truthfully for me as someone who has been in Java software development for a long time I’ve rarely seen many of the “cool” OOP features used in production code. If you are lucky there is just some basic inheritance to help with code reuse, mostly it’s just single classes that do whatever they need to do.

11 Likes

This article might be helpful to you: The unreasonable effectiveness of the Julia programming language | Ars Technica

4 Likes

FWIW, just to demonstrate diversity that exists in the use of any language, I also have many years experience in Java development, but mine does not match yours as described in the quote. Different strokes… I expect the diversity of application of various features in Julia would also be wide.

6 Likes

Many people don’t like OOP programming, it’s is cloying and complicates things unnecessarily, that’s why they choose functional programming languages such as Julia.

4 Likes

In my experience, Julia is more flexible than many OOP languages, such as Java. Traits are one example of the added flexibility that can be achieved with a combination multiple dispatch and Julia’s type system.

8 Likes

From a syntactic point of view, the main difference is if the functions that act on objects are part of the object or not. In some cases one thing is more natural than the other, in other cases it is the contrary. For example:

julia> mutable struct Person
         name
         height
         position
       end

julia> me = Person("Leandro","1.80",0.)
Person("Leandro", "1.80", 0.0)

julia> function displace!(p,Δx)
         p.position = p.position + Δx
       end
displace! (generic function with 1 method)

julia> displace!(me,2.)
2.0

julia> me
Person("Leandro", "1.80", 2.0)

The name and height are properties of me, and that makes sense. The displace! function acts on the the person, changing its position. That also makes sense. In a typical OO syntax, one would call something like

me.displace(2.0)

which is somewhat odd, I think. However, in other cases the natural way of thinking would favor this construct. For example:

julia> function walks!(p,Δx)
         p.position = p.position + Δx
       end
walks! (generic function with 1 method)

julia> walks!(me,2.0)
4.0

julia> me
Person("Leandro", "1.80", 4.0)

The function does the same, but now walking seems to be naturally a property of the a person, thus it could sound natural to call:

me.walks(2.0)

as in typical OO syntax.

Thus, it is not about what you can do, but how natural things look like at the end. In some cases the OO syntax looks more natural, in other cases the functional syntax is more natural. In math, in general, functional forms are more natural, I think, but it might be more natural to write in OO style if you are dealing with buttons, icons, etc, but that still depends on what operation one is doing to the object.

One great thing about the multiple dispatch in Julia is the following. If now I define another struct:

mutable struct Car
   color
   position
end

which also has a position field. You can simply use the same function to displace this car. You do not need to define a new function for that. On the other side, in strict OO programming you would need to define an abstract class of things to avoid having to copy the function to every type of object.

28 Likes

As an experiment, try creating a reasonably complex mutable struct in Julia - this is the thing that mimics the data part of a Java class - and then think about how you would either subclass it in Java or create facade interfaces, whatever - in order to give different behavior to that class under different circumstances. Now, back in Julia, implement the same function multiple times with different variations on the arguments, just as you might do with different Java methods on various interfaces. As a coder, I think you will immediately recognize how simple and straightforward it is to provide context-based behavior to your “class” which is the Julia struct. You can even dynamically build new functions and methods in Julia and execute them. That alone blows away anything Java can do. Embrace the difference!

10 Likes
21 Likes

Following the advice of some other thread I started reading the thesis of Jeff Bezanson. It is an excellent text. And I think that section 3.3 on Dispatch systems will completely answer your question here.

7 Likes

Trying to respond to a number of reactions at once (which may fail :wink:). When I got taught OOP at university one of the main challenges (which was focused on explicitly in the lessons) was how to model your actual problem using the restricted set of concepts that OOP provides. What classes do you need? Which methods should those classes have? Which fields? Should there be superclasses to group common data and/or behaviour? Etc. Once you get comfortable using OOP I don’t really find it too difficult or unnatural to use. Plus, a large number of often-used frameworks and languages are based on OOP so it’s a useful skill to have, as the general software world is very much object-oriented. It’s no wonder that even fairly new languages that have become mainstream, like Swift and Typescript still provide classical OOP features. Interestingly, Rust seems to have diverged by using trait objects instead of inheritance.

It can obviously make sense for certain sub-domains, like technical computing, to be based on a different paradigm, but I feel that discussions of Julia’s strengths often are too much focused on the promotion of multiple dispatch. Yes, it’s more powerful than single dispatch like in traditional OOP. But I don’t consider OOP actually that limited. So multiple dispatch doesn’t immediately provide a clear benefit for me, as it is a trade-off of more complexity and effort to apply, versus being able to leverage existing OOP skills/experience directly in Julia.

Plus, ironically the added flexibility Julia provides in how to model a problem doesn’t make it easier to use, as it will take quite some experience before you can use all that power effectively. How to apply multiple dispatch, taking advantage of the typing system, when to use traits (and through which package), when to use composition versus method forwarding (for simulating OO when needed, or should you simply use Classes.jl), etc. I’ve noticed quite a few posts on the forum here asking how to implement a certain tasks and then often see various answers with completely different (and incompatible) approaches. The variations then go beyond the usual choice in what algorithms and data structures to use (those suddenly appear mundane), as Julia adds a layer on top of those choices.

Even though I studied computer science most of the formal knowledge I got there has been long out of use, as you simply don’t need much of it with languages like C++ or Python, or OOP in general. Therefore the previous paragraphs summarizes the main type of knowledge I’m looking for in further learning Julia. Given a real-world problem/task of a realistic size how do you implement that in Julia, and why using those particular choices? So start from a concrete problem and then show how to use Julia’s power, instead of starting from a formal description of its features and then leaving the actual uses open.

5 Likes

Not really — since you don’t have a solution to eg the expression problem, you run into difficulties all the time. You may, of course, put up with them, like having a pebble in your shoe, but that does not make them go away.

This argument applies to almost all nontrivial areas of knowledge. Eg by the same token, you could argue that calculus does not make things easier, since you have to learn it first. Ironically.

Just look at any of the widely used Julia libraries.

17 Likes

Everything is relative. I’ve never heard of the expression problem, probably did run into it (as you claim you do all the time), but apparently it’s not really an issue in practice. And learning a new language or even switching the majority of your work over to it is always a trade-off between (perceived) benefit versus costs. It might very well be that Julia makes some things much easier, but that doesn’t necessarily imply it’s a good investment of time and effort overall (or in a particular case). There’s a large and diverse software world out there that does not use Julia and that’s not going to change anytime soon.

Which explains exactly nothing on why particular choices were made, or what alternatives could have been chosen.

3 Likes

https://wiki.c2.com/?BlubParadox

In case these choices are not accidental or trivial, usually you can track down the history using git (look at the source using “blame”, find the PR, read the discussion).

That said, what you are asking for is essentially a one-on-one tutorial into idiomatic Julia usage with the twist that you are not interested in making the initial investment (such as reading the manual, or a book like this). This may not exist in unpaid form — YMMV.

7 Likes

If you take a look at some of Uncle Bob Martin’s videos about clean code and programming languages he explains how the single most important aspect of OOP is polymorphism. Julia just chose to use it in the form of multiple dispatch. Inheritance and encapsulation can somehow be emulated, but that’s not the point. Encapsulation and inheritance come in OOP with their own limitations and dangers, and this is why we have interfaces and design patterns.

OOP is restrictive, and it’s more restrictive through the endless guidelines and design patterns that try to instruct the user to be careful, otherwise their library will end up as spagetti code and their dream of maintaining and extending the code will shatter.

Julia’s approach is a bit more liberating from this point of view, as the user doesn’t waste time figuring out if a new method can be added to one class or another, when, in reality it belongs to both with the same weight. This is a very useful approach for numeric libraries where operators like sum can’t be attached to one object more than to the other. (look how Python does away with this:).

Depending on application, one might favor one paradigm or another. Classical OOP make sense for example if one develops a GUI, where the inheritance hierarchy is top down, tree-like. But this doesn’t mean that every library should use this style. It’s just about the right tool for the job.

Heck, other people might simply like OOP just because of namespacing.(although you can’t “TAB” your way through everything).
That’s my 2c.

10 Likes

Didn’t know about that one either :wink: But I’m fully aware that Julia is more powerful and not just “merely a weird language […] about equivalent in power to Blub, but with all this other hairy stuff thrown in”. I’m just having a hard time making a step up the ladder.

Thanks for the book link, wasn’t aware of that one. It looks like it’s exactly what I need, so I just ordered and downloaded it (lots of their books apparently are currently in discount, for only 5 pounds).

7 Likes

I’d say Julia is not really a functional language as you can mutate variables.

Not that I mind that… I took a brief look at some Erlang tutorial, and it allows a variable to be assigned only once. I’d rather not start using Erlang.

1 Like

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.

88 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.

17 Likes