I am trying to solve an economic model. The algorithm involves an iterative two-step procedure, like a back-and-forth optimization. There are lots of inputs and moving parts. I created a struct to keep track of it all. What follows involves psuedocode rather than a MWE.
struct MyModel
A
B
C
D
end
The algorithm works like this: (1) holding B fixed, solve for new A, (2) holding new A fixed, solve for new B. Repeat. Each step takes as input potentially all fields of MyModel (and there many and of various types).
I created a functor that when called should initiate the algorithm and produce the solution (which is just A and B after convergence). However, inside that function I call functions that do each step of the algorithm â also in place.
function updateA!(m::MyModel)
# do stuff to get newA
# updates A in place (using Setfield.jl)
@set m.A = newA
return nothing
end
function updateB!(m::MyModel)
# do stuff to get newB
# updates B in place (using Setfield.jl)
@set m.B = newB
return nothing
end
So that the functor method actually is like this
function (m::MyModel)(;kw...)
chg = Inf
while chg > tolerance
updateA!(m)
updateB!(m)
chg = ...
end
# A and B updated in place so returns nothing
return nothing
end
I am wondering what happens in this situation. Is it really using the same m object everywhere? Do I need to worry about unexpected behaviors here, which could be very very difficult to debug? I think of functors as operating on or within themselves (perhaps this is completely incorrect), so it feels weird to âpassâ m to another mutating function as part of this process.
My initial goal of this approach was to keep the code clean, to eliminate unnecessary creation of temporary variables (eg. elements C, D, etc. are shared between the two steps), and ultimately to have this be performant. I am open to suggestions if this is a poor design decision.
The use of immutable datastructures come from the goal of mimimizing the amount of state you are keeping.
State is hard to think about.
So one tries to write code that only assigned each variable once, and never updates it.
Updating a variable cases that same variable âmeansâ something different in a different part of the code.
Thus your code gets a implict depenence order, so you canât move lines about without working out where state changes occur.
Where as if you only assign each once, you can move lines about, but if you move them such that something isnât assigned before it is used, you get an error.
Creation of temporary variables is cheap (free infact).
It is the allocation of memory (esp on the heap) that is (moderately) expensive.
You can just have local variables C and D and pass them into the functions that need them.
This is clearer also, because now its transperent that those functions need those inputs (and not anything else you didnât pass to it).
So one can look at it and see what it used.
Eh, I didnât notice that yet about Setfield.jl. Thanks for letting me know.
Regarding the other stuff, C and D are just stand ins. The actual number of things is nearing 20. It is stuff that I precompute so that the actual updateA!-type functions do a minimal amount of work. Perhaps I could separate that off (the stuff that is never updated once created) to be a different struct that gets passed around. I want to keep as much of it together as possible because it is not just used in the solution. Lots of it will continue to be in use afterwards in simulation and testing.
mutable struct EconomicProblem{A, B, C, D, ...}
A
B
C
D
...
end
function solve!(model::EconomicProblem)
Î = Inf
while Î > tolerance
Îâ = updateA!(m)
Îâ = updateB!(m)
Î = ...
end
end
Thatâs a pretty natural structure and sound strategy. I would use a mutable struct for updating the values in-place. I would also make he updateX! methods return the components to determine convergence directly. For performance, either specify the types in the struct definition or if they might differ or use parametrization.
In principle, you can make this work, and it is the same m inside the mutating callable.
But in practice, I would suggest using a functional style:
group fixed parameters (eg C, D, âŚ) into an (immutable) struct, which we call params below,
define solve_for_A(params, B) to get A, and similarly solve_for_B(params, A),
write a function solve_iteratively(params; A = initial_guess_A(params)) that performs the loop above.
Code like this is usually easier to reason about. You can later switch to re-using pre-allocated A and B, eg solve_for_A!(A, params, B), but the performance gains are usually not worth the complexity.
The normal software engineering advice would be to rearrange your program such that each function only had at most 7 variables.
Which one migh accomplish by drawing the boundries between functions differently.
Or by grouping some into structs with 7 or fewer fields, where that struct could be thought of as a single entitiy.
e.g. the train function takes a single argument optimizer_settings which it passes to the optimizer without looking inside (thus counting as 1). The optimizer takes that and does look inside counting as (say) 5 of its variables, with the loss and the parameters being tuned making uop to 7.
But I do appreciate this may not be a reasonable expectation for some scientific code.