8

Given a dictionary of values,

values = {:A => 3, :B => 1}

turn an (arbitrary) expression like

expr = :(2*A)

into a function foo(values) that evaluates the expression, so in this case foo(values) = 6. The resulting function will be called millions of times, so speed is an important consideration. I am happy to adopt a slightly different approach if necessary, as long as it can be automatised.

Things I tried:

  1. The conversion using convert(Function, expr), as suggested here. Fails for me (Julia 0.3.8-pre):

    convert has no method matching convert(::Type{Function}, ::Expr)

  2. Using @eval one can do

    @eval foo(A) = $(expr)

    and then call foo(values[:A]), but that would require knowing that expr depends on A (and only on A).

  3. I wrote a function find_vars(exp) to return the symbols in expr (in this case [:A]), but couldn't find how to use them in the @eval approach.

Community
  • 1
  • 1
user45893
  • 733
  • 5
  • 18
  • 2
    I believe your linked post is specific to the SymPy.jl package – ptb Apr 21 '15 at 18:52
  • @ptb: Thanks for pointing that out! I digged up the convert function in the SymPy package. It didn't help in this case, but for reference see [line 113 in type.jl](/jverzani/SymPy.jl/blob/master/src/types.jl). – user45893 Apr 22 '15 at 08:11

2 Answers2

9

Base.Cartesian has an unexported function lreplace which may be what you're after. Then you can do something like:

julia> values = Dict(:A=>3, :B=>1)
Dict{Symbol,Int64} with 2 entries:
  :B => 1
  :A => 3

julia> import Base.Cartesian.lreplace

julia> expr = :(2*A)
:(2A)

julia> function lreplace_all(expr, d)
       for (k, v) in d
           expr = lreplace(expr, k, v)
       end
       expr
       end
lreplace_all (generic function with 1 method)

julia> lreplace_all(expr, values)
:(2 * 3)

julia> @eval foo(A) = $(lreplace_all(:(2A), values))
foo (generic function with 1 method)

julia> foo(1)
6

Although, since A is defined by the values dict, it makes more sense to define foo as a zero-argument function (unless I've missed something).

EDIT: After rereading your question it seems like you want to pass in the actual dictionary to the function rather than have the values available at compile time as I've done above. In that case, we have get a little creative:

First we need an lreplace like function that will work with expressions which is easy enough

julia> dictreplace!(ex, s, v) = ex
dictreplace! (generic function with 1 method)

julia> dictreplace!(ex::Symbol, s, v) = s == ex ? v : ex
dictreplace! (generic function with 2 methods)

julia> function dictreplace!(ex::Expr, s, v)
           for i=1:length(ex.args)
               ex.args[i] = dictreplace!(ex.args[i], s, v)
           end
       ex
       end
dictreplace! (generic function with 3 methods)

julia> dictreplace(ex, s, v) = dictreplace!(copy(ex), s, v)
dictreplace (generic function with 1 method)

Now we want to replace every occurence of a symbol in our dict keys with a dictionary lookup

julia> function dictreplace_all(expr, kys, dsym)
           for k in kys
               expr = dictreplace(expr, k, :($(dsym)[$(QuoteNode(k))]))
           end
       expr
       end
dictreplace_all (generic function with 1 method)

julia> dictreplace_all(:(2A), keys(values), :d)
:(2 * d[:A])

julia> @eval foo(args) = $(dictreplace_all(:(2A), keys(values), :args))
foo (generic function with 1 method)

julia> values[:A] = -99
-99

julia> foo(values)
-198
ptb
  • 2,138
  • 1
  • 11
  • 16
  • this solution is great! However I have a similar problem that uses Floats instead of Integers and `lreplace` doesn't support Floats.. Do you have any idea on how to solve it? Thanks – kaslusimoes Jun 05 '15 at 18:06
  • It's odd that `lreplace` doesn't handle floats but the `dictreplace!` methods above don't make use of lreplace so they should work fine – ptb Jun 05 '15 at 18:14
1

Thanks to the solution by @ptb and another metaprogramming question I found a simpler yet slower solution:

function foo(values, expr)
    expr =  quote
                A = values[:A]
                B = values[:B]
                return $(expr)
            end
    eval(expr)
end        

Reading in the values from the dictionary can also be done programmatically by replacing the inner evaluation by

    $([:($k = $v) for (k, v) in values]...) 
    return $(expr)
Community
  • 1
  • 1
user45893
  • 733
  • 5
  • 18
  • 1
    Those functions are very different and will only yield the same results if `values` and `expr` never changes during program execution. I suspect that neither one do what you think they do since you've defined your functions to take superfluous arguments (i.e. foo should only take `values` and fooauto should be a zero-argument function. The items wrapped in a `$` are evaluated when `@eval` is run and _not_ when the function is called. Try wrapping your `@eval` blocks with `macroexpand(:(@eval ... end))` to see the actual generated function. – ptb Apr 22 '15 at 12:35
  • You are totally right! I have to add an outer function call to compile the inner function at runtime. Of course, that adds a lot of overhead, so now one can only do 10.000 calls/s. For my code, that is ok: I am going to put an expensive for-loop into the expression. – user45893 Apr 22 '15 at 12:56
  • @ptb: I profiled your code and get around 10.000.000 calls/s - awesome! So, for the given problem statement, your approach is much better! – user45893 Apr 22 '15 at 13:02