Before any piece of code runs, macros are expanded. You can take a look at the @macroexpanded version of your code to investigate why x is captured - if I remember correctly, @spawn just expands to an anonymous function, so the capturing rules should be the same.
The docs for @spawn mention using $ to interpolate variable values into the generated closure. If you look at the code, the macro gathers up all variables preceded by $ into a let block around the task body. (The @async macro uses the same helper function).
In general, though, I would absolutely avoid reusing the variable like you have there. You can just put the @spawn inside the fetch() call if you want:
This still seems to be a bug, i.e., already when using closures:
julia> let x = 1; x = () -> x + 3; x() end
ERROR: MethodError: no method matching +(::var"#9#10", ::Int64)
Stacktrace:
[1] (::var"#9#10")()
@ Main ./REPL[46]:1
[2] top-level scope
@ REPL[46]:1
According to the docs, i.e., in particular let always creates a new location and *
help?> let
search: let delete! isletter deleteat! selectdim reflect! ismutabletype length
let
let statements create a new hard scope block and introduce new variable
bindings each time they run. Whereas assignments might reassign a new value
to an existing value location, let always creates a new location. This
difference is only detectable in the case of variables that outlive their
scope via closures. The let syntax accepts a comma-separated series of
assignments and variable names:
let var1 = value1, var2, var3 = value3
code
end
The assignments are evaluated in order, with each right-hand side evaluated
in the scope before the new variable on the left-hand side has been
introduced. Therefore it makes sense to write something like let x = x,
since the two x variables are distinct and have separate storage.
I would also expect the following to return 4 (not 6):
julia> let x = 1; f = () -> x + 3; x = 3; f() end
6
PS: It does work when using a comma separated variable list though:
julia> let x = 1, f = () -> x + 3, x = 3; f() end
4
and also the original threading example can be fixed like that
julia> let x = 1,
x = @spawn x + 3
fetch(x)
end
4
I keep getting surprised by Julia’s syntax and imho that is not a good thing.
julia> let x = 1
f = (y) -> x + y
x = 3
f(x)
end
6
is equivalent to
let x = 1
begin
f = (y) -> x + y
x = 3
f(x)
end
end
6
i.e., the newline ends the let bindings and only the first variable is bound by let! If multiple variables are to be bound, they have to be separated by commas:
julia> let x = 1,
f = (y) -> x + y,
x = 3
f(x)
end
4