The first phase - getting the Julia syntax right - was rather easy - as long as everything is kept in a single file.
Then one (me) hits a wall - How to do the stuff below?
Want to use functions in several files?
In Matlab:
add path to path.txt
save function or class in a file with the same name, easy
In Julia: learn about include, import, module - steeper but feasible.
Want to write “functions with own memory” aka Data Structures?
In Matlab, use one of:
persistent variables
local variables with nested functions
a class with local variables (“properties”)
In Julia:
Text books and tutorials appear too basic and do not show fully developed functions.
Looking at DataStructures.jl is not helping either, since it is already 2 steps ahead.
(Another problem is that it uses a dict as base structure with its own limitations.)
So there remains a gap to be closed.
How to improve?
are there good sources for someone with a Matlab-like background?.
someone interested in adding a chapter “from Matlab to Julia”?
I would be happy to help with examples.
For example, how to correctly code the following?
struct mystruct
a
b
end
function add1(s::mystruct)
c = 10
a + b + c
end
function sub1(s::mystruct, d)
a - b + d
end
s1 = mystruct(1, 2)
add_result = add1(s1)
sub_result = sub1(s1, 100)
Code organization in Julia is definitely more complex than in Matlab, but the extreme simplicity of Matlab in this regard is both a strength (for absolute beginners in programming) and a liability (for everyone else).
However for very basic code it’s not that different: instead of adding to path (Matlab) you need to include the file that declares a function. It seems similar to me?
But it’s true the user is quickly exposed to modules and packages and that makes it a bit more overwhelming for beginners…
Regarding your code example: can you show how you would write this in Matlab? This way we can start with working code (in Matlab) and make a direct comparison.
Thanks sudete for taking this!
Example Matlab code below with a mix of object-owned/function-owned/external data.
classdef_example.m
classdef example_class < handle
properties
a
b
end
methods
function this = example_class(a,b)
this.a = a;
this.b = b;
end
function y = add1(this)
c = 10;
y = this.a + this.b + c;
end
function y = sub1(this, d)
y = this.a - this.b + d;
end
end
end
example_class_test.m
function example_class_test
s1 = example_class(1, 2)
add_result = add1(s1)
sub_result = sub1(s1, 100)
end
Note the main difference between class-based OO and Julia’s multiple dispatch approach: Instead of defining data and methods inside a class, in Julia you define a new type (“struct”) and then define functions (or more precisely, “methods”) that operate on that type. You can even extend existing functions and operators; for example:
julia> import Base.show
julia> function show(io::IO, x::ExampleClass)
println(io, "An instance of @Bardo's Matlab example class")
println(io, " a = $(x.a)")
println(io, " b = $(x.b)")
end
show (generic function with 269 methods)
julia> ExampleClassTest()
┌ Info: ExampleClassTest results:
│ s1 =
│ An instance of @Bardo's Matlab example class
│ a = 1
│ b = 2
│
│ add_result = 13
└ sub_result = 99
or
julia> import Base.*
julia> *(s1::ExampleClass, s2::ExampleClass) = ExampleClass(s1.a * s1.b, s1.a * s2.b)
* (generic function with 329 methods)
julia> ec1 = ExampleClass(2,4); ec2 = ExampleClass(3,3);
julia> ec1 * ec2
An instance of @Bardo's Matlab example class
a = 6
b = 12
Thanks a lot mbaz!
So close… I could swear I tried this too
But seriously, debugging is hampered when changing the definition of a struct.
Julia keeps it despite Revise.jl. Any alternative?
Thanks again.
One word of encouragement: while some things that are super simple in Matlab require a bit more setup and preparation in Julia, as an ex-Matlabber I can tell you, after a few weeks (or even days!) it’ll be painful to contemplate going back to Matlab.
# test1.jl (ok)
mutable struct mystruct
a
b
end
function add1(this::mystruct)
c = 10
this.a + this.b + c
end
function sub1(this::mystruct, d)
this.a - this.b + d
end
s1 = mystruct(1, 2)
add_result = add1(s1)
sub_result = sub1(s1, 100)
@info "myfun results:" s1 add_result sub_result
but
# test2.jl (buggy)
mutable struct mystruct2
a
end
function pushx!(this::mystruct2, d)
push!(this.a, d)
end
function popx!(this::mystruct2)
pop!(this.a)
end
s1 = mystruct2 # initially empty
pushx!(s1, 1)
pushx!(s1, 2)
a = popx!(s1)
b = popx!(s1)
@info "myfun results:" s1 a b
# test2.jl (ok)
mutable struct MyType2
a::Vector{Int}
end
MyType2() = MyType2(Int[]) # constructor for empty input
function pushx!(this::MyType2, d)
push!(this.a, d)
end
function popx!(this::MyType2)
pop!(this.a)
end
s1 = MyType2() # initially empty
pushx!(s1, 1)
pushx!(s1, 2)
a = popx!(s1)
b = popx!(s1)
@info "myfun results:" s1 a b
% fun_test.m
function fun_test
cmd = {@fun1, 1, 2};
cmd{1}(cmd{2:end});
end
function fun1(x,y) % example did not work with anonymous function def
z = x + y
end
>> fun_test
z =
3
Julia worked with a single-parameter function, but not with two parameters:
julia> fun1(x,y) = x + y
fun1 (generic function with 1 method)
julia> cmd = [fun1, 1, 2]
3-element Vector{Any}:
fun1 (generic function with 1 method)
1
2
julia> cmd[1](cmd[2:end])
ERROR: MethodError: no method matching fun1(::Vector{Any})
Closest candidates are:
fun1(::Any, ::Any) at REPL[30]:1
Stacktrace:
[1] top-level scope
@ REPL[32]:1
so that works (those dots are called “splatting”). But that code organization is really, really strange. You should not mix a vector of data with a function, that will completely break the performance of the code.
Yeah, in the end its a (f)eval in disguise.
But say, in an event-based simulation, there arises the need to execute functions “later”.
Means you have to store a function with parameters.
In some cases it is possible to evaluate, store the result and apply it later.
But this does not cover the general case that some action (function) will be triggered in the future.
Would it help to specify that the first argument is always a function handle, all others floats?
First timings with a random “event-based simulation” setting show wild differences.
The "regular struct approach apparently is not able to implement variable functions.
# store and evaluate functions with their parameters
n = 1e3
if 1==0
# throws ERROR: LoadError: UndefVarError: fun not defined
println("as regular struct")
struct A1{Fun, Par}
fun::Fun
par::Vector{Par}
end
@time for i = 1:n
if randn() > 0
fun(par) = prod(par)
a = A1(fun,[randn(), randn()])
else
fun(par) = sum(par)
a = A1(fun,[randn(), randn(), randn()])
end
a.fun(a.par)
end
end
println("as callable struct")
struct B{Par}
x::Vector{Par}
end
@time for i = 1:n
if randn() > 0
(b::B)() = prod(b.x)
b = B([randn(), randn()])
else
(b::B)() = sum(b.x)
b = B([randn(), randn(), randn()])
end
b()
end
println("as tuple")
@time for i = 1:n
if randn() > 0
e = (prod, [randn(), randn()])
else
e = (sum, [randn(), randn(), randn()])
end
e[1](e[2])
end
println("as array")
@time for i = 1:n
if randn() > 0
e = [prod, [randn(), randn()]]
else
e = [sum, [randn(), randn(), randn()]]
end
e[1](e[2])
end
as callable struct
10.034759 seconds (3.65 M allocations: 200.442 MiB, 0.30% gc time, 94.55% compilation time)
as tuple
0.000280 seconds (5.53 k allocations: 188.922 KiB)
as array
0.000312 seconds (6.49 k allocations: 281.203 KiB)
In the case of callable struct approach the compiler seems to try to hard-code or unroll.
Not the best approach here. Just ran a 1k loop. Typical would be > 1e6.