Hi, I’m writing some simulation code in which I run simulations for various choices of parameters and save the results to some files, where file-names are generated automatically by collecting the parameter names and values and putting them in a string. However, if there are many parameters, these file names become very long, making visual inspection harder.
As each parameter has a default value in my problem, I want to omit those parameters that take on their default value from the file name, to make the names a bit shorter. This can be achieved by a sequence of if statements, but this is not very ergonomical. Is there a way do this automatically, by looping over all arguments that have default values? Here is a MWE
function Foo(; a = 1, b = "yes", c = true)
registered_name = ""
if a != 1
registered_name *= "a=$a,"
end
if b != "yes"
registered_name *= "b=$b,"
end
if c != true
registered_name *= "c=$c,"
end
display(registered_name)
# Actual stuff happens here, of course
end
Foo()
Foo(a = 2, b = "Yes!")
Foo(c = false)
Then you at least spare the three-line if things – and the bracket on the left can of course also be interpolated and hence put a few things in one line together as well
Thank you, that does save some code length; however, it still requires a lot of manual typing. I was hoping to find a way to do this automatically (at least avoiding the duplication of the default)
const foodefaults = (; a=1, b="hello", c=pi)
function foo(; kwargs...)
for k in kwargs
k.second != foodefaults[k.first] && println(k)
end
kw = merge(foodefaults, kwargs)
# use kw in your code here, e.g.:
println(kw.b ^ round(Int64, kw.a * kw.c))
end
This will save you a lot of coding if there are many keywords, but you will have to write kw.a every time you formerly wrote a.
This smells like meta-programming. Maybe not a good idea, but a fun challenge. I would start something like
const isexpr = Meta.isexpr
macro capture_changed(fun)
return esc(_cap_c(fun))
end
function _cap_c(fun)
isexpr(fun, :function) || error("must be called on a function definition")
signature = fun.args[1]
body = fun.args[2]
newbody = quote
changed_kwargs = Vector{Pair{Symbol,Any}}()
end
if length(signature.args) >= 2 && isexpr(signature.args[2], :parameters)
kwargs = signature.args[2].args
for kwarg in kwargs
k = kwarg.args[1]
v = kwarg.args[2]
push!(
newbody.args,
:($k == $v || push!(changed_kwargs, $(QuoteNode(k)) => $k)),
)
end
end
append!(newbody.args, body.args)
return Expr(:function, signature, newbody)
end
@capture_changed function f(; a=3, b=4)
println(changed_kwargs)
return a * b
end
julia> f(; b=5)
Pair{Symbol, Any}[:b => 5]
15
Then I would get myself to a computer and test it/figure out the order of arguments/mess around with QuoteNodes until it worked.
There we go, took a bit more tinkering than I imagined, but this is along the right lines. Just needs to handle type annotations etc.
I have written a helper function to then write these things to a string, which seems to work nicely! Thank you!
function string_changed(changed_kwargs)
string_name = ""
for i in changed_kwargs
string_name *= "$(String(i.first))=$(i.second),"
end
return string_name
end
@capture_changed function f(; a=3, b=4, c=true)
display(string_changed(changed_kwargs))
return a * b
end
display(f(a=4, c=false))