Scope and anonymous functions

I need help :smiley:

What is the difference between:

function a() 
        funcs = []
        i = 1
        for j=1:100
           push!(funcs, (args...)->i)
           i += 1
       end
       return funcs
end
b = a()
b[1]()

and

function c() 
        funcs = []
        for i=1:100
           push!(funcs, (args...)->i)
       end
       return funcs
end
d = c()
d[1]()

The first one returns 101 for every call of b[x]() and the second one does the expected thing of d[x]() = x

I put it into a function to avoid global scope issues.

What I also find interesting is that the repl shows something like:

100-element Array{Any,1}:
 #76 (generic function with 1 method)
 #76 (generic function with 1 method)
 #76 (generic function with 1 method)

for both of them which looks like it creates the same function over and over again? (Based on the name #76).

@TheCedarPrince is also interested :smile:

1 Like

https://docs.julialang.org/en/v1/manual/variables-and-scoping/#Local-Scope

the behavior should be explained here, My guess is your first function should fall into:

When x = <value>

  1. Existing Local

because function is a hard local scope and your assignment is in a soft local scope (for).


One way to make your first function to β€œbehave” is by forcing a local variable:

function a()
               funcs = []
               i = 1
               for j=1:100
                  local k=i
                  push!(funcs, (args...)->k)
                  i += 1
               end
              return funcs
       end
1 Like

The behavior of a() is due to the way closure works, as all the functions in funcs will return the local variable i as it updates. The second scenario is a bit different because the iterator variable i is new for each iteration. Besides @jling recommendation, it’s also possible to use the let block for the first case.

3 Likes

Thanks for the explanation and the way to avoid this. The other question is why do all those functions have the same name?

Just the way @show works for arrays?

julia> @show d[1]
d[1] = var"#1#2"{Int64}(1)
#1 (generic function with 1 method)

julia> @show d[2]
d[2] = var"#1#2"{Int64}(2)
#1 (generic function with 1 method)
1 Like

Because that is their name:

# a is generated with the second loop
julia> a[1] === a[2]
false

julia> typeof(a[1]) === typeof(a[2])
true

julia> a[1]
#1 (generic function with 1 method)

julia> a[1]()
1

julia> a[2]()
2

It’s just that they box different values:

julia> dump(a[1])
#1 (function of type var"#1#2"{Int64})
  i: Int64 1

julia> dump(a[2])
#1 (function of type var"#1#2"{Int64})
  i: Int64 2

Remember, anonymous functions are implemented as callable structs, where each variable that is captured refers to a field. The second loop creates multiple different instances of the same struct, which capture different values in their single field. The first one captures a reference to the same value/binding.

Lowered code of your anonymous function
julia> Meta.@lower (args...) -> i
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─      $(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─      global var"#7#8"
β”‚        const var"#7#8"
β”‚   %3 = Core._structtype(Main, Symbol("#7#8"), Core.svec(), Core.svec(), false, 0)
β”‚        var"#7#8" = %3
β”‚        Core._setsuper!(var"#7#8", Core.Function)
β”‚        Core._typebody!(var"#7#8", Core.svec())
└──      return
)))
β”‚   %2 = var"#7#8"
β”‚   %3 = Core.apply_type(Vararg, Core.Any)
β”‚   %4 = Core.svec(%2, %3)
β”‚   %5 = Core.svec()
β”‚   %6 = Core.svec(%4, %5, $(QuoteNode(:(#= REPL[45]:1 =#))))
β”‚        $(Expr(:method, false, :(%6), CodeInfo(quote
    return i
end)))
β”‚        #7 = %new(var"#7#8")
└──      return #7
))))
3 Likes

I learn a lot today :slight_smile: Thanks for the example and explanation,

1 Like

I talk a little about the scope of variables inside loops here, and I really deepen the topic here.

1 Like