Passing a string into function as a expression

function Compare(a,b,c::String)
    d = Meta.parse(c)
    if eval(d)
        true
    else
        false
    end
end
Compare(1,1,"a>b")

I tried to compare two integers by a customized expression, like the function above. But the generated expression can’t relate its containing variables to the function input. So I got the output:

UndefVarError: a not defined

Also, I found if I have global variables of a and b, the expression just use the global variables, ignoring the local a and b in the function Compare.
It there any way to fix this?

1 Like

eval always works in global scope (of the current module). In your first case, a and b are in local scope, so they are not seen.

Thanks, but it’s a little strange to design it like this.
Do the developers have designed a function that just convert a string to the code right there without any other influence? Is that technically possible?

Have a look at https://docs.julialang.org/en/v1/manual/metaprogramming/index.html

2 Likes

You can also try this:

function Compare(a,b,c::String)
    d = Meta.parse(c)
    for i in eachindex(d.args)
        if d.args[i] ==:a 
            d.args[i] = a
        elseif d.args[i] == :b
            d.args[i] = b
        end
    end    
    ex = Expr(:call,d.args...)
    if eval(ex)
        true
    else
        false
    end
end
Compare(1,2,"a>b") # false
Compare(1,2,"b>a") # true
)

or you can call eval() on each argument

function Compare(a,b,c::String)
    d = Meta.parse(c)
    eval(:(a=$a))
    eval(:(b=$b))
    if eval(d)
        true
    else
        false
    end
end

but be aware this solutions are all performance killers
EDIT: Just because its mark as a solution: In particular the second approach can lead to unexpected behavior e.g.

a=3
Compare(1,2,"a>b>0") # true
a == 3 # false
a == 1 # true

so now I have warned you

3 Likes

Optimizing code becomes extremely hard if you could just eval arbitrary code into local scope at runtime.

See https://youtu.be/XWIZ_dCO6X8?t=168 which discusses this limitation and its impact.

4 Likes

Thank you.
That works!

Furthermore, if we have F(condition::String, object::AType) and the specific format of condition is unknow, such as "object.someAttribute%10 == 0", can we still have a solution?

In fact, all I want is to provide a interface to customize the condition of a function to perform. That is

function F(condition::String, object::AType)
    if ConcertToCode(condition)
        Perform()
    end
end
F("object.someAttribute % 10 == 0", object)

Is there any good way to do it? If not using string as condition, what else way can be elegant to do this?

Don’t do this. Don’t pass expressions, or strings, pass functions: Compare(1,1, >) or Compare(1,1, (a,b) -> a > b), for example.

Besides not needing to parse/eval the string, passing functions as arguments (higher-order functions) has many many advantages in terms of both performance and flexibility.

11 Likes

It’s hard to comprehend what you are looking for. Why is

function F(condition::Bool, object::AType)
    if condition
        Perform()
    end
end
F(object.someAttribute % 10 == 0, object)

not an option?

1 Like

Actually I’m writing a small scientific computation program. Sometimes I need the condition function is introduced by reading from a text file, and I think that would be useful because user can be more free to control the program. Whatever, if there’s no good way, at least I can generate the .jl file from the input text file.

I’m not an expert in this, but you can crawl trough the parsed arguments and replace the object like in the example above. You can generalize this task by performing some kind of “search and replace”.

function F(condition::String, object::AType)
    d = Meta.parse(condition)
    d.args[2].args[2].args[1] = object
    if eval(d)
        Perform()
    end
end
F("object.someAttribute % 10 == 0", object)

I’m pretty sure that this can be done in more general fashion.

Just to point out, something like

julia> function Compare(a,b,c::String)
           h = @eval (a, b) -> $(Meta.parse(c))
           if Base.invokelatest(h, a, b)
               true
           else
               false
           end
       end
Compare (generic function with 1 method)

julia> Compare(1,2,"a>b")
false

julia> Compare(1,2,"a<b")
true

can be done. It’s not that good of an idea though.

3 Likes

cool… makes the solution for @ke_xu much more convenient and general

function F(condition::String, object::AType)
    d= @eval object -> $(Meta.parse(condition))
    if Base.invokelatest(d, object)
        Perform()
    end
end
2 Likes

First, those who say that you should avoid evaluating expressions at runtime are absolutely correct. Don’t do it if there’s any other solution.

If you do need to do it, and there are situations where it is valid, you have two sane options to make it semantically local, even though the eval takes place in global scope.

  1. Eval an anonymous function and pass your arguments to it.
julia> function Compare(a, b, c::String)
           f = eval(Meta.parse("(a, b) -> $c"))
           return Base.invokelatest(f, a, b)
       end
Compare (generic function with 1 method)

julia> Compare(1, 1, "a>b")
false

The invokelatest call does the same thing as f(a, b) but is necessary for technical reasons, basically because it calls a function that didn’t exist when Compare itself was called.

  1. Inject your arguments into the expression before evaluating it. This has been demonstrated in earlier replies but let’s do it in a more general way, reusing a solution from Evaluate expression with variables from a dictionary - #2 by GunnarFarneback :
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
function Compare(a, b, c::String)
    expr = Meta.parse(c)
    return eval(interpolate_from_dict(expr, Dict(:a => a, :b => b)))
end
5 Likes

Thank you.
If I store the f the for once for a fixed c::string at the initial stage of program, and invoke it for different a and b for many times, is that would be not very harmful to performance?

Thank you again, that is what I want.

If you cache the evaled function you can avoid a part of the performance impact. Having to call through invokelatest is still slower than a normal function call though. You need to measure if the performance is good enough for your needs but don’t plan to use it for anything truly performance critical.

3 Likes

Sorry for necrobumping.

I needed something similar but solution 1 (define a function and evaluate it) turned out to be excruciatingly slow (I have millions of expressions to evaluate, don’t ask :man_facepalming:)
it turns out eval+let block are 20-30 × faster for me. Here’s what I did:

function parse_eval(arg, expr_str)
    ex = Meta.parse(expr_str)
    return @eval begin
        let Z=$arg
            $ex
        end
    end 
end
2 Likes