Why there is no OOP (object oriented programming) in Julia?

I agree with you after reading the thread you mentioned.

And the difference is that overloading is just at the compile time and it may produce a lot of version of code, which may not be used.

In multiple dispatch this is decided based on JIT compiling. So, the compiler may produce just a single version of code rather than all versions.

I think this is the beauty of JIT compiling to make more efficient code.

2 Likes

Thanks a lot for the response.

I completely understand what you mean.
What I was trying to say was that OOP function-class structure can be included in multiple-dispatch without any issues.

The bottom line is that:

  • if a function is included inside a class, it means it is going to be used by a lot of objects instantiated from that class. So all those object will share the same machine code for functions/method. This machine code will receive different object addresses to access different parts of memory but the same action or machine-level operation will be done on data stored in those addresses.

In multiple dispatch different machine codes are produced for processing different input arguments. And this is decided on the first run of the code.

So this is the reason why I am saying multiple dispatch is not in contradiction with OOP and is a superset of it, in the sense that when one uses OOP, one wants to create a lot of objects of those classes and it is good that they all share the same machine code.

So this surprises me why OOP is removed in Julia.

OOP (in the traditional single-dispatch sense), was never removed from Julia, because it was never added to Julia. Note, in Julia everything is an object, in some sense, unlike in most languages, such as Python and C++.

People do not agree on the definition of OOP, the class-based OOP of Python or C++, or OOP as meant by Alan Key who coined the term for Smalltalk. Either way this talk is relevant (IMHO the most important programming talk I’ve seen, at least look at the first 2-6 min.):

OOP wasn’t meant for speed (runtime speed that is, maybe for programming speed, but it’s also doubtful that holds up).

C++ is one of the most-performance-obsessed languages out there, with Bjarne Stroustrup claiming, if I recall, OOP is something you DO pay for (in runtime speed), if you use it. I tried to find the source for it, at least I found this interview:

https://www.stroustrup.com/slashdot_interview.html

9) C++ complexity vs. OOP simplicity
by hanwen

[Sorry for the potential inflammatory matter in this question].

How do you relate the complexity of current C++ with the much-touted simplicity of Object Oriented Programming?

Longer explanation: […]

C++'s evolution was motivated by a few mottos (you don’t pay for what you don’t use, C compatibility, etc.)

Bjarne:

[…] I was not a contributor to commercial OO hype. […]

If you do want there’s:

ObjectOriented.jl is a mechanical OOP programming library for Julia. The design is mainly based on CPython OOP but adapted for Julia.

2 Likes

As stated many times by Alan Kay, who created the term OOP, there is a common confusion between OOP and Abstract Data Types. See his answers below, where he just scratches the surface on what “real OOP” is about:

1 Like

My first comment was exactly about how mixing (Pythonic) OOP and multiple dispatch causes issues, so perhaps you don’t understand what I mean. In fact, I was downplaying how problematic it gets. Maybe it’s easier to see in an imaginary language with Julia’s multimethods in Python’s classes, let’s call it Pulia (Jython is taken). Multiple dispatch is intended to work just like in Julia, but the first argument (self) is specially tied to the class. Sounds reasonable, right? Well let’s try a simple case: 2 classes/types, and the multimethod dispatches on 1 or 2 arguments with those types:

# code                # intended usage
class A:
  f(self) = "A"       # A().f() 
  f(self, ::B) = "AB" # A().f( B() )

class B:
  f(self) = "B"       # B().f() 
  f(self, ::A) = "BA" # B().f( A() )

Looks good, so what’s the problem? Well, the f(self, ::B) line throws a UndefVarError: B not defined. Well okay, we can just move class B to the front then, right? But then f(self, ::A) throws the error. This circular dependency does not happen in Python because there are no formal type annotations.

So what do languages with both classes and type signatures do? In C++, you use forward declarations, in this case you could add a class B: pass line at the start. However, when it hasn’t been defined yet, you can only use pointers to B. Neither Python nor Julia have pointers as a core language feature, in fact they actively avoided it.

Well okay, we can still make this work. The problem is that the method was defined too early. We can just add it to the class after the fact:

# A must be defined before B
class A:
  f(self) = "A"

class B:
  f(self) = "B"
  f(self, ::A) = "BA"

A.f(self, ::B) = "AB"

Well now that methods don’t have to be contained by the class definition, why not define all the methods outside after the classes? Then we don’t have to define our classes in a particular order:

# A and B can be defined in any order before the methods
class B: pass
class A: pass

A.f(self) = "A"
A.f(self, ::B) = "AB"
B.f(self) = "B"
B.f(self, ::A) = "BA"

And now we have effectively separated data and methods. It really is just cleaner for multimethods, you don’t have to fight type definition order, you don’t have to introduce pointers. If the classes A and B are defined in separate modules/files, you don’t have to watch out for circular imports and are not forced to split f across those modules/files.

This also shows a limitation of classes encapsulating multimethods. If you want to look for methods that take in B in the Pulia example above, you probably want to find A.f(self, ::B), but you would have to search the class A instead. That’s not very feasible to figure out in more complex code.

7 Likes

A practical advantage of single dispatch is the ability to discover an object’s o methods by typing o. plus tab in modern IDEs. I imagine that the same could be achieved for Julia via the vscode extension if the same keyboard trick (o. + tab) could show the result of methodswith as a standard OOP method list. Once a given method is selected, fetch the Julia method call with the o argument at the proper position (since o is not necessarily in first position).

8 Likes

Good call, methodswith can find methods of any functions, while methods restricts me to 1 known function. Although, I don’t think the instance variable o is what should be searched for, but rather typeof(o); no name would work because arguments annotated with that type can have different names e.g. f(x::Integer), f(y::Integer, x).

This still is not as clean as single dispatch though. When the argument can be in any position, that list of methods and functions gets really large. Moreover, you probably want to set supertypes=true so methods with abstract annotations also show up, but then there’s no guarantee that the method you spot is what gets dispatched. For example, maybe you select add(x::Number, y::Number), but the call add(1, 2) is dispatched to add(x::Int, y::Integer) instead. Multimethods have their tradeoffs.

I think that I was not very clear…
I place myself in the case I have a type annotation for a given variable.

struct MyStruct
   data::Float64
end

my_add(a::MyStruct, b) = MyStruct(a.data+b)

function a_func_in_dev(x::MyStruct,y)
      x. #+tab

I I type x. + tab developping a_func_in_dev the field data is proposed. I thought that the method my_add could also be proposed.

Yeah that makes sense, though actually I wouldn’t think there is a need to tentatively annotate the source code, which actually affects its dispatch. All you have to do is +tab MyStruct tab or something, input the expected concrete type by hand.

I still think the bigger issue is sorting the methods so you get more specific ones like add(x::Int, y::Integer) ahead of add(x::Number, y::Number). Maybe this issue is actually a lot more solvable than I’m thinking, but it’s just that normally, this is figured out by dispatch, which requires an entire function call.

On the other hand, I’m not sure how necessary this is. Even in Python I never inspected class methods in the middle of coding. I read the documentation or the source code to learn what I could do, and then I coded what I knew.

1 Like

I guess that it depends on your programming task.

I currently work on a case where I connect generic optimization algorithms with “industrial” values with super specific types having a lots of field with long and complex names (x is a value of SpecificType with a lot of fields like x.super_specific_feature).

The connection between specific values and generic algorithms is done by adding new methods to generic functions like

function node_number(x::SpecificType)
     nodes = specific_function_computing_nodes(x)
     return length(nodes)
end

Although I already know about the method specific_function_computing_nodes(x), it is tedious to type and I could also be wrong about x type and I would like to know it as soon as possible (while typing).

Note that in my case, I do annotate the x type for dispatch purpose.

In C++, it comes for free and I though that I could get the same in Julia.

I agree that the possible methods should be sorted from the more specific to the more general.

1 Like

Function name autocompletion and static type checking where possible, then, both good features of a linter. Still, this will be a lot less comprehensive than statically typed languages like C++. For example, if x was annotated with an abstract type, or if a specific_function_computing_nodes(x::Any) spuriously allowed any x even if it would cause some error in some downstream methods. The more your code happens to be annotated with concrete types, the better the linting would be, but sometimes that just needlessly duplicates code.

1 Like

Totally agree. So it would be restricted to concrete types. Sufficient for my purpose :wink:

This really depends on what you are doing and scale and complexity (and how many people you work with). In more recent versions of Python there are lots features with typing which allow you to annotate a type. This gives you very good (mostly perfect) autocomplete and is a massive productivity boost as all the documentation is there when you type, reducing context switching.

I am unsure if it is even possible to achieve a similar quality of autocomplete in Julia, due to it’s design, but steps in that direction can have huge productivity boosts, especially when on a very large scale project.

Having the help in the REPL is very helpful, but it feels rather clunky when compared to autocomplete from other languages like C# or now Python (with typing).

6 Likes

Yeah multimethods really just complicate this. With single dispatch, you could search a vector of classes given a function name, and with class encapsulation, you search a vector of functions given a class (latter is done in Python with dir). With multiple dispatch, you get a (#number of arguments)-dimensional array of type signatures given a function name, and (who knows)-dimensional array of methods that may take some given types as arguments with unknown positions.

I think if you have a given function name, it’s more feasible to have name-autocompletion and a docstring panel than to list all the methods; being written by humans, the list of docstrings is often much simpler than the raw method table. That said, if someone figures out how to specify partial input types to interactively prune the method table or the list of docstrings being shown, that would be very cool.

On the other hand, if given a type name, I’m just not sure how far that can go. The closest thing to Python’s dir would be going up 1 type’s abstract type lineage’s docstrings which may link to interfaces, very far from finding all applicable functions. However, methodswith possibly extended to multiple given argument types would give a massive list of functions and even more methods, most of which you wouldn’t even be close to considering. Overloaded global functions in other languages would also have this issue. Python’s dir is only feasible because you’re restricting yourself to searching functions in a class and its superclasses; Julia’s equivalent for encapsulation is a module, but a module is much larger in practice than a class.

1 Like

Indeed, I think the idea of pruning, or at least ordering the list would be essential for it to be useful. In C# (I use as an example of amazing intellisense/autocomplete), the suggestions are smart and use a learning method on source code to highlight the most appropriate function. The learning, I think, is based on open source projects, and your own codebase.

I think a similar approach could be used in Julia to rank the most applicable functions together with some function name search to narrow down the options.

This could be a very interesting project to pursue, to see if it is even a viable (by which I mean something that helps productivity).

1 Like

Hi
sorry for the late reply.

The difficulty as I mentioned is that the data and the function that acts on it are now separated. It is as if data is just thrown in the middle and it is not known who should care of which part.

This may not be a big issue in computational sciences such as physics, mathematics, etc. because the goal is to just compute something but in software engineering just the naming of the entities and the interface between them is of huge importance.

For example, let’s say a machine learning task:

  • dataloader: is an object that works on data and divides it into smaller pieces on which training can be done.
  • network: takes the data and processes it to produce the output.
  • optimizer: works only with parameters and updates them according to their gradients.

Of course, one can merges all of them and write a mixed program but just dividing these tasks and the fact that each entity does part of the job with its own data is very important. For example, a dataloader does not care about parameters and an optimizer does not care about how data is fed into the network.

Of course, one can still definitely implement all these in Julia but without classes this is difficult.

On the contrary, your own description of the tasks suggests functions that act on data as inputs, which is the natural Julia way of doing it.

3 Likes

I encourage you to play around with the language for a while. Look at Julia code on Github for established packages to get an idea of how Julia packages are written. After you’ve had some time to learn and understand the Julian style of coding, maybe things will make more sense and you won’t miss OOP quite as much.

That said, discoverability of code dependencies in Julia is sometimes more challenging than in other languages. It’s one of the tradeoffs in language design—it’s a consequence of choosing a language focused on multiple dispatch.

4 Likes

No it is not difficult at all.

Use a module.

Divide your large program into separate modules, each module is responsible for doing one and only one specific task. The API to the module accepts the data structure that it needs to do its work.

Mixing data and functions together into one container is actually bad. Data/state and functions should be separated.

search for “Modular based programming” for more information. Here is a link Modular programming - Wikipedia

Not knowing what to do with a particular type and not being able to find out would actually be a massive issue in computational sciences, too. I think part of the issue is you’re overestimating how much class encapsulation helps discoverability. A class only helps you find methods that take its instances as the 1st (self) argument. It doesn’t help you find non-class functions that take it as input, and other classes can encapsulate methods that take it as the 2nd or more argument. For example, extending addition functionality for the builtin int type must be done in the __radd__ methods of other classes, and inspecting the int type does not help you find those.

So even in Python, there is a large need for other ways of discovering what to do with a type. Often that comes down to module-wise documentation. Since Julia only has modules for encapsulation (I showed earlier how class encapsulation hampers multiple dispatch), there is a strong culture of module-wise documentation; every package is at least 1 module, and I have not seen any released package lacking an API references page.

To be fair, Python and many other languages also has such a culture. Even Python often has module-encapsulated functions not belonging to any class, and they’re documented just as well as any Julia function. In these cases, the discovery is reversed; you’re learning what types can be inputted into a given function. This must also be done for the 2nd argument onward in class-encapsulated methods. In Julia’s case, you can also use methods(f) to inspect the runtime method table, while Python must rely on documentation.

3 Likes