Hello,
I am used to having an object containing configuration variables for my project.
For instance, in Java, I use static singletons to ensure those configuration variables are instantiated only once for the whole life-cycle of the program and that they are available anywhere in the code.
In Python, this is the same as having a module – i.e. a file – containing variables, since the module loading system ensures that each module is 1) a global object instance 2) not instantiable 3) imported only once 4) accessible from anywhere
In Julia, I see two options.
use the PatModule package to mimic the Python behavior
use a constant instance of a struct that makes itself not instantiable anymore:
module Settings
mutable struct SettingsStruct
a::Int64
b::Int64
function SettingsStruct(a, b)
eval(:(SettingsStruct(a, b) = error(
"Cannot instantiate SettingsStruct twice")))
return new(a, b)
end
end
const s = Settings(2, 3)
end
Such a method allows having a constant global variable named s which contains my settings. Anywhere in the code, I am sure that s was not replaced by anyone, neither in interactive sessions.
I would just like to receive opinions about these two alternatives and other
possible solutions.
It’s one of the design patterns in the Gang of Four.
It’s useful if you have some resource that must not be instantiated twice by the user, for instance, consider the following function:
module Settings
struct SettingsStruct
a::Int64
b::Int64
function SettingsStruct(a)
eval(:(SettingsStruct(a) = error(
"Cannot instantiate SettingsStruct twice")))
return new(a, a^2)
end
end
const settingsStruct = SettingsStruct(4)
const settingsDict = Dict(a => 4, b => 5)
end # module
module Library
using Settings
function test1(a::Int64, conf::SettingsStruct)
print("I'm doing stuffs with $a and $(conf.b)")
end
function test2(a::Int64, conf::Dict)
print("I'm doing stuffs with $a and $(conf["b"])")
end
end # module
Then the code of the user
module UserCode
using Library
using Settings
d = Dict("g" => 5)
test1(3, Settings.settingsStruct)
test2(3, d)
end # module
As you see, test2 will fail, because the user has instantiated a wrong d.
If SettingsStruct could be instantiated twice, the user could pass new
wrong values.
Your solution looks reasonable to me, except that redefining the constructor is very hackish (and do you need the struct the be mutable?).
You can instead check if the variable s is defined:
module Settings
struct SettingsStruct
a::Int64
b::Int64
function SettingsStruct(a, b)
isdefined(Settings, :s) && error("Cannot instantiate SettingsStruct twice")
return new(a, b)
end
end
const s = SettingsStruct(2, 3)
end
However in this case I would just use a const with no additional check: you already get a warning if you change it:
WARNING: redefinition of constant s. This may fail, cause incorrect answers, or produce other errors.
But yeah, I do wish it would give an error instead. The current behavior seems designed for playing around and prototyping rather than building robust applications.
Note that this kind of definition doesn’t really work that well in julia, as functions are not bound to instances of structs that they take as arguments. Simply instantiating SettingsStruct doesn’t make anything happen other than running the constructor function. If that is side effect free, it’s unnecessary to restrict creation of a new one (in fact, it may be preferable to do so, since being able to change settings on-the-fly and passing them around explicitly makes unit testing much easier and parallelizable).
Usually this is done with a const SETTINGS = Ref(SettingsStruct(...)) if you want to be able to change the settings, or just const SETTINGS = SettingsStruct() if you don’t, though this approach will require careful handling in multithreaded code that also modifies SETTINGS, which is why it’s better for that usecase to explicitly pass the “secret state” around. If you only read from SETTINGS, having a const global is not wrong though.
If Library doesn’t even provide the second method, the user will get a MethodError instead, signaling that they’ve done something wrong.
What do you mean by that? Are you thinking of keeping extra secret state per function call? If so, it’s much more user friendly to not do that and ask the user to pass in what you want to modify in the function.
Note that you cannot really ensure this with Julia. No matter how you plan to safeguard this, someone can always just eval into a module, etc.
I would just do
module Settings
export SETTINGS # note: only export
mutable struct _SettingsStruct
a::Int64
b::Int64
end
const SETTINGS = _SettingStruct(2, 3)
end
ie not export the constructor. Sure, the user can always access SettingsStruct, but that requires importing or an explicit module path, which should be warning enough, and also easy to grep the code for.