'What is a "closure" in Julia?

I am learning how to write a Maximum Likelihood implementation in Julia and currently, I am following this material (highly recommended btw!). So the thing is I do not fully understand what a closure is in Julia nor when should I actually use it. Even after reading the official documentation the concept still remain a bit obscure to me.

For instance, in the tutorial, I mentioned the author defines the log-likelihood function as:

function log_likelihood(X, y, β)
    ll = 0.0
    @inbounds for i in eachindex(y)
        zᵢ = dot(X[i, :], β)
        c = -log1pexp(-zᵢ) # Conceptually equivalent to log(1 / (1 + exp(-zᵢ))) == -log(1 + exp(-zᵢ))
        ll += y[i] * c + (1 - y[i]) * (-zᵢ + c) # Conceptually equivalent to log(exp(-zᵢ) / (1 + exp(-zᵢ)))
    end
    ll
end

However, later he claims that

The log-likelihood as we've written is a function of both the data and the parameters, but mathematically it should only depend on the parameters. In addition to that mathematical reason for creating a new function, we want a function only of the parameters because the optimization algorithms in Optim assume the inputs have that property. To achieve both goals, we'll construct a closure that partially applies the log-likelihood function for us and negates it to give us the negative log-likelihood we want to minimize.

# Creating the closure
make_closures(X, y) = β -> -log_likelihood(X, y, β)
nll = make_closures(X, y)

# Define Initial Values equal to zero
β₀ = zeros(2 + 1)
# Ignite the optimization routine using `nll`
res = optimize(nll, β₀, LBFGS(), autodiff=:forward)

From the paragraph, I understand that we NEED to use it because it is how Optim's algorithm works, but I still don't get what is a closure in a broader sense. I will be more than grateful if someone could shed some light on this. Thank you very much.



Solution 1:[1]

I'm going to complement Bogumi?'s answer by showing you what he has deliberately left out: a closure does not have to be a function in the strict sense. In fact, you could write them on your own, if nested functions were disallowed in Julia:

struct LikelihoodClosure
    X
    y
end

(l::LikelihoodClosure)(?) = -log_likelihood(l.X, l.y, ?)
make_closures(X, y) = LikelihoodClosure(X, y)
nll = make_closures(X, y)

Now you are allowed to call nll(??), which is an object of type LikelihoodClosure with a defined application method.

And that's really all to it. Anonymous functions are just syntactic sugar for creating instances of objects storing the "fixed variables" from a context.

julia> f(x) = y -> x + y
f (generic function with 1 method)

julia> f(1) # that's the closure value
#1 (generic function with 1 method)

julia> typeof(f(1)) # that's the closure type
var"#1#2"{Int64}

julia> f(1).x
1

julia> propertynames(f(1)) # behold, it has a field `x`!
(:x,)

And we can even cheat a bit and construct an instance:

julia> eval(Expr(:new, var"#1#2"{Int64}, 22))
#1 (generic function with 1 method)

julia> eval(Expr(:new, var"#1#2"{Int64}, 22))(2)
24

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1