O.O.P in Julia

Hello everybody,
I already used the ‘struct’ as an equivalent of class (say for Python or C++, or …) but I didn’t find what is the trick of Julia to emulate the ‘class-variable’ that is the variable which belongs to all instances of the class and is shared between them.
Can you give an answer to such simple question ? Of course forget about ‘global variables’ which is quick and dirty. May-be space-names or something like ?
Thank you !

There are some libraries for doing this. For example:

But read this first: Translating OOP into Idiomatic Julia · ObjectOriented.jl

5 Likes

Julia is not Object Oriented as you might see in Java or C#. struct is not like a class - there are no builtin ways to have a variable shared between all instances of a given struct. You can give all instances a Ref to the same object in their constructor, but there is no builtin way of doing this.

7 Likes

I don’t think global variables are considered quick and dirty in Julia. In fact, I would say global variables are often used for “private class variables” (that is, variables that do not belong to a particular instance and are not part of the user API). I used that in Decimals.jl to “re-use” some instances of BigInts (Decimals.jl/src/bigint.jl at 463869785a1789edcc5181c565e3d28e7516350e · JuliaMath/Decimals.jl · GitHub)

In the case of “public class variables” (that is, variables that do not belong to a particular instance and are part of the user API), they can be implemented as functions. Again, an example from Decimals.jl is the Base.precision function (Decimals.jl/src/context.jl at 463869785a1789edcc5181c565e3d28e7516350e · JuliaMath/Decimals.jl · GitHub).

4 Likes

There is a way which mimics a class variable via the getproperty/setproperty! functions. Whenever you do something like a.b it is lowered to getproperty(a, :b), and whenever to you do something like a.b = c, it’s lowered to setproperty!(a, :b, c). You can make your own extension like e.g.

const classvar = Ref(0)
struct MyStruct
    foo::Int
    bar::Float64
end

@inline function Base.getproperty(a::MyStruct, b::Symbol)
    if b == :classvariable
        return classvar[]
    else
        return getfield(a, b)
    end
end

@inline function Base.setproperty!(a::MyStruct, b::Symbol, x)
    if b == :classvariable
        classvar[] = x
    else
        setfield!(a, b, x)
    end
end

a = MyStruct(1, 2.3)
b = MyStruct(2, 3.5)
a.classvariable = 4
b.classvariable == 4  # true

I made the global classvar a const Ref, this makes the compiler happy. If you care about memory ordering, there is an additional argument to the property functions, see their docs. You may of course call the global whatever you like, to avoid mess, e.g. var"##MyStruct var 1".

You can get autocompletion by extending propertynames:

Base.propertynames(a::MyStruct) = (fieldnames(MyStruct)..., :classvariable)
4 Likes

I would choose to create a new type that holds the common fields and include an instance of this in each of the structs that needs the common fields. As Classes.jl said.

The following is a pure julia implementation.

struct SharedVariables
    a::Int
    b::Vector{Int}
end

const shared = SharedVariables(1,[1,2])

struct Mystuct
    shared::SharedVariables
    c::Int
    function Mystuct(c)
        new(shared, c)
    end
end

x = Mystuct(9)
x.c # 9
x.shared.a # 1
1 Like

But please don’t do this! (adding getproperty) methods.

The performance is bad, its not very composable and will give you a lot of pain when you want to refactor. It should be considered an anti-pattern unless you have very good user-experience reasons for it.

Instead of using hacks to get OOP patterns, learn multiple dispatch. Many of us tried to follow OOP styles when we came to Julia and later removed it all.

12 Likes

The getproperty stuff is taken care of at compile time. Usually no performance problems. It compiles to a mov.

2 Likes
x = Mystuct(9)
x.c # 9
x.shared.a # 1

Additionally, accessing fields directly like this will get in the way of object composition. Outside of the simplest cases its better to access fields with methods:

a(x) = x.a

Then you can refactor out shared objects and do this:

a(x::MyStruct) = a(x.shared)

And the rest of your code will keep working without change.

This is not necessarily true except in the simplest of cases. I have seen massive type instability in code bases using oop style getproperty in big if/elseif blocks.

Its making const propagation and other compiler optimisations a lot harder than it needs to be, and at some point that will have a cost.

1 Like

I can imagine that. If it’s not run at compile time there will be problems. It should perhaps be marked with Base.@assume_effects :foldable instead of @inline. (It will be called with a compile time field name if called from a dot notation access.) And used for fields, not for calling funny functions (which might be tempting for OOP people).

Otherwise I agree that it’s not a universal OOP emulator, but for this particular use, class variables, I think it’s quite useful.

The fact our compiler can “give up” on resolving types (and just box instead) needs to be constantly accounted for and avoided. But yes marking as :foldable may help, but maybe not if you have some type instability or union fields

Thank you all for your very reactive attitude and numerous and various answers. Of course it will take some time before I can read (and understand) every point of view and suggested attitude. I’m not in a rush and I will take time. Of course I will add some message asking for additional information if I find something not clear.
In my country they allow till end of January for New Year wishes. So I will use this possibility. Happy New Year everybody.
Gilles

1 Like

Very interesting suggestion, thanks !

1 Like

Yes, I know. That’s why my question. I’m looking for a clean ‘mimic’ not to transform Julia into a Python dialect.

OOP is an intersesting and useful concept although I understand that in some cases it can be a constraint and even confusing. So the good attitude is to use it whenever it is suited. A bit of OOP style can improve the structure of a program.

Whe, it comes to polymorphism ,multiple inheritance, freindship between classes, etc Java and C++ and the rest begin to be quite obscure.

It’s possible for getproperty to be efficient. But it’s also easy to get uninferrable code. This took a little work to get right in DotCall.jl

A lot of Julia code works well without these OOP conveniences. But sometimes it would make things much simpler and more convenient to have them. I’m tempted to use them sometimes, but never do because it’s not idiomatic Julia. I disagree with the idea that you lose nothing by giving up the class-based OOP conveniences. This has been discussed quite a few times in this forum.

1 Like

As many have already pointed out, it is possible to emulate OOP, but it is generally an antipattern when multiple dispatch with type hierarchy would be a better fit. I would also strongly recommend having a look at Hands-On Design Patterns and Best Practices with Julia for learning how code can be organised in Julia.

There are many ways to approach this in Julia, depending on what sharing means. When shared variables are supposed to be immutable, one can create an ordinary parameter struct and add it to the instance when it is constructed. If all instances always need to share the same value, then a global constant would be a better fit.

In case sharing means that one should be able to listen or even modify the thing, then Ref can be used as well as mutable struct (remember about using locks though). If all instances share the same resource, like a database, GUI window instance, or HTTP router, then putting those things in a global variable makes sense. The module namespace then serves the same purpose as the class singleton.

When these options become limiting and repetitive, I would recommend looking into macros.

3 Likes

A method is just as good for getting information associated with a type like a class variable does and it doesn’t lose in performance. If it’s a type-wise constant or a freshly computed value, then it doesn’t even need to be cached separately from the type. Happens all the time in Julia e.g. eltype.

1 Like

Interesting discussion on the inference, reminds me of Mapping a 'getter' - #16 by Marco_Antoniotti . I guess the problem, and the difference with very fast struct.field is that getproperty cant dispatch on the value of symbol. The constant propagation is neccesary for speed, and constant propagation cannot be counted on (yet) it is only offered as a convenience when it works.

Buuuut it seems to get better every release! Is there a chance that the antipattern will become a pattern in the future? Until then there is Lazy.jl and @forward and other ways of mimicing accessors that can dispatch.