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

Well, use modules to divide your code…

I’m curious about this distinction. Surely you need software engineering practices when developing computational science software. There is a (blurry) distinction between using and developing software, but why do you think that computational science is different in that regard?

2 Likes

Thanks. yes this was suggested by some one else as well. I should start learning about it.

No I don’t have a concrete reason but, by experience, I used Matlab for more than 15 years and never wrote a class since it was not that necessary.

I’ve been using Matlab for longer than that (and still do), and classes are something I use every day, and in most projects.

The moment one starts developing a project of a certain size, one needs to start thinking about code organization, interfaces, and naming things well, though one might not need classes for that.

But if in doubt, you can go and have a look at some of the Julia packages, e.g. at JuliaHub, and see if they don’t use software engineering. Saying they ‘just compute something’, seems, well, inadequate.

2 Likes

Thank you very much for all the comments. I have already implemented some code and I am slowly learning the features.

So far, no-one here has suggested using “closures” which I personally find ideal for having specific data be associated with a specific function. A search for Julia and closures should give you a lot of references, one of them is Julia Language Tutorial => Introduction to Closures . It is a functional programming technique and if I understand you correctly, then this can provide what you are asking for.

I use this technique a lot in places where I would have encapsulated things in an object in an OO language.

2 Likes

Instances can be callable because a call a(b, c, d) dispatches on the types of a, b, c, d. a is often a function, which is an instance of a singleton type, but a could be an instance of any type. This is also how closures are implicitly implemented. The key takeaway here is that a method table technically belongs to a type, or more precisely a TypeName.

Typically in Julia, callable instances are used like a multimethod analog to Python’s __call__. But a Python class has many other methods distinguished by name, is there a way for Julia to emulate such dispatch on a function name in addition to the primary instance? As that wording suggested, you could input a function and dispatch on its singleton type:

function f end
function g end

abstract type AbstractX end # all subtypes should have x field
(m::AbstractX)(::typeof(g), y) = m.x + y

struct MyX <: AbstractX
  x::Int
end
(m::MyX)(::typeof(f)) = m.x

a = MyX(3)
a(f)       # like a.f() in Python
a(g, 2)    # like a.g(2)
methods(a) # like dir(a) for methods
#=
# 2 methods:
[1] (m::MyX)(::typeof(f)) in Main at main.jl:10
[2] (m::AbstractX)(::typeof(g), y) in Main at main.jl:5
=#

So Julia’s multiple dispatch actually allows mimicry of the organization of Python’s classes and methods; methods(a) even mimics the use of dir (fieldnames(typeof(a)) gets the fields). It’s just not done because of how multimethod definitions need to be distant from most or all of the argument type definitions (see my comment 25 of this thread), and the syntax thus isn’t designed to fit it.

1 Like

In fact, this is why I love Julia. I used to use traditional encapsulation as you mentioned, but now I think Julia’s way is superior. In fact, I never have to use the traditional encapsulation anywhere in my many Julia packages.

May I ask why do you want to put data and methods in the same place (I guess you mean “class”?)? If you want to know which method is associated with any type, just use methodswith and other functions.

The external-method mechanism adopted in Julia lets you define any method you want on almost any type you want, even when these methods and types are not owned by your packages. And these methods and types just magically work!

For example, I can define a subtype of AbstractArray with the AbstractArray interface:

julia> struct SquaresVector <: AbstractArray{Int, 1}
           count::Int
       end

julia> Base.size(S::SquaresVector) = (S.count,)

julia> Base.IndexStyle(::Type{<:SquaresVector}) = IndexLinear()

julia> Base.getindex(S::SquaresVector, i::Int) = i*i

As you can see, size, IndexStyle, and getindex are not owned by me. They were originally defined in Base, but I can extend them to my types. And 99% of packages written by others that support this interface can work with my type. Isn’t it magical?

Also, I can define my methods for types defined in the standard libraries and other people’s packages. This feature is used daily in the Julia community. If Julia uses the internal-methods mechanism like Python, I cannot simply do that because it is not advisable to add methods to classes you do not own. I cannot persuade other developers and Python contributors to integrate my methods into their types, this is just not doable. So I have to either write a wrapper for that type or inherit from that type. The disadvantage of the former way is that you will have a deeply-nested type if other people also think so and such nesting is repeatedly used in your dependencies, dependencies of dependencies, etc. For the latter one, you could end up in mixin and strange method resolution orders, which could lead to unexpected effects.

Please watch this video, especially where it talks about why we use “external dispatch” in Julia.


There are a few packages that supports traditional “class”-based OOP, if you really want to use them.

  1. OOPMacro.jl
  2. ObjectOriented.jl
  3. Classes.jl
  4. Mixers.jl

But I still do not think you need them. It is Julia, please try to learn the new (or probably the better) way of thinking.

12 Likes

The way you mentioned entites makes me wonder if you’re doing / thinking of game development in Julia. It’s true that here OOP shines, but you can have a look at GitHub - louisponet/Overseer.jl: Entity Component System with julia
Anyways, when it comes to naming etc. at least when I’m starting out I’m not really sure what the structure that I’m trying to model is, and I start with small/abstract components, and then I compose/flesh them out.
At the end of this process, everything is just as nicely arranged, but it’s just easier to get started.

For example, I can have a function f(x::X, y::Y) where X and Y are defined by me. I may be not sure at the beginning if it should be next to the definition for X or Y. But at some point, it may become clear that it is more relevant to Y, so I reorder arguments: f(y::Y, x::X) and put it under the definition for Y. Or it might need a special struct S x::X y::Y end or module etc.

Hi,
Thank you very much. I am still in learning phase and I have started transferring some of my code to Julia. However, to be honest, I have not yet got the speed improvement I expected but I should keep learning and investigating. Also, still I prefer, OOP fashion, maybe it’s because I am so used to it.

Thanks a lot for the comments.
I think it is also a little bit development style. With OOP, I immediately divide the work into a couple of classes and how they should interact with each other. And the development comes so naturally!

Now post “my Python code is faster than Julia” , with the appropriate code test, and see the magic :rofl:
(unless the intrinsic botleneck is in a call to some fast blast routine or similar, in which you won’t see much difference).

It is not that different. Here you define:

julia> struct Cat end

julia> struct Fish end

julia> eat(x::Cat, y::Fish) = "yes" 
eat (generic function with 1 method)

julia> eat(x::Fish, y::Cat) = "no" 
eat (generic function with 2 methods)

julia> c = Cat(); f = Fish()
Fish()

julia> eat(c,f)
"yes"

julia> eat(f,c)
"no"
5 Likes

But can’t you just divide the work into a couple of types with functions associated with them? Except for the functions being defined outside the type, is it really that different?

5 Likes

Great minds think alike :grin: (Actually, I’ve heard that’s totally wrong, but anyway.)

1 Like

As a former OOP guru, I can promise you that, if you commit to learning and using the Julian (and more broadly, functional) approach to programming, you will undoubtedly “see the light” eventually :slight_smile: It truly is a more powerful paradigm, and the separation of data from behavior eliminates a whole range of anti-patterns that are endemic to OOP (like god classes, fragile base classes, ambiguous inheritance trees, etc).

Rather than forcing your code into rigid type hierarchies where each node represents some collection of data which “does” some number of things, you think about data and functions as separate things (which they are). A struct is just some collection of data with some type information, and a method is a function which can be defined on certain data patterns based on this type information.

Let’s consider a simple example where we make a struct to store the results of a (simple) linear fit:

abstract type AbstractLinearFit end
struct LinearFit{S,I} <: AbstractLinearFit
  slope::S
  intercept::I
end
slope(fit::AbstractLinearFit) = fit.slope
intercept(fit::AbstractLinearFit) = fit.intercept
predict(fit::AbstractLinearFit, x) = slope(fit)*x + intercept(fit)

Note the use of the getter methods slope and intercept in predict (a pattern which should be familiar from OOP) which makes predict totally agnostic to the underlying data structure of the AbstractLinearFit that it is given. The default implementations assume there is a corresponding field defined on the type, but this behavior can be easily extended. Imagine, for example, an alternative parameterization of a linear fit which computes y = slope*(x + shift), i.e. where the intercept is actually slope*shift. This could be implemented by simply defining a new type and a new dispatch for intercept:

struct ShiftedLinearFit{F,T} <: AbstractLinearFit
  slope::S
  shift::T
end
intercept(fit::ShiftedLinearFit) = slope(fit)*fit.shift

This is admittedly a slightly contrived example, but the point is that we can achieve encapsulation also using functional patterns and multiple dispatch by defining method interfaces that are agnostic to data structure and then implementing that interface for various data types.

The only real loss from single-dispatch OOP is the partial loss of discoverability mentioned earlier in this thread. But I say partial because, even in OOP, objects do not necessarily contain every single function or method which can operate on them.

36 Likes

What an extraordinary example! I will use it in my class. Thanks!

1 Like

As a former OOP guru, I can promise you that, if you commit to learning and using the Julian (and more broadly, functional) approach to programming, you will undoubtedly “see the light” eventually

This comment reminds me of when I started using Mathematica in 2004 after 4 years of Java programming. The first thing I did was implement an OOP system. The fact that I could do this, complete with inheritance that was in some ways more powerful than Java’s OOP (one could call super[super[...]] to access the methods of the super class of a super class), all in about half a page of code was enough to convince me that functional programming was so powerful there was no need to tack OOP on top.

9 Likes