Struggling to understand this Symbol behaviour - Scope determines error presence?

Hi Julians,

The question relates to Symbols and metaprogramming, been through the docs many times but still getting my head around it.

I’ve been struggling to pinpoint a bug in my code for the last 2 days and at last I’ve reduced it to this minimal example, can anyone help me understand what’s going on please? (In case it matters, Julia v0.6.3 on Windows 7.)

This code runs fine:

foo = :foo
@eval ($println)($foo)
> foo

and this too:

foo = :foo

function maintest()
    @eval ($println)($foo)
end

maintest()
> foo

But when I define foo in the function:

function maintest()
    foo = :foo
    @eval ($println)($foo)
end

maintest()

I get:

ERROR: LoadError: UndefVarError: foo not defined

As a sanity check I did the following, which works as expected:

function maintest()
    foo = "bar"
    @eval ($println)($foo)
end

maintest()
> bar

So my question is, why do I get the error when defining the symbol in the function’s hard local scope?

Thanks

Eval always happens in the global scope. This makes sense because functions are essentially statically compiled so they don’t really have dynamic behavior (giving the speed). If course, if you want to do metaprogramming inside of a function, just metaprogram the AST (and return expressions) or use an @generated function to build function expressions. Or, just pull in the global with global x in the function if that’s what you want to do.

3 Likes

The difference between Symbol and String in maintest occurs because: here

function maintest()
    foo = :foo
    @eval ($println)($foo)
end

the value of $foo that gets interpolated is :foo, so @eval is calling println(foo) (since :(println($(:foo)))), but then foo does not exist in that context. The foo that is now being called for is a different foo (note that the other one is already interpolated).

While here,

function maintest()
    foo = "bar"
    @eval ($println)($foo)
end

the value of $foo that is interpolated is "bar", so the commaned given to @eval is println("bar"), which does not require the lookup of an undefined variable, so that is okay and does not give an error.

1 Like

Thanks for your explanation that makes sense. I’ve gone over the @generated and AST docs several times to try and understand your suggestions, I was wondering if you might be able to give a small code example to demonstrate? Essentially, in (Julia-esque) pseudocode, what I’m trying to achieve is this:


function (self::ImmutableStruct, fieldsToModify::Array{Symbol,1})
    for i in fieldsToModify
        i = modifyingFunction( getfield(self, i) )
    end
    
    createNewImmutableStructWithModifiedFields(...)
end

Also the fields not included fieldsToModify should be duplicated from original struct.

Edit: from my understanding of @generated now it doesn’t seem like it would solve this particular problem

Thanks for the explanation that does help me understand but:

In the global scope it seems to work ok?

@generated is not what you want you. You want to write a macro @modifier this_struct :these :fields that takes in those args and returns an expression for building the struct.

Thanks, I’m trying with a macro now. Sorry just a follow up to your first answer the bit I’m struggling to understand, if eval happens in the global scope how come this works ok?

function maintest()
    foo = "bar"
    @eval ($println)($foo)
end

maintest()
> bar
julia> @macroexpand @eval println($foo)
:((Base.Core).eval(Main, (Core._expr)(:call, :println, foo)))

It’s interpolating foo into the expression locally (interpolation is local) and then evaling that expression.

1 Like

ah ok I get it thanks! But what if I want something to be interpolated to a symbol, i.e. so I can use the ‘getfield’ function in an expression? I tried playing with ‘esc’ but that doesn’t seem to do the trick.

Example?

Going back to a function for the sake of example, i think interpolating to a symbol is the crux of my problems, the rest I seem to have got working:


struct testhang

    a::Float64
    b::Float64
    c::Float64

end

function maintest(structy, symbolist)

    for (i, v) in enumerate(symbolist)
         @eval $v = ($getfield)($structy, $(esc(v)))
    end

end

insty = testhang(1.0, 2.0, 3.0)

maintest(insty, [:a, :b, :c])

edit: in other words, in the local namespace I want variables ‘a’, ‘b’ and ‘c’ to be set equal to ‘structy.a’, ‘structy.b’ and ‘structy.c’ respectively.

edit: woops sorry I know I’m missing the @eval, just put it in

edit again: fixed number of brackets

@eval evaluates in global scope so you cannot use it to declare local variables.

You can use something like

julia> using Parameters

julia> struct Foo
           a
           b
           c
       end

julia> function f(f::Foo)
           @unpack a, b, c = f
           print(a, b, c)
       end
f (generic function with 1 method)

julia> f(Foo(1, 2, 3))
123

You would use a macro for your usecase but note that you can only construct it so that at expansion time the macro knows exactly what syntax to generate. So you need to fix the type or the fields to unpack before runtime.

OK thanks, I see that now. And after some playing around I figured why the getfield wasn’t working. Will post code in case others read this thread and want to know. Realised that if you want to interpolate to the symbol value you have to wrap it in a $(Expr(:quote, symvar)).

So the code below basically does what I need as I don’t really mind that the variable is created in global scope.

struct foo
    a::Float64
    b::Float64
end

function modifyfoo(self::foo, tomodify::Array{Symbol,1})

    for symvar in tomodify
        @eval $symvar = ($getfield)($self, $(Expr(:quote, symvar)))
    end

end

instance = foo(0.0, 0.0)

modifyfoo(instance, [:a, :b])

println("From global\na: $a \nb: $b")

> From global
> a: 0.0
> b: 0.0