How to create a "single dispatch, object-oriented Class" in julia that behaves like a standard Java Class with public / private fields and methods
Asked Answered
S

2

23

I read in a book that "you can't create traditional 'classes' in julia with single-dispatch-style methods like obj.myfunc()" ... and I thought that sounded more like a challenge than a fact.

So here's my JavaClass type with public / private fields and methods just for the sheer shock and horror factor of having something ugly like this in Julia, after all the trouble the devs have gone to to avoid it:

type JavaClass

    # Public fields
    name::String

    # Public methods
    getName::Function
    setName::Function
    getX::Function
    getY::Function
    setX::Function
    setY::Function

    # Primary Constructor - "through Whom all things were made."
    function JavaClass(namearg::String, xarg::Int64, yarg::Int64)

        # Private fields - implemented as "closed" variables
        x = xarg
        y = yarg

        # Private methods used for "overloading"
        setY(yarg::Int64) = (y = yarg; return nothing)
        setY(yarg::Float64) = (y = Int64(yarg * 1000); return nothing)

        # Construct object
        this = new()
        this.name = namearg
        this.getName = () -> this.name
        this.setName = (name::String) -> (this.name = name; return nothing)
        this.getX = () -> x
        this.getY = () -> y
        this.setX = (xarg::Int64) -> (x = xarg; return nothing)
        this.setY = (yarg) -> setY(yarg) #Select appropriate overloaded method

        # Return constructed object
        return this
    end

    # a secondary (inner) constructor
    JavaClass(namearg::String) = JavaClass(namearg, 0,0)
end

Example use:

julia> a = JavaClass("John", 10, 20);

julia> a.name # public
"John"

julia> a.name = "Jim";

julia> a.getName()
"Jim"

julia> a.setName("Jack")

julia> a.getName()
"Jack"

julia> a.x # private, cannot access
ERROR: type JavaClass has no field x

julia> a.getX()
10

julia> a.setX(11)

julia> a.getX()
11

julia> a.setY(2) # "single-dispatch" call to Int overloaded method

julia> a.getY()
2

julia> a.setY(2.0)

julia> a.getY()  # "single-dispatch" call to Float overloaded method
2000

julia> b = JavaClass("Jill"); # secondary constructor

julia> b.getX()
0

Essentially, the constructor becomes a closure, which is how one creates "private" fields and methods / overloading. Any thoughts? (other than "OMG Why??? Why would you do this??")
Any other approaches?
Any scenarios you could envisage where this might fail spectacularly?

Semiporcelain answered 24/8, 2016 at 21:38 Comment(4)
I don't know if this is suited for StackOverflow... but it's interesting.Becquerel
You just wanted to share this... But I mean, I would want to too.Becquerel
I'm sure someone's going to look for this exact question on stackoverflow at some point. It's the kind of question that's inevitable! So while the format may be a bit unorthodox, it's probably a more than legit article for stackoverflow :)Semiporcelain
I admire the lengths to which you go to avoid working on your thesis!Dygal
T
42

While of course this isn't the idiomatic way to create objects and methods in julia, there's nothing horribly wrong with it either. In any language with closures you can define your own "object systems" like this, for example see the many object systems that have been developed within Scheme.

In julia v0.5 there is an especially slick way to do this due to the fact that closures represent their captured variables as object fields automatically. For example:

julia> function Person(name, age)
        getName() = name
        getAge() = age
        getOlder() = (age+=1)
        ()->(getName;getAge;getOlder)
       end
Person (generic function with 1 method)

julia> o = Person("bob", 26)
(::#3) (generic function with 1 method)

julia> o.getName()
"bob"

julia> o.getAge()
26

julia> o.getOlder()
27

julia> o.getAge()
27

It's weird that you have to return a function to do this, but there it is. This benefits from many optimizations like the language figuring out precise field types for you, so in some cases we can even inline these "method calls". Another cool feature is that the bottom line of the function controls which fields are "public"; anything listed there will become a field of the object. In this case you get only the methods, and not the name and age variables. But if you added name to the list then you'd be able to do o.name as well. And of course the methods are also multi-methods; you can add multiple definitions for getOlder etc. and it will work like you expect.

Trierarch answered 25/8, 2016 at 16:34 Comment(3)
Very interesting! Especially the bit about captured variables becoming fields! However, I see a problem with this approach: these fields seem to be immutable, which defeats the purpose of "public" fields; also, it seems their values become "boxed" if "setter" methods exist, rather than just "getter" ones, leading to further complications (e.g. if age is added to the "public" variables above, o.age + 1 fails with ERROR: MethodError: no method matching +(::Core.Box, ::Int64)). Could there be any way around these with this approach?Semiporcelain
I do like the fact that this can lead to a potentially "callable" object though (i.e. if the last argument in the compound statement is a function itself)! It's almost like creating an "object" which also has an overloaded () operator, like Functors in C++.Semiporcelain
PS: it's a privilege to meet the Creator. (Forgive me, oh Lord! I am not worthy!)Semiporcelain
C
2

Jeff Bezanson's answer is quite good, but as alluded to in the comments, fields may get boxed, which is quite annoying.

There is a superior solution to this problem.

Alternative 1 (basically the same approach as presented in the question):

# 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)

Alternative 2:

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)

Alternative 1 looks better, but alternative 2 performs better.

I created a more in depth analysis of this matter and you may check it here: https://acmion.com/blog/programming/2021-05-29-julia-oop/

Camfort answered 2/6, 2021 at 13:22 Comment(2)
Interesting alternative approach. +1. Thank you for sharing! (I note that apart from some outdated syntax in the original question, your first approach is otherwise the same as in the question, except without private fields/methods -- I don't know if that was intentional!)Semiporcelain
Yeah, you are right about the first alternative. I did not realize that the deprecated keyword "type" is the same as "mutable struct". I'll add a comment to the answer.Camfort

© 2022 - 2024 — McMap. All rights reserved.