Julia 中的“闭包”是什么?

Álv*_*gas 6 closures function julia

我正在学习如何编写最大似然实现Julia,目前,我正在关注此材料(强烈推荐顺便说一句!)。\n所以问题是我不完全理解Julia 中的闭包是什么,也不知道什么时候应该实际使用它。即使在阅读了官方文档之后,这个概念对我来说仍然有点模糊。

\n

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

\n
function log_likelihood(X, y, \xce\xb2)\n    ll = 0.0\n    @inbounds for i in eachindex(y)\n        z\xe1\xb5\xa2 = dot(X[i, :], \xce\xb2)\n        c = -log1pexp(-z\xe1\xb5\xa2) # Conceptually equivalent to log(1 / (1 + exp(-z\xe1\xb5\xa2))) == -log(1 + exp(-z\xe1\xb5\xa2))\n        ll += y[i] * c + (1 - y[i]) * (-z\xe1\xb5\xa2 + c) # Conceptually equivalent to log(exp(-z\xe1\xb5\xa2) / (1 + exp(-z\xe1\xb5\xa2)))\n    end\n    ll\nend\n
Run Code Online (Sandbox Code Playgroud)\n

不过,后来他声称

\n
\n

我们写的对数似然是数据和参数的函数,但从数学上讲它应该只取决于参数。除了创建新函数的数学原因之外,我们只需要参数的函数,因为Optim 中的优化算法假设输入具有该属性。为了实现这两个目标,我们将构建一个闭包,该闭包部分地为我们应用对数似然函数,并对其求反以给出我们想要最小化的负对数似然。

\n
\n
# Creating the closure\nmake_closures(X, y) = \xce\xb2 -> -log_likelihood(X, y, \xce\xb2)\nnll = make_closures(X, y)\n\n# Define Initial Values equal to zero\n\xce\xb2\xe2\x82\x80 = zeros(2 + 1)\n# Ignite the optimization routine using `nll`\nres = optimize(nll, \xce\xb2\xe2\x82\x80, LBFGS(), autodiff=:forward)\n
Run Code Online (Sandbox Code Playgroud)\n

从这段文字中,我明白我们需要使用它,因为这就是Optim算法的工作原理,但我仍然不明白什么是更广泛意义上的闭包。如果有人能阐明这一点,我将不胜感激。非常感谢。

\n

Bog*_*ski 7

在您询问的上下文中,您可以认为闭包是一个引用其外部作用域中定义的某些变量的函数(对于其他情况,请参阅@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
Run Code Online (Sandbox Code Playgroud)

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

正如您所指出的,在估计的上下文中,这很有用,因为find_zero在我的情况下,要求函数仅采用一个参数 -m在我的情况下是变量,而您希望返回值同时取决于 passm和 on x

重要的是,一旦x在闭包中捕获,fun它不必在当前范围内。例如,当我调用时,fun(10)代码会正确执行,尽管我们超出了 function 的范围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
Run Code Online (Sandbox Code Playgroud)

在这里您可以看到函数x中定义的变量被我绑定到该变量gen的匿名函数捕获。稍后,当您调用绑定到变量的对象时,它会被更新(并且可以被引用),尽管它是在函数作用域中定义的。尽管我们离开了作用域,但绑定到变量的对象的寿命比作用域长,因为它是由我们定义的匿名函数捕获的。v -> push!(x, v)fun2fun2xgengenx

如果有不清楚的地方请评论。