Attempting to assign to a pre-existing function name inside a function returns an UndefVarError


#1

If I try to assign to a pre-existing function name in the global scope, I get a reasonable error:

function foo(x)
    x
end
foo = foo(7)
# ERROR: invalid redefinition of constant foo
# Stacktrace:
#  [1] top-level scope at none:0

But if I do the same inside a function, I get an UndefVarError exception:

function bar(x)
    foo = foo(x)
    x
end
bar(7)
# ERROR: UndefVarError: foo not defined
# Stacktrace:
#  [1] bar(::Int64) at ./REPL[3]:2
#  [2] top-level scope at none:0

"UndefVarError: foo not defined" does not make much sense to me, and makes the cause of the above exception much less obvious. Is this the expected exception here? If so, can someone explain to me why that’s the expected exception, instead of something like "ERROR: invalid redefinition of constant foo"?


#2

I think the confusion here is because the name foo on the right hand side of foo = foo(x) is resolved in local scope because foo is an implicit local variable due to being assigned within the function block. Implicit local variables are resolved during lowering, so you can see which scope is chosen by using @code_lowered:

julia> function bar(x)
           foo = foo(x)
           x
       end

julia> function baz(x)
           foo = qux(x)
           x
       end

julia> @code_lowered bar(7)
CodeInfo(
1 ─     foo = (foo)(x)       # Here foo is looked up in local scope
└──     return x
)

julia> @code_lowered baz(7)
CodeInfo(
1 ─     foo = (Main.qux)(x)  # Here qux is looked up in module scope
└──     return x
)

(See https://github.com/JuliaLang/julia/blob/8b7c88c650bb20d2b45bebfeb51a0ad8a4856175/src/julia-syntax.scm#L2491 for nitty gritty details.)

I think this is required behavior to allow recursive (or mutually recursive) closures to be defined in local scope. For example, what does the foo on the right hand side of the following expression refer to?

julia> function asdf(x)
           foo(x) = x > 1 ? x*foo(x-1) : 1
           foo(x)
       end

It seems tricky to give a clearer error message for your example, as more evil examples such as the following are permitted and syntactically rather similar to your original function bar.

julia> function asdf(x)
           foo = (x) -> x > 1 ? x*foo(x-1) : 1
           foo(x)
       end

#3

Ok, thanks! I’ll have to mull that over a bit. It sounds like you’re saying that any assignment to foo within the function block (no matter where in the block it occurs!) causes foo to be treated as a local variable, and since foo hasn’t been defined yet in line 2 of bar, it throws an UndefVarError.

I think this example is a little better:

foo(x) = x

function bar(x)
    y = foo(x)
    foo = 11
    return "hello"
end
# ERROR: UndefVarError: foo not defined
# Stacktrace:
#  [1] bar(::Int64) at /Users/cameron/Julia/test.jl:4
#  [2] top-level scope at none:0

In this case, foo is assigned to after it’s used as a function, and yet this assignment in line 3 of bar changes the behavior of line 2 in bar! This messes with my conception of imperative/procedural programming where lines of code are executed one at a time, in order.

I just tried this example in R and Python. The analogous bar function in R returns "hello", but the Python bar throws an exception similar to the Julia exception (though the error message is more informative).

Here’s the analogous R code:

foo <- function(x) x

bar <- function(x) {
    y <- foo(x)
    foo <- 11
    "hello"
}

bar(7)
# [1] "hello"

And here’s the analogous Python code:

def foo(x):
    return x

def bar(x):
    y = foo(x)
    foo = 11
    return "hello"

bar(7)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 2, in bar
# UnboundLocalError: local variable 'foo' referenced before assignment

I guess R programming has either pampered me or taught me bad habits.


#4

Good example! I agree this seems odd at first sight. Again, I think it’s to allow the definition mutually recursive inner functions:

julia> function bar(u)
           F(n) = (n > 0) ? n - M(F(n-1)) : 1 # M used before definition
           M(n) = (n > 0) ? n - F(M(n-1)) : 0
           F(u)
       end

The manual section on local scope has this to say:

An assignment introducing a variable used inside a function, type or macro definition need not come before its inner usage:


#5

Great, thanks for your help!