Hi! I’m not sure how to choose my title
For example, if I specify my output as a string value "a", which means I want to get the value of variable a inside the main function, then after passing this string to the main function, how can I obtain the variables accordingly (the user may specify output = "b") ?
output = "a"
function main(output)
a = 1
b = 2
# some operations....
return Symbol(output) # ??? something like this?
end
vars = main(optput) # I want to get the value of `a` in the main function
I’m not quite familiar with metaprogramming stuff, but I think they may be related?
Guess one question would be why do you need to do this? Depending on the exact use you might not need metaprogramming at all. For instance You could just return a tuple with all values you are interested in, and filter tem afterwards
function do_stuff(x)
a = x^2
b = a*cos(x)
c = b*exp(x) + a
return (a,b,c)
end
do_stuff(2)
or you could actually return all locally defined values in a dictionary
function do_stuff2(x)
a = x^2
b = a*cos(x)
c = b*exp(x) + a
return Base.@locals
end
although that last one is much worse performance-wise
Since I’m writing a package which have a lot of variables involved, and I want the user to have the freedom to specify in a setting script file what variables they want to return… Not sure whether this is the best practice.
I have considered using Dict! But it seems that I have to create a one to one mapping for the strings and symbols.
output = "a"
function main(output)
a = 1
b = 2
# create a dictionary mapping variable names to their values
values = Dict("a" => a, "b" => b)
return values[output]
end
Not sure whether this is a standard way or not. Thanks!
outputSymbol = [:a, :b]
function main(outputSymbol)
a = ones(2,3)
b = zeros(2,3)
var_dict = Base.@locals
outputList = []
for o in outputSymbol
push!(outputList, var_dict[o])
end
return outputList
end
vars = main(outputSymbol)
println(vars)
Just curious, why are you unpacking your dictionary in your example? You can access its values directly if you keep it, and it prevents the extra cost of creating that output vector.
function do_stuff2(x)
a = x^2
b = a*cos(x)
c = b*exp(x) + a
return Base.@locals
end
result = do_stuff2(2)
@show result[:a]
In case you really want to remove the arguments, it could be simpler to take those entries from the dictionary
function do_stuff2(x)
a = x^2
b = a*cos(x)
c = b*exp(x) + a
result = Base.@locals
for key in keys(result)
if !(key in [:a, :b, :c])
delete!(result,key)
end
end
return result
end
However, if you’re already investing time to determine the things that will be outputted (you’re writing an explicit list of those outputs), you might want to approach this differently. Just returning a tuple with the values of interest is far faster. Guess that depends on how important is performance in this bit of code.
In particular, a friendly, idiomatic, and performant way to go about it would be to return a named tuple:
function do_stuff2(x)
a = x^2
b = a*cos(x)
c = b*exp(x) + a
return (; a, b, c)
end
makes it easy for the caller to extract any variables they want. e.g. if the caller wants a and c, they can do:
julia> (; a, c) = do_stuff2(3.7);
julia> a
13.690000000000001
julia> c
-455.92299991101584
In general, I would say that you are fighting with the language by using Base.@locals and arrays of symbols here. You can get something that works, but it will be slow (e.g. type-unstable and allocating), fragile, and hard for other Julia programmers to understand. Better to learn how to write idiomatic code, use appropriate data structures, and think carefully about the useful inputs/outputs of your functions (your API) rather than trying to give the caller blanket access to all local variables.
Don’t reach for “metaprogramming” (e.g. generated lists of variable names, code introspection, passing code as strings) as your first resort.
The main reason not to reach for metaprogramming in this case is that it all works on expressions, the state of the code between the unparsed text and the evaluated working code. Nothing can change the method body once evaluated, you can only replace the method entirely (which is not worth doing all the time); once compiled, variables don’t typically exist anymore; a method call-time value like output is far too late. Many examples of mapping data structures so far, but the most primitive selective structure across languages in history would just be a conditional branch, far predating any metaprogramming features:
if output == "a" return a
elseif output == "b" return b
end
Still, I concur it’s not a great design to toss out outputs in the same method that computes them. Let the user get everything from one method and filter those separately, it accomplishes the same thing without forcing them to learn a method-specific API to do everything in one step. You definitely do not want to vary the returned expressions among methods calling each other, simplify to consistent outcomes instead of maintaining a nightmare.
Hi @stevengj
Thanks for the recommendation! I’ve watched the video in the post. While I didn’t understand some parts, I still learned a lot!
I’m currently writing an FDTD/PSTD package for my daily research, transitioning from a script-style approach like in MATLAB to a more structured programming style. This has certainly been a challenge.
I’ve realized that using global variables extensively is a bad habit, even though it makes everything easy to access . Now, I’m trying to modularize the code. I aim to expose the variables users are interested in (especially myself at this stage) as raw data. For example, after a simulation, I might need to analyze the z-component of the electric field, but next time, I might need to analyze a different variable. This is the motive behind my question and really pushes me to think ahead about how to implement this kind of flexibility.
I found that I still need to put in a lot of effort to transition from “it works” to “understanding how it works” (like knowing what’s going on at compile time, run time, etc.). I will find more materials on internet to fill the knowledge gap. Thanks!