Julia Object Oriented Programming

Published: 2021-05-29
Discussion on: Acmion (on this website), Dev, or Reddit.

Julia is a nice and promising scientific language for high performance computing, with the central paradigm of multiple dispatch. However, Julia does only partially support object oriented programming (OOP) with dot notation. For example, "objects" can not have their own methods. Unfortunately, this happens to be the style of programming that I prefer.

This StackOverflow question discusses the matter, however, the fields (if exposed) in the solution get wrapped in Core.Box, which kind of defeats the purpose of fields, as they can no longer be accessed in the manner that one would expect.

I figured out an undocumented way in which object oriented programming with dot notation, including methods and without boxing, can be implemented and decided to write this post so that the knowledge can be passed on and the relative common question of dot notational OOP in Julia could be answered with more than "No, it does not work". Additionally, I hope that this way of programming would become more widely supported in Julia.

Python Vs Julia Classes

Consider the following class in Python and how it's methods are called:

# Python
class ExampleClass:
    def __init__(self, field_0, field_1):
        self.field_0 = field_0
        self.field_1 = field_1

    def method_0(self):
        return self.field_0 * self.field_1

    def method_1(self, n):
        return (self.field_0 + self.field_1) * n

    def method_2(self, val_0, val_1):
        self.field_0 = val_0
        self.field_1 = val_1

ex = ExampleClass(10, 11)
ex.method_0()
ex.method_1(1)
ex.method_2(20, 22)

The common way to implemented this in Julia would look like this:

# Julia
mutable struct ExampleClass
    field_0
    field_1
end

function method_0(example_class)
    return example_class.field_0 * example_class.field_1
end

function method_1(example_class, n)
    return (example_class.field_0 + example_class.field_1) * n
end

function method_2(example_class, val_0, val_1)
    example_class.field_0 = val_0
    example_class.field_1 = val_1
end

ex = ExampleClass(10, 11)
method_0(ex)
method_1(ex, 1)
method_2(ex, 20, 22)

The key difference between these two examples is that the methods in Python belong to the object and the functions in Julia belong to the global scope. I do not like the concept of global scope and would like to avoid it as much as possible.

Julia OOP Methods With Dot Notation

By implementing the class in a different way we can replicate the Pythonic dot notation. Consider the following:

# Julia
mutable struct ExampleClass
    field_0
    field_1
    method_0
    method_1
    method_2

    function ExampleClass(field_0, field_1)
        this = new()

        this.field_0 = field_0
        this.field_1 = field_1

        this.method_0 = function()
            return this.field_0 * this.field_1
        end

        this.method_1 = function(n)
            return (this.field_0 + this.field_1) * n
        end

        this.method_2 = function(val_0, val_1)
            this.field_0 = val_0
            this.field_1 = val_1
        end

        return this
    end
end

ex = ExampleClass(10, 11)
ex.method_0()
ex.method_1(1)
ex.method_2(20, 22)

Now it works like in Python, just as I like it!

Private Variables

What about private variables? I see that there are two ways in which private variables can be implemented. One is to add new fields to the struct, but prefix them with an underscore like this:

# Julia
mutable struct ExampleClass
    field_0
    field_1
    method_0
    method_1
    method_2
    _private_var

    function ExampleClass(field_0, field_1)
        this = new()
        this._private_var = 0
        ...
    end
end

Unfortunately, the variables are not really private and can still be accessed from the outside. Another way, with truly private variables is to include them in the constructor like this:

# Julia
mutable struct ExampleClass
    field_0
    field_1
    method_0
    method_1
    method_2

    function ExampleClass(field_0, field_1)
        this = new()
        private_var = 0

        ...
    end
end

Now the private variables are really private, however, they can not be accessed with dot notation from this, which is unfortunate. I prefer the underscore method.

How Does it Work?

The revised Julia code relies primarily on three things: closures, variable capturing and anonymous functions.

Caveats

Unfortunately this way of programming has some caveats, at least when compared to the "idiomatic" Julia way.

  • The size of the classes grow, with each method increasing the size by 8 bytes (at least on 64 bit systems) and thus allocation becomes slightly slower.
  • Calling the anonymous functions is slightly slower than calling the global functions.
  • Capturing variables in Julia has some performance implications. See the Julia docs. Using let this = this may have some performance benefit when applied to the anonymous functions, but I am uncertain of its effects in this case.
  • The classes (or structs) must be mutable, since they are modified in the constructor. This can perhaps be circumvented in some fashion.

Alternative Julia OOP Methods With Dot Notation

Eigenspace shared this alternative and more performant approach at Reddit. The syntax is not as clean, but it works. The class would now look like this:

mutable struct ExampleClass
    field_0
    field_1

    function ExampleClass(field_0, field_1)
        this = new()

        this.field_0 = field_0
        this.field_1 = field_1

        return this
    end
end

function Base.getproperty(this::ExampleClass, s::Symbol)
    if s == :method_0
        function()
            return this.field_0 * this.field_1
        end
    elseif s == :method_1
        function(n)
            return (this.field_0 + this.field_1) * n
        end
    elseif s == :method_2
        function(val_0, val_1)
            this.field_0 = val_0
            this.field_1 = val_1
        end
    else
        getfield(this, s)
    end
end

ex = ExampleClass(10, 11)
ex.method_0()
ex.method_1(1)
ex.method_2(20, 22)

Performance Evaluation

This section provides a quick performance comparison between the styles in Julia. Note that the results are highly dependent on the fields and methods of the classes and as such this test should not be considered to be representative of every situation. This test focuses on allocation and method call performance. You should benchmark your own code for your self, which probably has other underlying assumptions. The benchmarks are executed with these types:

mutable struct GlobalScopeType
    field_0::Int
    field_1::Int
    field_2::Int
    field_3::Int
end

function do_work(gst::GlobalScopeType, n::Int)
    sum = 0

    for i in 1:n
        sum += gst.field_0 * gst.field_1 + gst.field_2 * gst.field_3 ^ n 
    end

    return sum
end

mutable struct DotNotationType
    field_0::Int
    field_1::Int
    field_2::Int
    field_3::Int
    do_work::Function

    function DotNotationType(field_0::Int, field_1::Int, field_2::Int, field_3::Int)
        this = new()
        this.field_0 = field_0
        this.field_1 = field_1        
        this.field_2 = field_2
        this.field_3 = field_3


        this.do_work = function(n::Int)
            sum = 0

            for i in 1:n
                sum += this.field_0 * this.field_1 + this.field_2 * this.field_3 ^ n 
            end

            return sum
        end

        return this
    end
end

mutable struct BasePropertyType
    field_0::Int
    field_1::Int
    field_2::Int
    field_3::Int

    function BasePropertyType(field_0::Int, field_1::Int, field_2::Int, field_3::Int)
        this = new()
        this.field_0 = field_0
        this.field_1 = field_1        
        this.field_2 = field_2
        this.field_3 = field_3

        return this
    end
end

function Base.getproperty(this::BasePropertyType, s::Symbol)
    if s == :do_work
        function (n::Int)
            sum = 0
            for i in 1:n
                sum += this.field_0 * this.field_1 + this.field_2 * this.field_3 ^ n 
            end
            sum
        end
    else
        getfield(this, s)
    end
end

Type Sizes

println(sizeof(GlobalScopeType))
println(sizeof(DotNotationType))
println(sizeof(BasePropertyType))
32
40
32

The size of DotNotationType is 8 bytes, or 1.25x, larger than the size of either GlobalScopeType or BasePropertyType. Note that in "real" OOP languages, methods on a class do not generally make its memory imprint larger. This is due to the fact that the methods are conceptually placed in the global scope at compile time (not accounting for anonymous functions in other languages).

Boxing

println(GlobalScopeType(1, 2, 3, 4).field_0)
println(DotNotationType(1, 2, 3, 4).field_0)
println(BasePropertyType(1, 2, 3, 4).field_0)
1
1
1

The fields of any type are not boxed within Core.Box, as in this question on StackOverflow.

Allocation Performance

using Pkg
Pkg.add("BenchmarkTools")
using BenchmarkTools

function allocate_GlobalScopeType()
    types = GlobalScopeType[]

    for i in 1:100000
        push!(types, GlobalScopeType(i, i * i - i, i * 2, i))
    end

    return types
end

function allocate_DotNotationType()
    types = DotNotationType[]

    for i in 1:100000
        push!(types, DotNotationType(i, i * i - i, i * 2, i))
    end

    return types
end

function allocate_BasePropertyType()
    types = BasePropertyType[]

    for i in 1:100000
        push!(types, BasePropertyType(i, i * i - i, i * 2, i))
    end

    return types
end

display(@benchmark allocate_GlobalScopeType())
display(@benchmark allocate_DotNotationType())
display(@benchmark allocate_BasePropertyType())
BenchmarkTools.Trial: 
  memory estimate:  6.58 MiB
  allocs estimate:  100017
  --------------
  minimum time:     1.676 ms (0.00% GC)
  median time:      1.986 ms (0.00% GC)
  mean time:        2.821 ms (29.49% GC)
  maximum time:     17.085 ms (88.37% GC)
  --------------
  samples:          1772
  evals/sample:     1
BenchmarkTools.Trial: 
  memory estimate:  8.10 MiB
  allocs estimate:  200017
  --------------
  minimum time:     2.447 ms (0.00% GC)
  median time:      2.782 ms (0.00% GC)
  mean time:        5.090 ms (45.31% GC)
  maximum time:     31.272 ms (87.66% GC)
  --------------
  samples:          982
  evals/sample:     1
BenchmarkTools.Trial: 
  memory estimate:  6.58 MiB
  allocs estimate:  100017
  --------------
  minimum time:     1.695 ms (0.00% GC)
  median time:      1.953 ms (0.00% GC)
  mean time:        2.760 ms (29.75% GC)
  maximum time:     11.978 ms (83.33% GC)
  --------------
  samples:          1811
  evals/sample:     1

Allocating GlobalScopeType or BasePropertyType is approximately equally fast. On the other hand, allocating DotNotationType is about 1.40x (comparing medians) slower than allocating the other types. This seems to be directly dependent on the fact that the size of DotNotationType is 1.25x larger than the size of the other types.

Method Performance

using Pkg
Pkg.add("BenchmarkTools")
using BenchmarkTools

gsts = allocate_GlobalScopeType()
dnts = allocate_DotNotationType()
bpts = allocate_BasePropertyType()

function call_GlobalScopeType(gsts)
    sum = 0.0
    
    for g in gsts
        sum += do_work(g, 4)
    end

    return sum
end

function call_DotNotationType(dnts)
    sum = 0.0
    
    for d in dnts
        sum += d.do_work(4)
    end

    return sum
end

function call_BasePropertyType(bpts)
    sum = 0.0
    
    for b in bpts
        sum += b.do_work(4)
    end

    return sum
end

# Assuming execution from a Jupyter Notebook
display(call_GlobalScopeType(gsts))
display(call_DotNotationType(dnts))
display(call_BasePropertyType(bpts))

display(@benchmark call_GlobalScopeType(gsts))
display(@benchmark call_DotNotationType(dnts))
display(@benchmark call_BasePropertyType(bpts))
4.3018363623291675e21
4.3018363623291675e21
4.3018363623291675e21
BenchmarkTools.Trial: 
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     2.342 ms (0.00% GC)
  median time:      2.420 ms (0.00% GC)
  mean time:        2.467 ms (0.00% GC)
  maximum time:     4.659 ms (0.00% GC)
  --------------
  samples:          2026
  evals/sample:     1
BenchmarkTools.Trial: 
  memory estimate:  3.05 MiB
  allocs estimate:  199998
  --------------
  minimum time:     7.056 ms (0.00% GC)
  median time:      7.299 ms (0.00% GC)
  mean time:        7.573 ms (2.17% GC)
  maximum time:     14.988 ms (49.54% GC)
  --------------
  samples:          660
  evals/sample:     1
BenchmarkTools.Trial: 
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     2.339 ms (0.00% GC)
  median time:      2.421 ms (0.00% GC)
  mean time:        2.430 ms (0.00% GC)
  maximum time:     3.453 ms (0.00% GC)
  --------------
  samples:          2056
  evals/sample:     1

All methods get the same result, which implies that both methods do the same work. The performance of both GlobalScopeType and BasePropertyType is approximately equal. However, the performance of DotNotationType is about 3.02x (comparing medians) slower. I am not entirely sure what is causing the slowdown, but it may have something to do with the issues mentioned in the Julia docs about the performance of capturing variables or it may have something to do with the optimization of anonymous functions.

Conclusions

  • OOP is possible in Julia.
  • OOP with dot notation methods is possible in Julia.
  • OOP without boxing of fields is possible.
  • OOP with dot notation, if implemented via anonymous functions, does have caveats and generally worse performance.
  • OOP dot notation methods, if implemented via anonymous functions, in Julia are implemented in a generally inferior approach than that of most mainstream programming languages. Note that this is due to the fact that the way of programming presented in this post declares the functions as anonymous functions. This is not how the most common "real" OOP languages implement class methods.
  • OOP with Base.getproperty has the same performance as the standard approach in Julia, however, the syntax is slightly unconventional.

Great, we got a working solution for object oriented programming with dot notational methods! The issues with the dot notational way of programming may or may not affect the general performance of your application, but I expect that this is highly domain specific. For example, in the "Method Performance" section I specifically focused on measuring the performance of method calls. Measuring the performance of actual work within a method may behave drastically different. Regardless, this may at least be a good way of porting dot notational code from, for example, Python to Julia and hopefully this will encourage Julia to implement true classes with true dot notation!

Discussion

Discuss this post here! You may also discuss it on Dev, or Reddit.

Post a Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.
Jasoncix (2021-11-10 11:19:08 UTC)

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.
Ugocix (2021-11-08 04:39:48 UTC)

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.
Tedcix (2021-11-04 06:21:23 UTC)

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.
Marycix (2021-11-04 20:13:04 UTC)

Online

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.
Louisunine (2021-09-27 02:02:41 UTC)

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.
Luoqoh (2022-03-31 09:59:21 UTC)

Simply think about - your future wife or girlfriend may be sitting hopefully proper opposite her laptop display screen, hoping that someone just like you will seem..! On the lookout for an Asian wife online has been trending for more than a decade. If you are the identical bodily age however you are way more spiritually mature than somebody, you won't be as proud of this individual as you could be with someone youthful than you who is closer to your spiritual maturity or ever more spiritually mature than you. The way more critical daters wants a much better probability with discovering what they need on an affiliate site that that has fesses owing. We aren't saying that China girls relationship is best than courting ladies from other nations. Oh, what am I saying? Once you uncover your DatingDamns and why you are drawn to them, it starts to unblock vitality and focus and offers you space to get the relationship vitality flowing in a new path. For them, women are anything however “the weaker sex”. Last but not least, it's best to remember that girls love surprises. No, you just need to watch out and prepared for surprises. You need to care not only about how to seek out an Asian lady to marry but also about the way to impress Asian mail brides and make your relationship successful. While having your first date make it sure that the folks with whom you might be courting with know your intention very effectively. There are a number of laborious and fast guidelines relating to dating and shedding your problematic scalp behind you earlier than you attempt to make an impression is one among them. Due to this, we’ve developed a easy and streamlined relationship app that’s accessible for each iOS and Android techniques. Morin says. "There will probably be some bumps within the road when you’re courting, and that’s effective. Adjusting to new situations and new folks is usually a bit of a process." Let it unfold naturally. Many reasons exist for people log on to consider a soul mate, and in addition where most of those reasons are personal, it might enable you to some bit in your on the web adventure if you happen to might strive the next online dating companies ideas. They're loyal, but who knows, they only is likely to be good actors whereas sneaking round your back. Almost about relationship, stuttering may be a huge problem for a individual that's suffering from stuttering. If there are any two words to describe Russian men in a relationship, it’s romantic and chivalrous. You’ve probably seen a number of in style films the place typical Russian males are shown to be these alcohol-obsessed, violent, hulking giants with two or three mind cells, all the time willing to throw a struggle each time they heard someone speak English or has a damaging opinion on communism. For example, are you trying for someone from Asia then start your search there. For example, you might come across such factor as Russian household. All of these things are nice ways to get multiple factors of view on a problem that might not be as clear-minimize because it first appears. When they're off to work, you may even see them wearing shirts and some casual denims. Younger guys like sweatpants and boots, and it’s not uncommon to see them wearing layered jackets. Most guys assume it's essential to ship an amazing first message to get a woman’s consideration. 3. Groom yourself- One essential factor you could keep in mind that it’s not important so that you can be exceptionally handsome in an effort to strategy girls, if you're properly groomed and clean you can simply strategy girls. We’ve already shared a lot about their character - it’s like that, however not as idealized. Much like [url=https://www.bestbrides.net/how-to-tell-if-a-woman-likes-you-based-on-her-zodiac-sign/]virgo woman signs she likes you[/url] Individuals, they really feel like they should protect the world, beginning from their particular somebody. I’d be more than pleased to allow you to in on a couple of neat little secrets that can assist you out when it comes to dating Russian men, whether or not you’re searching for an adventure, or one thing far more meaningful! Or if you happen to seek for nothing complicated, let’s learn the way good are Russian men in mattress. There is probably nothing more brutal on a date as a shy man than merely sitting dealing with one another. Take things someday at a time and don’t go on to overwhelm your date by expecting a full dedication on day one from him. Firstly, it’s captivating. You don’t must provide you with ridiculous stories, play foolish roles, and really feel fixed tension out of your lies which, by the way, are never convincing sufficient, even through the display. It’s arduous to give you a particular look of the common Russian man - Russia is a large country, with numerous ethnicities, and every of them has its peculiar look.

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.
Yoncix (2021-11-11 14:03:40 UTC)

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.
Marycix (2021-11-10 15:35:53 UTC)

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.
Amycix (2021-11-11 15:22:47 UTC)

Reply to Comment

Error! Incorrect value.
Note: Commenting is completely anonymous, thus, comments can not be edited.

Report Comment

Error! Incorrect value.