Julia 中的 "closure" 是什么?

What is a "closure" in Julia?

我正在学习如何在 Julia 中编写最大似然实现,目前,我正在关注 this material(顺便说一句,强烈推荐!)。 所以问题是我不完全理解 Julia 中的 closure 是什么,也不知道我什么时候应该真正使用它。即使阅读了 official documentation 这个概念对我来说仍然有点模糊。

例如,在教程中,我提到作者将对数似然函数定义为:

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

然而,后来他声称

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)

从段落中,我了解到我们需要使用它,因为它是 Optim 算法的工作原理,但我仍然不明白它是什么广义上的闭包。如果有人能对此有所了解,我将不胜感激。非常感谢。

在您询问的上下文中,您可以认为闭包是一个引用某些在其外部范围内定义的变量的函数(对于其他情况,请参阅@phipsgabler 的回答)。这是一个最小的例子:

julia> function est_mean(x)
           function fun(m)
               return m - mean(x)
           end
           val = find_zero(fun, 0.0)
           @show val, mean(x)
           return fun # explicitly return the inner function to inspect it
       end
est_mean (generic function with 1 method)

julia> x = rand(10)
10-element Vector{Float64}:
 0.6699650145575134
 0.8208379672036165
 0.4299946498764684
 0.1321653923513042
 0.5552854476018734
 0.8729613266067378
 0.5423030870674236
 0.15751882823315777
 0.4227087678654101
 0.8594042895489912

julia> fun = est_mean(x)
(val, mean(x)) = (0.5463144770912497, 0.5463144770912497)
fun (generic function with 1 method)

julia> dump(fun)
fun (function of type var"#fun#3"{Vector{Float64}})
  x: Array{Float64}((10,)) [0.6699650145575134, 0.8208379672036165, 0.4299946498764684, 0.1321653923513042, 0.5552854476018734, 0.8729613266067378, 0.5423030870674236, 0.15751882823315777, 0.4227087678654101, 0.8594042895489912]

julia> fun.x
10-element Vector{Float64}:
 0.6699650145575134
 0.8208379672036165
 0.4299946498764684
 0.1321653923513042
 0.5552854476018734
 0.8729613266067378
 0.5423030870674236
 0.15751882823315777
 0.4227087678654101
 0.8594042895489912

julia> fun(10)
9.453685522908751

如您所见,fun 持有来自外部作用域(在本例中为 est_mean 函数引入的作用域)对 x 变量的引用。此外,我已经向您展示了您甚至可以从 fun 外部检索该值作为其字段(通常不推荐这样做,但我向您展示这个是为了证明 fun 确实存储了对对象 x 在其外部范围内定义;它需要存储此引用,因为变量 xfun 函数体内使用。

在估计的上下文中,正如您所指出的,这很有用,因为在我的例子中 find_zero 要求函数只接受一个参数 - 在我的例子中是 m 变量,而你希望 return 值取决于传递的 mx.

重要的是,一旦 xfun 闭包中被捕获,它就不必在当前范围内。例如,当我调用 fun(10) 时,代码会正确执行,尽管我们在函数 est_mean 的范围之外。但这不是问题,因为 fun 函数已捕获 x 变量。

再举一个例子:

julia> function gen()
          x = []
          return v -> push!(x, v)
       end
gen (generic function with 1 method)

julia> fun2 = gen()
#4 (generic function with 1 method)

julia> fun2.x
Any[]

julia> fun2(1)
1-element Vector{Any}:
 1

julia> fun2.x
1-element Vector{Any}:
 1

julia> fun2(100)
2-element Vector{Any}:
   1
 100

julia> fun2.x
2-element Vector{Any}:
   1
 100

在这里你看到 gen 函数中定义的 x 变量被我绑定到 fun2 变量的匿名函数 v -> push!(x, v) 捕获。稍后当您调用 fun2 时,绑定到 x 变量的对象得到更新(并且可以被引用),尽管它是在 gen 函数范围内定义的。虽然我们离开了 gen 范围,但绑定到 x 变量的对象比范围长,因为它被我们定义的匿名函数捕获。

如有不明之处请评论。

我将通过向您展示他故意遗漏的内容来补充 Bogumił 的回答:闭包不一定是严格意义上的函数。事实上,如果 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)

现在您可以调用 nll(β₀),它是一个具有已定义应用程序方法的 LikelihoodClosure 类型的对象。

仅此而已。匿名函数只是语法糖,用于创建存储来自上下文的“固定变量”的对象实例。

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,)

我们甚至可以作弊并构造一个实例:

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