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

I am coming from Matlab and Python background and I have recently started considering Julia seriously for software development.

My main question is why Julia is not designed as OOP like python? As far as I can see OOP is not against speed and multiple-dispatch but in contrast it allows a very clean division of code into classes, etc.

This may not be a big deal in mathematics and computation but it is quite necessary in software development because it encapsulates functions and data in the same place.

Of course, Julia has constructs but they are very difficult to use. And there is always a distance between the data and the function that acts on and processes the data.

This was a big turnoff for me about Julia.

1 Like

You will find (of course) long discussions about this topic. For example here: Is Julia's way of OOP superior to C++/Python? Why Julia doesn't use class-based OOP?

The paradigm of Julia is different, and it is natural to feel some frustration being used to something different. But once we get used to it, it’s nice and we miss it’s features the other way around as well.

That said, this package provides a fairly comprehensive support for OOP: [ANN] ObjectOriented.jl (renamed from TyOOP.jl), complete OOP support

16 Likes

Can you explain a bit more what you found to be difficult ? Julia does things a bit differently but it’s not supposed to be harder (for most things anyway).

1 Like

OOP kind of doesn’t work well together with multiple dispatch, and we can just look at Python to see that. Base Python only has single-dispatch classes e.g. you dispatch f on the a in a.f(b, c, d), but there are third-party libraries for multimethod decorators. If you look at code examples, you’ll notice that most are just functions e.g. g(a, b, c, d), not methods in classes. That makes sense from a design perspective; if a function g is dispatched on all arguments a, b, c, d, g doesn’t really belong to any one of them. At least one library allows you to split a multimethod across methods in separate classes, but it sometimes doesn’t dispatch on the class. Another reason to avoided mixing classes and multimethods is if the classes were defined in separate files, you lose the option of writing the methods together in one file that imports the argument classes.

Granted, writing method definitions next to each other isn’t the same as class-based encapsulation. It’s a lot easier in Python to find the methods that dispatch on a particular class, whether by reading a source file or inspecting the runtime class object. In Julia, the methods can be scattered across multiple files, though inspection is still possible. The multiple methods belong to the function itself and can be listed with methods(g); in fact, Julia dispatches on the function’s singleton type, so in a weird way you could say the function’s type encapsulates all the methods. If the function was written to informally imply the first argument to be most important, similar to how Python’s classes are designed, you can actually find all the methods for a particular first argument type methods(g, Tuple{ClassA, Vararg{Any}}); if you also take care to write all such methods in one file next to the ClassA type definition, that’s as close as you can get to (yet still far from) the organization of class-based encapsulation.

6 Likes

TBH, I’m glad that Julia did not adopt the ubiquitous OO paradigm.

Admittedly, I started out with Fortran and OO has always been alien to me. Or rather the concept that methods have to belong to classes. I always found that restricting. Multiple dispatch is, I feel, the better concept.

In Python I often see my students not really using OO, but getting frustrated that they can’t simple use autocompletion to discover all methods that are available with the object/datatype. What @Benny describes in his comment. But then all their code is not really OO at all.

So I guess it depends where you come from. If one started with (and possibly all one has seen is) OO, then it’s difficult to think in a different way. If one started with Fortran/C, then the OO paradigm will be difficult to get.

5 Likes

“There is no Self”

The Buddha

26 Likes

You might enjoy Stefan’s presentation:

which compares Julia with class-based object-oriented languages.

7 Likes

Most of the necessary encapsulation of code can be done using modules and submodules (Modules · The Julia Language). This allows you to clearly segment code into different sections, and essentially operate just like a namespace.

I usually rely on modules for auto-complete as well, but it often isn’t as reliable as in OOP languages.

2 Likes

Regarding encapsulation, here’s how Wikipedia defines encapsulation:

In object-oriented programming (OOP), encapsulation refers to the bundling of data with the methods that operate on that data, or the restricting of direct access to some of an object’s components. Encapsulation is used to hide the values or state of a structured data object inside a class, preventing direct access to them by clients in a way that could expose hidden implementation details or violate state invariance maintained by the methods.

Julia provides neither bundling of data with methods, nor restricting of direct access to object components (i.e. struct fields). However, Julia does encourage using interfaces to interact with objects, rather than directly accessing object fields (although direct access of fields can be incorporated into an interface). Examples of interfaces that are defined in Base Julia can be found here. Using interfaces achieves what I consider to be the most important goal of encapsulation, which is to hide implementation details and ensure that invariance properties of data objects are maintained.

There’s another interesting quote from the Wikipedia page on encapsulation:

The authors of Design Patterns discuss the tension between inheritance and encapsulation at length and state that in their experience, designers overuse inheritance. They claim that inheritance often breaks encapsulation, given that inheritance exposes a subclass to the details of its parent’s implementation.

This is one of the reasons why it is often better to prefer composition over inheritance, which is a principle that is recommended in Design Patterns. Julia does not allow inheritance of concrete data types, so in fact composition is essential when writing Julia code.

So, to sum up, I would say that using interfaces and composition is generally the best way to write code, and neither interfaces nor composition require class-based OOP. :slight_smile:

(By the way, don’t forget that there are many functional programming languages like Haskell, Elm, and Elixir that get by just fine without classes and inheritance.)

8 Likes

Thanks a lot for the reply!

I have been to C++ as well. So even in terms of implementation object.function(param) is OOP is first converted into function(object, param) and then it runs. This can be done in Julia as well.

What we really miss is data encapsulation: data and the function that runs and processes data are separated. This makes a big difference when it comes to using Julia in software development.

I will definitely ready the posts you suggested. Thanks a lot.


I had a look at the package you mentioned. It is quite cool! I think it will save me! :slight_smile:

1 Like

Thanks a lot for the reply.

What I mean by data encapsulation is that data and the function that runs and processes the data are at the same place. This can be done (with some difficulty) in Julia.

I do not know maybe I am so in love with OOP and it is pity that we donot have in in Julia :slight_smile:

1 Like

Thank you very much for the reply.

Yes, access control such as public, private, protected is really out of fashion, even python does not have it. What is more important, which I cll encapsulation, is having data and functions in the same place, which is missing in Julia.

Even in python, when we call object.function(params) it is first converted into function(object, params) before it runs. This can be also done in Julia, no problem. But the fact that data and function now are separated, makes it difficult for nice software development since one needs to always take care of two things, where is data and where is the function.

Not that things cannot be done, but encapsulation makes things super simple.

1 Like

Hahah :slight_smile:
you are right the small egoic self is gone!

1 Like

Thanks a lot for the response.

I also started from C and also did a lot with MATLAB. But I loved OOP in C++ and then Java, Python very much.

Yes, for writing simple things, OOP does not matter and can be even difficult at first. But it has the beautiful idea that data and function that acts on the data are on the same place. So they can be managed more carefully. Also in OOP data is more important than the function, function is there to just modify and process the data.

Also, I donot get the idea of multiple dispatch: even in OOP when we write object.function(params) it is first converted to function(object, params) and then it runs. So I guess the idea is the same … The same function can have different signatures …

1 Like

I understand completely that this is not a perfect solution, or a one-to-one comparison, but maybe an example may help:

You can define an interface inside a module, which clearly define the methods that should be defined for a certain type:

module Animal

abstract type AbstractAnimal end

name(::AbstractAnimal)=error("unimplemented")

export AbstractAnimal, name

end # module

Then your implementation of a concrete type will mirror this exactly, keeping the type (the data) together with the functions:

module Dog
using Animal

struct Dog <: AbstractAnimal
    name::AbstractString
end

Animal.name(x::Dog) = x.name

export Dog
end # module

You can use modules to act like a class, keeping the properties (the data) together with the functions that act on it. The only issue is that the burden of keeping this pattern falls on the developer.

Multiple dispatch on your own types and restricting function definitions allows you to easily keep track of which function implementation corresponds to a certain data type, as in the example above.

3 Likes

To be absolutely precise, object.function(params) is converted to type(object).function(object, params). In Python, a function can only have 1 method; that is obvious for functions outside classes. Methods in separate classes are actually the same way; even if they share a name, they are not the same function object. For example, A.f and B.f are totally separate functions in Python, while f(x::A) and f(x::B) are two methods belonging to the same function f in Julia. But conceptually we can treat the name f as being dispatched by A or B, so let’s get to the important point. The difference between OOP and multiple dispatch is how much the function’s signature can vary.

By tying a single-method-function name to a class, you can only dispatch on that class: A.f, B.f. Sure, maybe those methods have different argument signatures A.f(self, x), B.f(self, x, y), but that argument variation is totally separate and does not mix with the class-based dispatch e.g. you cannot also do A.f(self, x, y). Varying argument signatures also tends to be avoided because a consistent signature is necessary for duck-typing a method call either_a_or_b.f(x).

With multiple dispatch, you can define f(a::A, x), f(b::B, x, y), as well as f(a::A, x, y), f(b::B, x). Defining different function signatures are a LOT more flexible when the first argument (self in Python) isn’t tied to the class. Ducking typing is also more flexible, here you can call f(either_a_or_b, x) or f(either_a_or_b, x, y).

Python prioritized classes, Julia prioritized multiple dispatch, and both languages were wise enough to not try to mix them (see my previous comment for a few ways those two paradigms don’t mix well). As you rightfully point out, tying functions to classes does have its perks. Other commenters have said how multiple dispatch fits their programming style better; I personally have used both Python and Julia and preferred to organize code with multiple dispatch. But if classes make it easier for you to code, it’s valid for you to pick that paradigm. No matter how we prefer to talk to the machine, the machine only sees 1s and 0s anyway.

8 Likes

The difference is that all of the arguments to a function are used to determine which method gets called, not just the first argument:

foo(::String, ::Int) = 1
foo(::Int, ::Int) = 2
foo(::Int, ::Float64) = 3
foo(::Int, ::String) = 4
julia> foo("hello", 42)
1

julia> foo(42, 1234)
2

julia> foo(42, 3.14)
3

julia> foo(42, "hello")
4

In Python, you can’t dispatch on the second argument of foo, the best you can do is use if-elif-else control flow:

In [1]: def foo(x, y):
    ...:     if isinstance(y, int):
    ...:         return 2
    ...:     elif isinstance(y, float):
    ...:         return 3
    ...:     elif isinstance(y, str):
    ...:         return 4
    ...:     else:
    ...:         raise TypeError
    ...: 

In [2]: foo(1, 2)
Out[2]: 2

In [3]: foo(1, 3.14)
Out[3]: 3

In [4]: foo(1, "hello")
Out[4]: 4
2 Likes

Thank you so much.
I haven’t though this way. You are right, this may work, but it would be a little awkward in big projects I guess.

1 Like

yes and no!

I think you are right about Python, one cannot do this in Python because once a function is redefined it replaces the previous definition. So the only possibility to get different responses is to check the argument value, via default arguments, which will take time and will not compile. For example,

def f(a, b=None):
     if b==None:
          print("a was called!")
          return

    print("a and b both were called!")

Otherwise multiple dispatch is just function overloading and has been always around in C++, Java, etc.

Also, although I agree with you that the function is the same in multiple dispatch, this may be true only in source code level. Otherwise, I guess each of the functions with different argument are compiled as different machine codes. So ultimately various machine codes are produced to handle various data. C++ template coding also works in this way.

1 Like

Overloading is definitely similar to multiple dispatch, but they’re not quite the same. See this long discussion:

4 Likes