Let block in an inner function captures variables from outer function

I expected the following definitions f1 and f2 to be equivalent but inner in f1 overwrites state variable from its outer scope despite being assigned in a let-block. Just trying to understand whether this is a bug? Tested on 1.8.5 and 1.9-rc1. Thanks!

function f1(x)
    state = 0
    inner(y) = let
        state = y # overwrites state
        state
    end
    inner(x)
    state
end

function f2(x)
    state = 0
    function inner(y)
        let state = y # fresh state variable
            state
        end
    end
    inner(x)
    state
end

julia> f1(1)
1

julia> f2(1)
0
1 Like

That’s not a bug, but a feature (?) of the let syntax, i.e., let only introduces new bindings for the variables declared in a comma separated list on the same line as the let itself (otherwise let is just like begin):

julia> function f1(x)
           state = 0
           inner(y) = let state = y # on same line => new binding for state
               state
           end
           inner(x)
           state
       end
f1 (generic function with 1 method)

julia> f1(1)
0

This is one of the few cases where Julia syntax is white-space sensitive. Don’t ask me why the parser works like that … having been bitten by this several times, I tend to use let much less than I would in Lisp or so.

4 Likes

You mean new scope, do you?

You’re right. let always introduces a new local scope, i.e., unlike begin which does not, and in addition creates new bindings for the variables in the same line as let (and only for those):

julia> function outer()
           # scope with locals x, y
           x = 1
           y = 2
           @show :before, x, y
           let x = 3  # new binding for x
               y = 4  # assignment to existing local
               @show :let, x, y
           end
           @show :after, x, y
       end
outer (generic function with 1 method)

julia> outer()
(:before, x, y) = (:before, 1, 2)
(:let, x, y) = (:let, 3, 4)
(:after, x, y) = (:after, 1, 4)
(:after, 1, 4)

Unless you use the local keyword :wink:

I like to think of the semantics of let as similar to function definitions with default argument values:

let a=1, b=1
    let a=2
        b=2
    end
    a, b # 1, 2
end

similar to:

((a=1, b=1)->begin
    ((a=2)->begin
        b=2
    end)()
    a, b # 1, 2
end)()

except that let doesn’t introduce boxes, but it also doesn’t introduce an inference barrier.