Local variable inside a loop: why it ERRORs

Dear all:
I wonder why is the UndefVarError, and, is the Suggestion accurate?

julia> function test(C)
           count = 1
           v = Function[]
           while count < C
               local item
               f = function()
                   println("I'm ", item[])
               end
               push!(v, f)
               if count == 1
                   item = [count]
               else
                   item[] += 1
               end
               map(x -> x(), v)
               count += 1
           end
       end

julia> test(2)
I'm 1

julia> test(3)
I'm 1
ERROR: UndefVarError: `item` not defined in local scope
Suggestion: check for an assignment to a local variable that shadows a global of the same name.
Stacktrace:
 [1] test(C::Int64)
   @ Main .\REPL[1]:13
 [2] top-level scope
   @ REPL[3]:1

How to analyze the item, is it a local variable in one pass, or is it a local variable in all C-1 passes.
(by ā€œpassā€ I mean one go from top to bottom within the while).

It always helps to form a minimal reproducible example. Get rid of all unnecessary lines so you can focus on the underlying issue.

Here’s one example:

julia> function test()
           for i in 1:2
               local item
               if i == 1
                   item = [i]
               else
                   item[] += 1
               end
           end
       end
test (generic function with 2 methods)

julia> test()
ERROR: UndefVarError: `item` not defined in local scope
Suggestion: check for an assignment to a local variable that shadows a global of the same name.
Stacktrace:

One the first iteration, you declare local item and initialise it.

On the second iteration, you declare a new local item, and then attempt item[] += 1, which tries to read item[]. But because of local item, the variable does not persist between iterations and so it doesn’t exist in iteration 2.

Change where you put the local:

julia> function test()
           local item
           for i in 1:2
               if i == 1
                   item = [i]
               else
                   item[] += 1
               end
           end
           return item
       end
test (generic function with 2 methods)

julia> test()
1-element Vector{Int64}:
 2
5 Likes

I’m a fussy person… thereby open a PR to modify the doc

I think it’s misleading to say ā€œwhile/for introduces a local scopeā€.

1 Like

Would you say that about a function also, that it doesn’t introduce a local scope?

function foo(i)
    if i == 0
        a = 10
    else
        a += 1
    end
    return a
end
julia> foo(0)
10

julia> foo(1)
ERROR: UndefVarError: `a` not defined in local scope

There is no ambiguity in this case—the three a’s are the same name. This case is not confusing to users.

This is not the case for while/for, people needs to understand that if you have 10 iterations, then the local (user-specified) names in the loop body are not identical.

(My thoughts are also updated in the Github link above)

Isn’t this in the manual:

Note that the local scope of a for loop body is no different from the local scope of an inner function.
Scope of Variables Ā· The Julia Language

It’s the loop body which has its own scope, so for scope purposes it’s the same as calling a function repeatedly.

1 Like

Note that the local scope of a for loop body is no different from the local scope of an inner function. This means that we could rewrite this example so that the loop body is implemented as a call to an inner helper function and it behaves the same way:

This may be theoretically equivalent. But would anyone write this conversion in practice? e.g. due to performance issues? Is the plain loop faster? I’m not very clear about this.

Anyway, I think the stress is on learning a skill to producing the predictable final result.

To be honest I’m having difficulty understanding some parts of that manual, e.g.

begin
    local c
    c += 1
end

It says begin block will not introduce a scope. But why is here a local?

Anyway, I have no chance using begin, so I would skip it.


And break continue is exclusive to for/while.

I think your idea is correct :white_check_mark:.


I re-express my thoughts in the following region:

If you ask a new user this question

  • how many different entities named item exist in the following code
function test()
    item = 0
    for i in 1:2
        local item
        if i == 1
            item = [i]
        else
            item[] += 1
        end
    end
end
test()

It is possible for him to answer: Oh, the function test introduce a local scope, there is one outer item being associated to 0. Oh, I also see the for loop introduce an inner local scope and there is also an local item inside. So if there are no global name item elsewhere, the conclusion is:

  • there are 2 different entities named item here, both are non-global.

(Yes, surely—I asked ChatGPT, who answered 2.)

But in reality, the proper answer is 3, given Oscar’s answer in #2 post.


Anyway, I think my comprehension still needs to be advanced—therefore I give up the Github actions, giving way to people who are more experienced. :blush:


Edit: I took a closer look at ChatGPT’s answer, and find it’s even worse

An if you call the function twice, it’s … 6? Or do you want repeated executions of the scope in the loop body to count multiple times, but not repeated execution of the scope in the function? What about repeated execution of let? Are there 10 scopes with b in this function?

function f()
    i = 1
    a = 0
    @label L
    let b = i^2
        a += b
    end
    i += 1
    if i < 10
        @goto L
    end
    return a
end

Perhaps in the sense of dynamic scopes, but not the lexical scopes which are used in julia.

1 Like

The preceding description is pretty much all there is to it:

If a top-level expression contains a variable declaration with keyword local , then that variable is not accessible outside that expression.

So while a begin block doesn’t make a local scope, a local declaration makes a variable only last in that scope, similar to how uncaptured local variables only last in their home local scope. The Manual currently does not describe this as a local variable, whether directly or indirectly. However, it does currently behave similarly to a local variable, even if the capture and boxing are implemented differently for global functions.

julia> begin
        x = 1 # normal global
        f() = x
        print(f(), ", ")
        let # rules say this would reassign outer locals
          x = 10
        end
        local y = 2
        g() = y
        println(g())
        let
          y = 20
        end
        nothing
       end
1, 2

julia> f(), g() # local y got reassigned
(1, 20)
1 Like