This post is for educational purposes only.
Someone asked in my Julia Encapsulation post how to code a more conventional object-oriented class in Julia. If you ignore class inheritance, then I assume we are talking about a composite type with methods inside that you can call via object.method(args...)
.
Note that in Julia a method is an instance of a function. In this post I might be using the terms a little interchangable, due to my mental confusion with some other OO language terminology of methods vs functions. I will also use type vs class vs struct definitions interchangeable, but an object is an instance of a type.
Pseudo-code of a Julia type with contained methods would look like:
mutable struct MyClass
myInt::Int
function print_int(self::MyClass)
println("hello, I have myInt: $(self.myInt)")
end
function set_int!(self::MyClass, new_int::Int)
self.myInt = new_int
end
function MyClass(int::Int)
return new(int)
end
end
Which you then call via:
obj = MyClass(5)
obj.set_int!(8)
obj.print_int()
The struct definition syntax above is actually possible in Julia, but doesn't do what you want. The inner methods are not available as properties. You will get the following error:
julia> obj.set_int!(8)
ERROR: type MyClass has no field set_int!
What's the closest we can get?
I think we have two options available:
use immutable fields of type function inside the struct
define a custom getproperty
method that returns functions
Let's try them both and see how they feel.
Simple solution: we turn the functions into immutable fields and set them in the constructor. Please consider all possible horrors if you accidentally make these fields mutable and the user inserts a custom function into your field.
I will place the code inside a module, which is typically how you will share your types anyway. This also shows the one advantage of using the dot syntax to access functions. Now you do not have to export the functions or call them via the module namespace, the type itself carries the functions.
module MyModule
mutable struct MyClass
myInt::Int
# we have these `const` fields since Julia 1.8
const print_int::Function
const set_int!::Function
function print_int(self::MyClass)
println("hello, I have myInt: $(self.myInt)")
end
function set_int!(self::MyClass, new_int::Int)
self.myInt = new_int
return self
end
function MyClass(int::Int)
obj = new(
int,
()->print_int(obj),
(new_int,)->set_int!(obj, new_int),
)
return obj
end
end
end
This code works and the inner functions remain somewhat hidden, you cannot directly access them in the module. You can also move the functions outside the struct definition, and into the module. The result will be similar. The difference is that the functions are no longer hidden, you can access them via MyModule.print_int(obj)
.
julia> obj = MyModule.MyClass(5)
Main.MyModule.MyClass(5, Main.MyModule.var"#1#5"{Main.MyModule.var"#print_int#3"}(Core.Box(Main.MyModule.MyClass(#= circular reference @-3 =#)), Main.MyModule.var"#print_int#3"()), Main.MyModule.var"#2#6"{Main.MyModule.var"#set_int!#4"}(Core.Box(Main.MyModule.MyClass(#= circular reference @-3 =#)), Main.MyModule.var"#set_int!#4"()))
julia> obj.print_int()
hello, I have myInt: 5
julia> obj.set_int!(8).print_int()
hello, I have myInt: 8
For extra fun I made the set_int!
return the mutated object, so we can chain the function calls. This is extremely convential looking OO syntax. I did keep the exclamation mark !
for mutating function names. This is Julia after all.
Downside of this field::Function
approach is that by default you get a lot of circular references in the REPL display. We can get rid of the circular reference display by defining a custom Base.show
function. Here's a straightforward attempt:
function Base.show(io::IO, obj::MyModule.MyClass)
print(io, "$(typeof(obj))($(obj.myInt))")
end
Now it will display more conventially:
julia> obj
Main.MyModule.MyClass(5)
In this approach we will return functions when calling the getproperty
method on our custom type. Note that we need to make the functions available in the module scope, so no hiding of functions.
module MyModule
mutable struct MyClass
myInt::Int
end
function print_int(obj::MyClass)
println("hello, I have myInt: $(obj.myInt)")
end
function set_int!(obj::MyClass, new_int::Int)
obj.myInt = new_int
return obj
end
function Base.getproperty(obj::MyClass, prop::Symbol)
if prop == :myInt
return getfield(obj, prop)
elseif prop == :print_int
return ()->print_int(obj)
elseif prop == :set_int!
return (new_int,)->set_int!(obj, new_int)
else
throw(UndefVarError(prop))
end
end
end
Now you can access the functions again as properties:
julia> obj = MyModule.MyClass(5)
Main.MyModule.MyClass(5)
julia> obj.set_int!(8)
Main.MyModule.MyClass(8)
julia> obj.print_int()
hello, I have myInt: 8
Either of the options above could be automated away with a macro. Let's call that macro @class
. I will not meta-program that macro here, but then it could look like the pseudo-code at the start. Everything is possible in Julia if you really want it. Some say that's an advantage, some say that's a disadvantage.
module MyModule
@class mutable struct MyClass
myInt::Int
function print_int(self::MyClass)
println("hello, I have myInt: $(self.myInt)")
end
function set_int!(self::MyClass, new_int::Int)
self.myInt = new_int
end
end
end
What is considered OO? The Rust manual section "What is OO" states:
Objects contain data and behavior. Confirmed in this post.
Encapsulation that Hides Implementation Details. Discussed in a previous post.
Inheritance as a Type System and as Code Sharing.
Actually Julia objects already contain behavior via multiple dispatch. All I did was implement the syntactic sugar often provided with class-based object oriented programming languages. This might give you more feeling of belonging.
I did not try to write an implementation for class inheritance. Maybe it can be done with some very complex meta-programming. But like the Rust language, a trait based approach is more suitable for Julia, see for example discussions in Traits.jl.
To repeat: I wrote this code for educational purposes only. I would not advise to use such coding patterns in large scale Julia code bases. Please stick to exporting (public) methods and keep the style more Julian. But perhaps this post can help you better understand Julia, especially if you have only been exposed to strictly class-based object oriented programming styles.