Accessing Variable Values Dynamically in Julia

Hi! I’m not sure how to choose my title :sweat_smile:
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? :thinking:

Thanks in advance!

You can use dictionaries for that?

julia> d = Dict("a" => 1, "b" => 2)
Dict{String, Int64} with 2 entries:
  "b" => 2
  "a" => 1

julia> output = "a"
"a"

julia> d[output]
1
2 Likes

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

2 Likes

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!

Maybe Preferences.jl would be useful for this?

1 Like

I’ve tried this out and it works :joy:

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)

Hope it’s not a stupid way :sweat_smile:

Haven’t heard of!
I’ll take a look. Thanks!!

AddToField.jl might also be close to what you are looking for.

1 Like

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.

2 Likes

Thanks for the advice!

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.

11 Likes

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.

1 Like

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! :heart:

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 :joy:. 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.

Thanks for the answer!

Hi @Benny,

Thanks for the detailed suggestions!

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!