Define another function in the definition of a function

I learned from somewhere I can’t remember that Julia allows defining a function in the definition of another function. Is there any detailed introduction for this usage?

Thanks in advance!

1 Like

I use this feature a lot for passing parameters to callback functions:

function gamma(c_ep, e_p, gamma_est)
    function residual(Γ0)
        return c_ep([Γ0, gamma_est]) - e_p
    end
    find_zero(residual, 0.9)
end

In this example the inner function has access to all parameters of the outer function,
even though it is called only with the value Γ0 which is varied by the root finder (find_zero) such that the returned value approaches zero.

5 Likes

This is called a “closure” but it does not appear in many places in the basic docs. Here are pages with more details:

3 Likes

Functions are first-class objects in Julia and obey all of the normal scope rules. So, a function defined inside of another function can be used internally and can be returned for use externally. However, if you don’t return the function then it will cease to exist outside that local scope.

4 Likes

Thanks to everyone for providing good resources and succinct and popular interpretation. They do make me get deeper understanding on the concept “closure”, but please forgive me that I still don’t fully understand it. :woozy_face:

For example, the following code confused me ver much:

function f()
  i = 0
  function ()
    i += 1
  end
end
counter = f()
println(counter(), ", ", counter())

Can someone tell me what happened respectively when the closure function counter() was called twice? Why the value of i can be remembered?

Thanks!

2 Likes

This technically works but would probably be considered by many to be bad practice and likely to produce bugs. I am not a wizard in Julia’s low-level implementation, but my interpretation of this behavior is:

The function f returns a function that increments a global variable whose name was i at the moment this code was compiled. Once f returns, the binding of the name i to the variable whose data is stored at this location in memory is lost. However, the actual abandonment/recycling of that memory block is the responsibility of Julia’s garbage collector. Since a reference to that variable data continues to somewhere in the code, the memory block is not recycled automatically.

Julia is not an object-oriented language. It’s more of a functional language and makes extensive use of multiple dispatch; this is a defining feature and I found my productivity in the language increased greatly when I learned how to leverage it instead of fighting it.

In my opinion, the big mental shift between OO languages and Julia is the distinction between data and functions. You can use structs to store data, and define functions that act on that data. In that way, a counter isn’t a function that carries some state inside of it, but would be better represented as a struct that carries that state and a function that increments or interacts with it.

mutable struct MyCounter
    i::Int
end

function count(c::MyCounter)
    c.i += 1
    return c.i
end

myc = MyCounter(0)
count(myc)
#    1
count(myc)
#    2

You could also define the function to act directly on an instance of the type, like

function (c::MyCounter)()
    c.i += i
    return c.i
end

count = MyCounter(0)
count()
#    1
count()
#    2
3 Likes

That’s why it’s called a closure, in fact!

We say that the anonymous function “closes over” i. Ordinarily, the value of i is purely local, and would be lost when the function f returns. But because it returns a closure, and that closure refers to i, we say that i has “escaped”, so it continues to exist. Every time you call counter() that closed-over value of i is incremented. If you call f again, it creates a new value of i, a new function escapes, and the whole thing starts over.

One way to anchor this is to ask what would happen if the language didn’t support closures? At best, the anonymous function would throw an error because i doesn’t exist anymore. At worst, it would point to a stack frame which contains new data, and incrementing would corrupt it.

So when you a) allow functions to be returned from other functions and b) allow functions to refer to the outer lexical scope in which they’re defined, you’ll have closures.

Technically there can be two kinds of closures: ones passed as an argument are “downward closures”, like a do-block in Julia, and ones returned from functions, like your example, are “upward closures”. Some languages only support downward, or have efficient implementations of downward closures and inefficient implementations of upward closures. Julia supports both.

But you do have to be careful, because Julia’s escape analysis is not perfect. For instance, this example:

Should probably be written like so:

function gamma(c_ep, e_p, gamma_est)
    residual = let c_ep = c_ep, e_p = e_p, gamma_est = gamma_est
        Γ0 -> c_ep([Γ0, gamma_est]) - e_p
    end
    find_zero(residual, 0.9)
end

It isn’t always clear when Julia can infer that variables won’t be shared or mutated, and that can change between releases, using this sort of defensive pattern will assure that the closed-over values are private to the closure.

2 Likes

i isn’t a global variable, although it could be, that’s still a closure. But in this case it’s a function-local variable. From the closure’s perspective, it’s an outer variable, sometimes known as an upvalue.

This could be read (although I don’t believe you meant it that way) as though the continued existence of i depends on whether or not the garbage collector has run. So to clarify, when the closure escapes, everything it refers to escapes with it. Since i is a primitive/isbits type, it would start out allocated on a stack frame, not the heap, and escape analysis would move it to (I believe it’s) a special struct, which has a reference to the function and to its closed-over variables. As long as there’s a reference to the function, the data it can “see” will not be garbage collected.

There can be efficiency issues with closures in Julia, but not correctness issues. The counter example will work correctly and continue to yield a monotonically increasing value indefinitely (it’s pretty hard to reach typemax(Int64) starting from one).

It’s one of many techniques to have under one’s belt, closures are nothing to be afraid of. There’s nothing wrong with your callable-struct reimplementation of a counter, except for two things: it’s about twice as long, and anything can disrupt the count by setting the mutable counter to any value. With a closure, code doesn’t have access to i, except for the counter.

The flip side of that is that if you want to be able to modify i, then a different approach, such as yours, is going to be a better choice.

5 Likes