Bug or feature: nested let variable scope

I was expecting the second and third code to behave exactly like first, except neither behave the same

# First 
i = 0

let 
    i = 1
    Fs = Vector{Any}(undef, 2)

    while i <= 2
        let 
            j =  i 
            Fs[j] = ()->j
        end 
        i += 1
    end

    println("$(Fs[1]())")
    println("$(Fs[2]())")
    println("inner $i")
end

println("global $i")

#= output
 1
 2
 inner 3
 global 0
=#
# Second 
i = 0

let 
    i = 1
    Fs = Vector{Any}(undef, 2)

    while i <= 2
        let 
            i =  i 
            Fs[i] = ()->i
        end 
        i += 1
    end

    println("$(Fs[1]())")
    println("$(Fs[2]())")
    println("inner $i")
end

println("global $i")


#= output
 3
 3
 inner 3
 global 0
=#
# Third

i = 0

let 
    local i = 1
    Fs = Vector{Any}(undef, 2)

    while i <= 2
        let 
            local i =  i 
            Fs[i] = ()->i
        end 
        i += 1
    end

    println("$(Fs[1]())")
    println("$(Fs[2]())")
    println("inner $i")
end

println("global $i")

#=
ERROR: LoadError: UndefVarError: #1#i not defined
Stacktrace:
 [1] top-level scope at C:\dev\lang\julia\tester3.jl:9
 [2] include(::Function, ::Module, ::String) at .\Base.jl:380
 [3] include(::Module, ::String) at .\Base.jl:368
 [4] exec_options(::Base.JLOptions) at .\client.jl:296
 [5] _start() at .\client.jl:506
in expression starting at C:\dev\lang\julia\tester3.jl:3
=#

  • I assumed that local is redundant in a let block, and that you only need to specify global otherwise local will be assumed
  • the top let statement seem to be able to create a local i variable, but not the inner let
  • The inner let uses the outer i and raises and error when i try to force to make a local copy

let blocks, similarly to loop bodies, introduce new scopes. By itself, making a new scope does not create new local bindings for names that exist in the outer local scope. let, however, has an option to create them by listing the names that must shadow the outer bindings on the same line with the word let. Compared to local declarations, the difference with let is that the rhp of let <var> = <expr> is evaluated in the enclosing scope, so that it can contain <var> (in contrast with, e.g., local i = i, which throws UndefVarError).

i = 0

let i = 1 # `i=1` on the same line
    Fs = []

    while i <= 2
        let i=i
            push!(Fs, ()->i)
        end
        i += 1
    end

    println("$(Fs[1]())")
    println("$(Fs[2]())")
    println("inner $i")
end

println("global $i")

#= output
 1
 2
 inner 3
 global 0
=#
2 Likes

Thanks this is very clear, this line needs to be added to the docs

That is in the docs :man_shrugging: :

(https://docs.julialang.org/en/v1/base/base/#let)

4 Likes

I think if they add

The assignments are evaluated in order, with each right-hand side evaluated in the scope of the enclosing let block before the new variable on the left-hand side has been introduced in the new nested let block

or something like it, I would have understood better

I remember reading this line, but I didn’t get it .
The way you explained it clearer

in the line of let new local variable are introduced in the new let block but evaluated in the the scope of the enclosing let bock

I remember thinking why do I need to do

let x , z , y

when I cant just do

let 
  x
  z 
  y

and it look much cleaner , now I know because on the same line I have the enclosing scope available for my variable declaration, once I leave this line, i dont have them

1 Like