Language agnostic 值上的闭包与上下文

Language agnostic 值上的闭包与上下文,language-agnostic,programming-languages,closures,language-design,Language Agnostic,Programming Languages,Closures,Language Design,我正在考虑闭包的各种实现,并想知道不同风格的优点。似乎有两种选择,关闭执行上下文或值。例如,根据上下文,我们有: a = 1 def f(): return a f() # returns 1 a = 2 f() # returns 2 或者,我们可以关闭值,并具有: a = 1 def f(): return a f() # returns 1 a = 2 f() # returns 1 有没有实现第二种语言?有优势还是劣势?C++lambdas可以通过值显式捕获: int a =

我正在考虑闭包的各种实现,并想知道不同风格的优点。似乎有两种选择,关闭执行上下文或值。例如,根据上下文,我们有:

a = 1
def f():
  return a
f() # returns 1
a = 2
f() # returns 2
或者,我们可以关闭值,并具有:

a = 1
def f():
  return a
f() # returns 1
a = 2
f() # returns 1

有没有实现第二种语言?有优势还是劣势?

C++lambdas可以通过值显式捕获:

int a = 1;
auto f1 = [a]() -> int { return a; }
f1() == 1;
a = 2;
f1() == 1;
或通过引用:

a = 1;
auto f2 = [&a]() -> int { return a; }
f2() == 1;
a = 2;
f2() == 2;
您还可以通过任何一种方式隐式捕获:

auto f1 = [=]() -> int { return a; }
auto f2 = [&]() -> int { return a; }
优点是您可以控制复制或引用哪些变量以及变量是复制还是引用的。一个潜在的缺点是,您必须谨防生命周期问题,因为C++引用不是拥有的:如果<代码> A<代码>超出范围,则调用<代码> F1 < /C> >仍然有效,但调用<代码> f2< /代码>是未定义的。如果它是自然的,并且您不介意开销,那么您可以随时捕获
共享的\u ptr
(具有共享所有权的指针)

所以对于不可变值:

  • 按值捕获强制复制。通过引用捕获不需要

  • 按值捕获没有所有权问题。通过引用进行捕获是可行的

对于可变值,您当然必须通过引用捕获。下面是一个类似于
std::partial_sum()
的人为示例:

int和=0;
自动f=[&sum](inti)->int{sum+=i;返回sum;}
向量输入{1,2,3,4,5};
矢量输出;
变换(开始(输入)、结束(输入)、返回插入器(输出)、f);
总和=15;
输出==向量{1,3,6,10,15};

我认为,在这种情况下,这不是上下文与值的问题,而是您是将变量作为参考单元格关闭,还是关闭变量包含的值

如果你真的是指上下文,那么你指的是动态范围和词汇范围。请参阅Wikipedia的文章以获得深入的比较

大多数语言实现词法范围(或尝试实现词法范围)。有些语言确实实现了动态范围:特别是像ELisp for emacs这样的旧Lisp。大多数带有闭包的语言(例如Scheme、Haskell、ML等)都在词法范围内的值上关闭。动态范围通常被认为是一个坏主意,因为它更难推理(它是“远处的恐怖行为”)


请注意,即使在词汇范围的语言中,如果关闭引用单元格,也可以获得与第一个示例类似的行为。这就是Scheme和JavaScript闭包行为类似的原因(因为变量是引用单元格)。

在大多数同时具有闭包和可变变量的语言中,闭包捕获位置,而不是值(即第一种行为)。示例包括Scheme、Python和Javascript

为了安全地做到这一点,在许多情况下,该语言必须分配由闭包捕获的可变变量。这通常由编译器过程实现,该过程将实际发生变异的变量转换为显式分配的可变单元,之后编译器可以忽略该问题


为了避免隐式堆分配,Java要求(必需?)捕获的变量(通过内部类)是declaredi
fnal
(即不可变的)。其他语言,如ML和Haskell,完全避免了这个问题,因为变量总是不可变的。在C++中,引用引用可能是不安全的,正如乔恩在他的回答中指出的。

闭包应该像第一种情况一样,但是有些语言提供了第二种情况。p> Smalltalk根据第一种情况起作用。假设一个类定义了方法m和test:

要考虑闭包,必须考虑堆栈。如果在方法m中定义了闭包c并在临时变量计数器上关闭,则在对闭包进行垃圾回收之前,无法删除m的堆栈帧。闭包是一流的,所以您不知道何时不再有对它的引用

但是许多闭包不关闭任何临时变量,或者关闭定义闭包后未修改的临时变量。在后一种情况下,定义闭包时临时变量的值可以复制到闭包中,这样它们就不需要对堆栈帧m的引用

对于上面的闭包c,闭包可以复制计数器的值。这是Java通过强制关闭的临时变量为final来强制实现的

如果方法m是

我想这会挫败优化,因为在创建闭包后计数器会发生变异


至少我是这样理解闭包的

Felix实际上提供了相当复杂的语义,这有时是违反直觉的。闭包通过指向上下文框架的指针捕获上下文。。在这一点上形成闭包。因此,您希望捕获的变量始终反映执行闭包时变量的当前值

情况并非如此,因为优化人员可能会用其值替换变量,特别是如果“变量”声明如下:

val x = 1;
它被视为一个不可变的值,这样的替换被认为是安全的。即使该值作为参数传递,也是如此!例如:

fun f(x:int) () => x;
val y = 1;
val fy = f y;  // closure formed
println$ fy();
我们很可能将fy定义为:

val fy = fun () => 1;
已经写好了。在这种情况下,变量可能是相同的:

var z = 1;
val fz = f z;
z = 2;
println$ fz (); // prints 1 .. maybe
在闭包形成时用z值替换x,但也可以打印2,用变量名z替换x

在Felix中,不确定应用了哪些优化,这是经过深思熟虑的:它允许编译器自由选择(它认为是什么)最佳优化

如果要强制进行解释,可以:对于参数参数:

funf(var x:int)(=>x;//强制求值,将参数复制到参数 乐趣f(x:unit->int)=>x();//强制惰性评估

对于最初的问题:你可以强迫懒惰的解释者
fun f(x:int) () => x;
val y = 1;
val fy = f y;  // closure formed
println$ fy();
val fy = fun () => 1;
var z = 1;
val fz = f z;
z = 2;
println$ fz (); // prints 1 .. maybe
var x = 1;
fun f()=> *&x;
var x = 1;
val y = x;
var x = 2;
fun f() => y; // prints 1
var g : unit -> int;

for var i = 0 upto 10 do
   val x = i;
   fun f()() => x;
   if i == 3 do
     g = f();
   done
done