I have to parse a configuration file and run a simulation based on the settings in that file. In some language like Python, I’d probably make a class, say ConfigParser and it would both parse and store my configuration. Since I can’t rest on all of my old bad habits, I figured I’d ask for advice on new ways to do old things.
I guess I’m looking for a Julian way of doing things, not just something that doesn’t provoke the compiler to throw things.
I could use global variables
Global variables have obvious problems (not to mention it winds up being
state::Dict = Dict()
fh = open(fname)
# ... read file, stuff info in state
# ... run simulation based on state
I could use inner functions (closures?)
Also has problems, and my co-workers aren’t fond of.
# ... read file, stuff into `state`
state = Dict()
fh = open(fname)
run_simulation() # run simulation based on `state`
Chain things together somehow?
dict = Dict()
# ... read config, store in dict
# based on configuration, run stuff
read_configuration("config.cfg") |> run_simulation
All the Julia solvers I’m aware of (NLOpt, Optim, DiffEq, Trixi, …) use something like the third approach, where
read_configuration returns a struct (if possible) rather than a dict, allowing more optimizations to be applied. This is related to the function barrier performance tip. If there are a number of fundamentally-different configurations that could be simulated (say, 3D Cartesian vs. 2D r-z axisymmetric, or with vs. without body forces), those differences can be encoded in the type domain, and you’d then define multiple
simulate methods, e.g.
Well, I always use a mutable struct for my configuration, which I store in a global const variable.
You can actually modify const mutable structs, so if I want to run the simulation based on a configuration file, but vary one parameter I can easily do that…
I pass this variable, named se (like settings) as first parameter to my functions. Sometimes I create
a deep copy of the global settings, modify the copy and then call one of my functions.
I store the configuration on disc using a .yaml file which is easy to read and write for humans.
I’m glad to hear that at least someone else uses global const values … I know that they can cause performance issues if they are
Any typed. Sometimes I feel like I am overcorrecting in favor of having few arguments - since there was a codebase I had to work on for a long time that had
fuction(way, way, way, too, many, darn, positional, arguments, good, greif) … and no, that is not an exaggeration, one function (in Matlab) had 11 positional arguments.
Imho, the third approach should always be supported. It just gives more flexibility, i.e., when running simulations across multiple different configurations etc.
In case, a good default is available it can be combined with global state:
Having a non-exported global with getter (and possibly setter) functions and default methods. E.g.,
randuses this approach:
rand() # use default
Again using some non-exported global state and providing a convenient way to execute code with different values:
const global_cfg = Ref(:whatever)
function with_config(body, cfg)
old = global_cfg
global_cfg = cfg
global_cfg = old
export with_config # but not the actual global Ref
run_simulation() = run_simulation(global_cfg)
run_simulation(cfg) = ...
Note that this pattern is very similar to scoped values and similarly implemented by
This is not true. They CANNOT be any typed if they are defined as const. They get
a concrete type on first assignment, and you cannot change the type afterwords.
Only disadvantage: You need to restart Julia if you extend your configuration struct
and add new parameters.
Too complicated for a simple scientist. And not needed unless your work in a team on a large piece of software.
I could be wrong about the intent of scoped values in Julia, but they seem like they were meant to be like parameters in Lisp/Scheme? They can in some cases make code cleaner, safer.
Yes, these are basically known as dynamic variables in Common and Emacs Lisp. Especially, in Emacs they are put to good use enabling a highly extensible/configurable system.
The main discussion nowadays seems to be how they should best interact with Threads or Tasks.