Bridge the gap, please! (from Matlab to Julia)

Coming from Matlab, my experience with Julia:

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)
2 Likes

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.

8 Likes

This might be helpful: https://cheatsheets.quantecon.org/

For your code example:

function add1(s::mystruct)
    c = 10
    s.a + s.b + c
end

function sub1(s::mystruct, d)
    s.a - s.b + d
end

Regarding “functions with own memory”, I believe you will need to make your structs mutable

mutable struct mystruct
    a
    b
end
1 Like

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

>> example_class_test
s1 = 
  example_class with properties:

    a: 1
    b: 2
add_result =
    13
sub_result =
    99

Here you go:

mutable struct ExampleClass
    a
    b
end

add1(x::ExampleClass) = x.a + x.b + 10

sub1(x::ExampleClass, d) = x.a - x.b + d

function ExampleClassTest()
    s1 = ExampleClass(1, 2)
    add_result = add1(s1)
    sub_result = sub1(s1, 100)
    @info "ExampleClassTest results:" s1 add_result sub_result
end

And then:

julia> ExampleClassTest()
┌ Info: ExampleClassTest results:
│   s1 = ExampleClass(1, 2)
│   add_result = 13
└   sub_result = 99

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
4 Likes

Thanks a lot mbaz!
So close… I could swear I tried this too :wink:
But seriously, debugging is hampered when changing the definition of a struct.
Julia keeps it despite Revise.jl. Any alternative?
Thanks again.

Yeah, redefining types is tricky. The best approach is to wrap your code in modules; see Workflow Tips · The Julia Language

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.

8 Likes

When on initial prototyping you can use named tuples:

julia> s = (a = 1, b = 2)
(a = 1, b = 2)

julia> s.a
1

julia> s.b
2

There is actually a package to help with that:

But at sometimes there is not way out (still) from restarting the REPL.

3 Likes

Hm, struggling elsewhere for the moment.

For the record:

# 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

Debugger messages are not always conclusive.

You’ve assigned the type mystruct2 to the variable s1, not created an instance of that type. You probably want mystruct2() here instead.

2 Likes

To push to a you need it to be a collection. This is how that works:

julia> struct MyStruct
         a::Vector{Int}
       end
       MyStruct() = MyStruct(Int[]) # constructor for empty input
MyStruct

julia> pushx!(this::MyStruct,d) = push!(this.a,d)
pushx! (generic function with 1 method)

julia> popx!(this::MyStruct) = pop!(this.a)
popx! (generic function with 1 method)

julia> s1 = MyStruct()
MyStruct(Int64[])

julia> pushx!(s1,1)
1-element Vector{Int64}:
 1

julia> s1
MyStruct([1])

julia> popx!(s1)
 1

julia> s1
MyStruct(Int64[])

2 Likes

You guys rock!

# 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
1 Like

Possibly related to be a Vector or not to be

Matlab:

% 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

You should do this:

julia> cmd[1](cmd[2:end]...)
3

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?

two options:

# regular struct
julia> struct A{F,T}
         f::F
         x::Vector{T}
       end

julia> f(x) = sum(x)
f (generic function with 1 method)

julia> a = A(f,[1,2])
A{typeof(f), Int64}(f, [1, 2])

julia> a.f(a.x)
3

#or a callable struct

julia> struct B{T}
         x::Vector{T}
       end

julia> (b::B)() = sum(b.x)

julia> b = B([1,2])
B{Int64}([1, 2])

julia> b()
3




1 Like

Excellent. My toolbox is growing.

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.

This is still an odd design choice.

The way you would handle this in the most “julian” way, imo, would be do have a function that takes in a function.

function operate(f, b::B)
    f(b.x)
end
1 Like

Thx for your suggestion,
Not idea how to apply.
Could you add your way to the timing comparison?