Using @eval in a function

Hi! I am trying to extend the methods of a collection of functions programmatically with no partial success.

foo(x::Real) = x
for f in (:foo,)
    @eval ($f)(x::Complex) = x'
end

function fun()
    foo(x::Real) = x
    for f in (:foo,)
        @eval ($f)(x::Complex) = x'
    end
    return foo
end
bar = fun()

methods(foo)

# 2 methods for generic function "foo":
[1] foo(x::Real) in Main at /home/cedricmc/Dropbox/Documentos/matematicas/articulos/2020-variational-accelerated-methods/simulations/test_eval.jl:1
[2] foo(x::Complex) in Main at /home/cedricmc/Dropbox/Documentos/matematicas/articulos/2020-variational-accelerated-methods/simulations/test_eval.jl:9

methods(bar)

# 1 method for generic function "foo":
[1] (::var"#foo#1")(x::Real) in Main at /home/cedricmc/Dropbox/Documentos/matematicas/articulos/2020-variational-accelerated-methods/simulations/test_eval.jl:7

I would expect that foo and bar have the same methods, however the complex method for bar is ignored. Why? I guess it is a scope problem. How to solve this so bar be like foo?

I have updated the script with something more eloquent.

foo(x::Real) = x
f = :foo
eval("$f(x::Complex) = x'")
display(foo)
display(eval(f))

function fun()
    goo(x::Real) = x
    g = :goo
    eval("$g(x::Complex) = x'")
    display(goo)
    display(eval(g))
    return goo
end
bar = fun()

foo (generic function with 1 method)
foo (generic function with 1 method)
(::var"#goo#1") (generic function with 1 method)
ERROR: LoadError: UndefVarError: goo not defined

The first eval within fun() triggers no errors, but the second, display(eval(g)), does trigger an UndefVarError, which to me makes no sense.

You’re returning a local variable goo. It’s not associated with the global symbol :goo. However, eval always runs in global scope and creates the only function associated with the global symbol :goo.

1 Like

You’re returning a local variable goo.

Yes indeed

It’s not associated with the global symbol :goo.

Why do you say β€œglobal”? There is no global symbol :goo.

However, eval always runs in global scope and creates the only function associated with the global symbol :goo.

Had the first eval run in global scope, a function called goo in global scope should be defined for complex numbers, however no function goo is created in global (or local) scope, hence the error of the second display.

Using quoting, eval(:($g(x::Complex) = x')), or macro, @eval $g(x::Complex) = x', behaves as you say.

I think you’re dealing with a β€œworld age” issue.

2 Likes

Every eval runs in global scope, and eval’ing a string does not define a function, it merely evals the string. Your first example, with the foo functions suffers from this. A slight variation shows what happens:

foo(x::Real) = x
for f in (:foo,)
    @eval ($f)(x::Complex) = x'
end

function fun()
    foo(x::Real) = x
    for f in (:foo,)
        @eval ($f)(x::Rational) = x'
    end
    return foo
end
bar = fun()

methods(foo)

yields

(::var"#foo#5") (generic function with 1 method)
# 3 methods for generic function β€œfoo” from Main:
[1] foo(x::Complex)
@ REPL[2]:2
[2] foo(x::Rational)
@ REPL[3]:4
[3] foo(x::Real)
@ REPL[1]:1

This shows that the eval inside a function runs in global scope, and does not add methods to the local function foo.

2 Likes

Right! I was on my phone and didn’t notice that earlier. Eval operates on expressions. You’d need to parse it first!

I did not know/understand this, thx.

It is similar to my second example but more clarifying, thx again.

Coming back to my initial post, how can I create a function within a function, specify it programmatically and return it?

function fun()
    foo(x::Real) = x
    f = :foo
    @eval $f(x::Complex) = x'
    return foo
end
bar = fun()
varinfo()

  name                    size summary                                         
  –––––––––––––––– ––––––––––– ––––––––––––––––––––––––––––––––––––––––––––––––
  Base                         Module                                          
  Core                         Module                                          
  InteractiveUtils 307.688 KiB Module                                          
  Main                         Module                                          
  bar                  0 bytes (::var"#foo#1") (generic function with 1 method)
  foo                  0 bytes foo (generic function with 1 method)            
  fun                  0 bytes fun (generic function with 1 method) 

I would like bar to have two methods, the ones that currently have foo and bar itself.

I’m not sure if you are mixing different concepts here. The following seems to work for me

fun_returning() = x::Real -> x
proc_evaluating() = @eval foo(x::Complex) = x
bar = fun_returning()
@show bar(42.0)
proc_evaluating()
@show foo(Complex(42.0, 43.0))

yielding

bar(42.0) = 42.0
foo(Complex(42.0, 43.0)) = 42.0 + 43.0im

(and this is not yet a problem of world age, which I suspected, too).

Thx but this is not what I need.

Documentation refers to this and it seems to be world age realated.

OK, let us try

test() = begin
    @eval foo(x::Real) = x
    @eval foo(x::Complex) = x
end
test()
varinfo()

yielding

  name                    size summary
  –––––––––––––––– ––––––––––– –––––––––––––––––––––––––––––––––––––
  Base                         Module
  Core                         Module
  InteractiveUtils 331.024 KiB Module
  Main                         Module
  foo                  0 bytes foo (generic function with 2 methods)
  test                 0 bytes test (generic function with 1 method)

then.

within a single function call, using @eval to create a new method and then trying to immediately call that method will I think not work, because of world age.

Yes, that was what I thought, but:

test() = begin
    @eval foo(x::Real) = x
    @eval foo(x::Complex) = x
end
test()
@show foo(42.0)
@show foo(Complex(42.0, 43.0))
versioninfo()

yields

foo(42.0) = 42.0
foo(Complex(42.0, 43.0)) = 42.0 + 43.0im
Julia Version 1.8.2
Commit 36034abf26 (2022-09-29 15:21 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: 12 Γ— Intel(R) Core(TM) i7-10710U CPU @ 1.10GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-13.0.1 (ORCJIT, skylake)
  Threads: 4 on 12 virtual cores
Environment:
  JULIA_EDITOR = code
  JULIA_NUM_THREADS = 4

You can generate an β€œanonymous” name with gensym() and define various methods with eval:


function fun(x)
    nm = gensym()
    T = widen(Complex{Float32})
    y = x^2
    @eval begin
        $nm(x::Real) = x
        $nm(x::$T) = x' + $y
    end
end

bar = fun(2.0)

methods(bar)

bar(1)
bar(1.0+im)

yields the output

##295 (generic function with 2 methods)
# 2 methods for generic function β€œ##295” from Main:
[1] var"##295"(x::Real)
@ REPL[9]:6
[2] var"##295"(x::ComplexF64)
@ REPL[9]:7
1
5.0 - 1.0im

But take note of the β€œworld age” problem that was referenced above, new methods/functions are not immediately visible.

Nice approach, but the first foo could overwrite an existing method.

This might be key. I’m gonna give it a try.

by β€œimmediate” i mean within the same function call where it was defined.

julia> test() = begin
           @eval foo(x::Real) = x
           @eval foo(x::Complex) = x
           foo(1+1im) # will error
       end
test (generic function with 1 method)

julia> test()
ERROR: MethodError: no method matching foo(::Complex{Int64})
The applicable method may be too new: running in world age 32433, while current world is 32435.
Closest candidates are:
  foo(::Complex) at REPL[1]:3 (method too new to be called from this world context.)
  foo(::Real) at REPL[1]:2 (method too new to be called from this world context.)
Stacktrace:
 [1] test()
   @ Main ./REPL[1]:4
 [2] top-level scope
   @ REPL[2]:1
2 Likes

You may make it even more anonymous:

function fun(x)
    T = widen(Complex{Float32})
    y = x^2
    @eval let foo = x::Real -> x
        (::typeof(foo))(x::$T) = x' + $y
        foo
    end
end

bar = foo(2.0)

However, the only difference I’ve noticed is that the output of methods(bar) states that it is an anonymous function, not a generic function. I don’t know the difference.

Can you step back and tell us what you are trying to accomplish? Trying to @eval in a function is usually an indication that you made a wrong turn somewhere.

The usual way to create new functions inside a function (i.e. a higher-order function) is to return a closure (an anonymous function), not to do code generation / metaprogramming.

4 Likes