Auto-evaluate function/expression in local scope

Hi guys,

I have a problem which I think should be fairly easy to solve (I guess) but I don’t know how.
I’ll try to describe it as on-point as possible.

I want to define an array of “formulas” that I want to evaluate at different times.
I tried it with expressions and it worked really well at first sight. Here is a simplified example:

ex1 = :(3 + a^2)
ex2 = :(5 + a)
ex3 = :(12)
arr = [ex1, ex2, ex3]

a=15
eval.(arr)

This is exactly what I need (I love that I can evaluate the whole array with just one .eval call and get an array back) BUT: it only works with global a since expressions always get evaluated in global scope.
Now where I want to evaluate this stuff is in a scope where a is local.

Of course I googled and found the advice “try it with functions”. Apart from the problem that I don’t know how to call a bunch of functions in an array with one call I also wasn’t successful in trying to access local variables.

This simple example (with just one function) doesn’t work:

f = () -> a + b^2 + 20

for a = 1:10
    b = a-1
    println(f())
end

I get an UndefVarError: b not defined.

How could I make this work?
Any help is greatly appreciated!

My spider senses give me the suspicion of a XY problem - Wikipedia but welcome to this fine community, I am sure you are not a villain :wink:

What I mean is, that just solving your direct questions seem to lead you on a bad path, where other problems will likely arose.

For your first question, what do you mean by local. If local is local to a module there is

Base.eval(m::Module, x)

but this is not a recommendation to use it.

For your second question it would be best to just use function parameters, like e.g.:

julia> f = [ (a,b) -> a + b^2 + 20 , (a,b) -> a + b^2 + 10 ]
2-element Vector{Function}:
 #25 (generic function with 1 method)
 #26 (generic function with 1 method)

julia> for a = 1:10
           b = a-1
           println( [ x(a,b) for x in f ] )
       end
[21, 11]
[23, 13]
[27, 17]
[33, 23]
[41, 31]
[51, 41]
[63, 53]
[77, 67]
[93, 83]
[111, 101]

But for better advice we need more information about your real problem you like to solve.

2 Likes

Instead of local variables, you could use a dictionary with symbols as keys that work as variable names. Then, you could make a macro that transforms formulas into functions that take such dictionaries:

using MacroTools

macro makefun(expr)
    new_expr = MacroTools.postwalk(expr) do subex
        if @capture(subex, :s_)
            return :(dic[$(QuoteNode(s))])
        else
            return subex
        end
    end
    return :( dic -> $new_expr )
end

f = @makefun :a + :b^2 + 20

variables = Dict()
for a = 1:10
	variables[:a] = a
	variables[:b] = a - 1
	println(f(variables))
end

Note: inspired in:

1 Like

Thank you both so much!

@oheil to answer your first question: I mean local as in the local scope of another function where a and b would be parameters of said function. Your answer to my second question is a nice solution though - evaluating the functions in a list comprehension is actually a working idea.

Beyond that I was just wondering if I could also find a solution to address future local variables that I already know the names of. This is (to some length) what @heliosdrm provided.

Of course I could provide more information of the real problem but I don’t think it’s needed here. This was more of a “what would be an elegant way to achieve this” question and I got some nice ideas that I can take to my co-researchers to discuss.

Thanks again

There is also

which maybe of use for you.

1 Like

E.g.

fn1(a) = 3 + a^2
fn2(a)= 5 + a
fn3(a) = 12
arr = [fn1, fn2, fn3]

a=15
x = [f(a) for f in arr]

julia> x
3-element Vector{Int64}:
 228
  20
  12

Edit: alternatively using map

y = map(x -> x(a), arr)

As an advice (from my own experience, too) - don’t start with metaprogramming before you are fluent with the language. And then you’d in most cases find out, you don’t need it anyway.

2 Likes

Julia does not have eval in local scope, by design. This is in part a reason why julia can be fast & dynamic at the same time, with lots of compilation to native code.

2 Likes

You can use the pipe operator:

a .|> f

where f is your array of functions.

2 Likes

or

ff(fn, x) = fn(x)
ff.(arr, a)
1 Like

Julia (as almost languages nowadays) is lexically scoped, i.e., variables get the values from the lexical context, i.e., the local block in the source code, around the definition of an expression:

f = () -> a + b^2 + 20  # f is defined in the global scope, ergo, a and b refer to names in the global scope

Thus, calling f() fails in any context as no global bindings for a and b are available.

g = let a = 10
          b = 2
          () -> a + b^2 + 20  # defined in the context with a=10 and b=2
    end

Here, the function g closes over the values from the lexical context around its definition – therefore it’s commonly called a closure. Now, no matter in which context it is called g() will evaluate to 34. Even if you define different bindings for a and b in the global scope:

julia> a = 3; b = 5
5

julia> g()
34

What you would need to make the example work is dynamic scope. In this case, variables are looked up in the current runtime environment. This can be emulated in Julia using task_local_storage:

ff = () -> task_local_storage(:a)^3 + 1

task_local_storage(ff, :a, 4)  # Call ff in an environment with a=4, i.e., let a = 4 ff() end if Julia were dynamically scoped

In most cases it is better and easier to pass variables as arguments. If more flexibility is needed, passing a dictionary as suggested by @heliosdrm is a good option – in the end, task_local_storage is like a global dictionary that can be accessed from, i.e., is implicitly passed to, any function.

1 Like