What is the purpose of `let` blocks in global scope?

I was looking at the implementation of randstring when I saw something I didn’t quite understand.

function randstring end

let b = UInt8['0':'9';'A':'Z';'a':'z']
    global randstring
    randstring(r::AbstractRNG, chars=b, n::Integer=8) = String(rand(r, chars, n))
    randstring(r::AbstractRNG, n::Integer) = randstring(r, b, n)
    randstring(chars=b, n::Integer=8) = randstring(GLOBAL_RNG, chars, n)
    randstring(n::Integer) = randstring(GLOBAL_RNG, b, n)
end
  1. What is the point of the “empty” function function randstring end.

  2. Why do we need to have b in a local scope… dosn’t that mean after the let statement ends, b is destroyed? What happens to the functions that depend on b

So for example, when I call randstring(10), it should dispatch randstring(n::Integer) = randstring(GLOBAL_RNG, b, n) but b is not in scope anymore since it’s defined in the global setting and is probably run at start of Julia, correct?

1 Like
  1. note the docstring before the definition: this is how you document generic functions.

  2. b is visible only to the methods it encloses, and thus isn’t defined in the global namespace of the module. This is not only elegant, it prevents accidental modification, and also pins down the type.

BTW, reading Base and standard libraries and understanding what happens in them is indeed the best way to learn Julia.

3 Likes

This is a good example of a closure. The idea is that even though b goes out of scope, functions that depend on it maintain their reference to it. Closures are one way to maintain state across function calls, and there’s actually this nice equivalence between closures and objects.

Here’s another example of a closure:

julia> function counter()
          x = 0
          () -> x += 1
       end
counter (generic function with 1 method)

julia> i1 = counter()
#9 (generic function with 1 method)

julia> i1()
1

julia> i1()
2

julia> i1()
3

julia> i2 = incrementer()
#9 (generic function with 1 method)

julia> i2()
1

julia> i2()
2

So when counter is called it creates a new value x, and then returns an anonymous function that refers to it.

2 Likes

Oh I’ve always heard of closures but didn’t know what they were!

So the x = 0 in your counter() is still allocated after the function ends? That is, does the GC not clean it up since there is an existing reference to this x ?