Evaluate expression with variables from a dictionary

I have a set of variables stored in a Dict{Symbol, Any} like this:

data = Dict(
    :a=>1,
    :b=>2,
    :c=>3    # etc.
)

Then I’ve got a set of calculations that are defined as expressions with variable names that match the keys of the data set, e.g.

f1 = :(a + b)

I’m looking for a way of evaluating such expressions using the variables that are stored in data. But I prefer not to populate Main (or the module where the calculations are done) with those variables.

A macro like this might do the trick:

macro calculate(ex, dict_ex)
    dict = __module__.eval(dict_ex)
    vex = Expr[]
    for (k, v) in dict
        push!(vex, :($k = $v))
    end
    push!(vex, ex)
    return quote $(vex...) end
end
julia> @calculate a + b data
3

julia> # Or if I want to use the expression stored in f1:

julia> @eval @calculate $f1 data
3

However, this macro allocates variables for all the data contained in the dictionary, even if the expression only uses some of those variables:

julia> @macroexpand @calculate a + b data
quote
    #= untitled-a25e1fb3875a91a253aef47c6b6eba3d:8 =#
    #36#a = 1
    #37#b = 2
    #38#c = 3
    #36#a + #37#b
end

Is there a smarter way to do this? E.g. something that lets me know what symbols are actually contained in the expression, which I could use to filter the dictionary entries that are converted into variables inside the macro.

2 Likes

This is one way to do it.

interpolate_from_dict(ex::Expr, dict) = Expr(ex.head, interpolate_from_dict.(ex.args, Ref(dict))...)
interpolate_from_dict(ex::Symbol, dict) = get(dict, ex, ex)
interpolate_from_dict(ex::Any, dict) = ex

Then you have

julia> interpolate_from_dict(f1, data)
:(1 + 2)

julia> eval(interpolate_from_dict(f1, data))
3
2 Likes

Why do you need expressions? Can you just use normal functions?

1 Like

Expressions are nice for quickly writing down formulas that you find in papers. But functions would be fine as well. For instance, I don’t mind writing:

julia> f1(a, b) = a + b
f1 (generic function with 1 method)

Two drawbacks:

  1. The code becomes “hidden”, you can’t type f1 and see what it does anymore. But that’s not a real problem, as I do have the source anyway.
  2. How can I tell f1 who are a and b from the dictionary…?

… Oh, wait!

julia> f1(; a, b, _...) = a + b
f1 (generic function with 2 methods)

julia> f1(; data...)
3

Horray! :partying_face:

Thanks @dpsanders!

2 Likes

If you can use functions, I don’t quite see why the simple

function f1(data)
  return data[:a] + data[:b]
end

does not solve the problem. Am I missing some complication?

2 Likes

Yes, of course I can use functions. What I was looking for is a way to write code copying it straight from e.g. papers or textbooks. One thing I like of Julia is how easy is to write code as math formulas. So it’s nice to have a way to write a + b instead of data[:a] + data[:b] when you work with data from dictionaries.

(The example of a+b is perhaps too simple. The visual difference is greater when the formulas are more complicated, with many variables and combined operations.)

But in the end, it is just a cosmetic issue - in situations when you don’t really care about optimal code.

An interesting use case.

Perhaps

using Parameters

function foo(data :: Dict)
  @unpack a,b = data
  return a+b
end

would be easier?

3 Likes

I didn’t know of Parameters.jl. Thank you!