I’ve been reading about the one question that always comes up for people using Julia after coming from an OOP language with traditional classes and methods. Is Julia's way of OOP superior to C++/Python? Why Julia doesn't use class-based OOP?
That thread has great information about it, but it’s tackling conceiving code, not converting to Julia.
I’ve recently found a pretty interesting FOSS project which does optical systems simulation. I know Julia has its own physics libraries and community projects on the subject, but this has been a tried and tested project and I can’t help but wonder how would it be done in Julia without the traditional classes. The project is called Robochameleon.
As a practical example, here’s a module made using the object structure from it (Robochameleon/BranchSignal_v1.m at master · dtu-dsp/Robochameleon · GitHub)
BranchSignal_v1 is a unit, it has a few properties, it has its constructor and the traverse function, which is called when the block needs to execute its part of the chain during processing.
Bigger units can contain smaller units, like how BS_1xN, a beam spliiter, contains a BranchSignal definition in traverse.
Another meaningful example is SimpleAWG (arbitrary waveform generator) which contains a WaveformGenerator and a DAC unit.
I think this type of structure is fairly natural and intuitive: you connect unit outputs in a chain and each call their
traverse functions when necessary.
If I were to (hypothetically) port the project to Julia, how would the code structure look like? How would I mimick the concept of chaining blocks/modules/units together?
Thanks in advance!
Taking a look at your examples, using structs and functions that take the structs (and other arguments). You could port it without changing much of the underlying structure. For classes with non-trivial constructors you can define a functor that returns the struct.
Something a bit similar that you might want to look at is Acausal Component-Based Modeling the RC Circuit · ModelingToolkit.jl. Its a different purpose but has individual components and connects them. It also does a lot of math to simplify the system underneath.
Thanks for that link. It does create components (as functions) and links them together (by knowledge of how system variables, current and voltage in this case, interact with each other). This does seem like bigger components can be created by having a new function call these smaller functions and use
connect_pins to return that bigger system’s I/O equations (
eqs, in the sample code there)
What bothers me about this specific example is the number of non-Base Julia required to do the modelling. Yes, I understand it’s a Toolkit, which usually comes with a sub-language to use, but it’s a bit weird to see all those @ macros and additional equation packages at first.
Anyways, I still think it’s a great example I’ll definitely come back to while trying to think Julia in these OOP situations.
I used ModellingToolkit because its very fleshed out and has a focus on performance.
If I understood correctly the original project has some sort of recursive way of calling traverse, like you call traverse on the input and it does something to it and passes the value forward by calling traverse on another object? You could do that by having a struct that has the previous components inside it. GitHub - FluxML/Tracker.jl: Flux's ex AD does something similar to this by creating a a list of operations and then pulling back in order to do Reverse Mode Automatic Differentiation
One of the key components that Julia brings is Automatic Differentiation, and that is surely extremely powerful for modelling dynamic systems.
I wonder if such tools are necessary for things like signal processing. Robochameleon almost certainly (as far as I’ve delved into packages) doesn’t use differential equations, so it’s a simple matter of getting a signal vector, applying some functions (no derivates or integrals) and outputting another vector or a value.
This makes me believe I can be inspired by these tools, but take away everything that regards AD. So instead of functions returning equations that can be solved, they can return the values themselves. Then, as you mentioned, I can make a list of operations and
traverse through that list by calling the appropriate function list for the application I want.
To summarize, maybe the last thing I’ll need is a configurations
struct which will take care of any properties the blocks inside Robochameleon have. The rest can be pure functions which receive those configurations and process the signal.
Something which has a constructor and a single method sounds a lot like a functor to me, and that could be a nice way to set up your code. For example:
julia> struct Adder
julia> function (adder::Adder)(in)
adder.x + in
julia> struct Multiplier
julia> function (mult::Multiplier)(in)
mult.x * in
julia> a = Adder(1)
julia> m = Multiplier(2)
Now that we’ve set things up in this way, chaining modules is exactly the same as chaining functions:
You can use the
|> operator to write things in order from input to output, which may be more natural for pipelines:
julia> 5 |> a |> m
And you can use the
\circ operator to compose functions together:
julia> ma = m ∘ a
Multiplier(2) ∘ Adder(1)
I’m new to the concept of functor (next thing on the list!) but it does seem to summarize exactly what I just expressed.
Then wrappers can be a combination of functors chained in an appropriate order and provide some sort of hierarchy. Awesome!
If instead of composing functions you want to compose objects you can do this
julia> struct component
julia> struct Start
julia> struct Doubler
julia> myfun(obj::Doubler,in) = 2*in
myfun (generic function with 1 method)
julia> struct Addval
julia> myfun(obj::Addval,in) = in + obj.val
myfun (generic function with 2 methods)
julia> function traverse(comp,in)
if comp.before == Start
return myfun(comp.self, in)
previous = traverse(comp.before,in)
return myfun(comp.self, previous)
traverse (generic function with 1 method)
Which allows you to do something like this
julia> doubler = Doubler()
julia> add5 = Addval(8)
julia> a = component(doubler,Start)
julia> b = component(doubler,a)
component(Doubler(), component(Doubler(), Start))
julia> c = component(add5, b)
component(Addval(8), component(Doubler(), component(Doubler(), Start)))
16 + 12im
I came back to thank you once again. I’ve got this thread in my bookmarks and I always come back to it to get inspiration on the two methodologies.