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?

gsamaras
  • 71,951
  • 46
  • 188
  • 305
Tasos Papastylianou
  • 21,371
  • 2
  • 28
  • 57

2 Answers2

41

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.

Jeff Bezanson
  • 3,147
  • 23
  • 26
  • 2
    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? – Tasos Papastylianou Aug 26 '16 at 00:21
  • 2
    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++. – Tasos Papastylianou Aug 26 '16 at 00:22
  • 4
    PS: it's a privilege to meet the Creator. (Forgive me, oh Lord! I am not worthy!) – Tasos Papastylianou Aug 26 '16 at 00:26
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/

Acmion
  • 73
  • 9
  • 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!) – Tasos Papastylianou Jun 02 '21 at 22:09
  • 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. – Acmion Jun 04 '21 at 17:05