【问题标题】:How do closures in Perl work?Perl 中的闭包是如何工作的?
【发布时间】:2013-06-07 00:38:06
【问题描述】:

这里又是 Perl 新手,试图理解 Perl 中的closure

所以这里有一个我不明白的代码示例:

sub make_saying  {
    my $salute = shift;
    my $newfunc = sub {
        my $target = shift;
        print "$salute, $target!\n";
    };
    return $newfunc;            # Return a closure
}
$f = make_saying("Howdy");      # Create a closure
$g = make_saying("Greetings");  # Create another closure
# Time passes...
$f->("world");
$g->("earthlings");

所以我的问题是:

  1. 如果将变量分配给函数,它是否会自动引用该函数?
  2. 在上面的代码中,我可以改写$f = \make_saying("Howdy") 吗?我什么时候可以使用&,因为我尝试在传递参数时使用它(&$f("world")),但它不起作用。
  3. 最后,在上面的代码中,他**如何将单词 worldearthlings 附加到单词 howdygreetings 之后。

注意:我知道 $f 在某种程度上绑定到带有参数 howdy 的函数,所以这就是我理解 world 是如何附加的。我不明白的是里面的第二个功能。那个人如何发挥它的魔力。对不起,我真的不知道怎么问这个。

【问题讨论】:

  • +1 问题写得很好。我希望这里有一些有见地的答案:)
  • "# 返回一个闭包"。这是一个常见的误解,反映在(词汇)“闭包”的许多定义中。外部函数实际上返回一个(对 a 的引用)function。这样就形成了一个闭包。
  • @Beetroot-Beetroot 我猜原因是闭包必然随函数一起携带,因此在某种意义上与函数一起“返回”。或者这也不符合您在这里暗示的“关闭”的定义? (真的很想了解这种区别。)
  • @IMSoP 如果准确地说,my $x = 5; return sub { 1 + $_[0]}my $x = 5; my $f = sub { $x + $_[0] }; $f->(37); 都没有使用闭包:在第一个示例中,该函数不引用外部范围内的任何变量。第二个示例在定义它的相同环境中使用该函数。在实践中,最好将闭包视为函数-指针 × 环境对。调用闭包时,重新安装环境,然后运行该函数。退出时,环境被卸载,调用者环境重新进入。
  • @amon 我不确定我是否遵循这两个示例:在第一种情况下,我可以看到编译器可能会发现不需要环境中的任何内容,然后将其丢弃;第二,在实践中永远不需要重新安装环境。但在这两种情况下,至少在概念上都需要捕获,以便语言支持闭包。否则,一个通过捕获的环境传递但从未执行的函数也不会是一个闭包,这似乎是一个不切实际的定义。

标签: perl closures


【解决方案1】:

在 Perl 中,标量变量不能直接保存子程序,它们只能保存引用。这很像标量不能保存数组或哈希,只能保存数组引用或哈希引用。

sub { ... } 的计算结果为 coderef,因此您可以直接将其分配给标量变量。如果你想分配一个命名函数(例如foo),你必须获得像\&foo这样的引用。

您可以调用诸如$code->(@args)&$code(@args) 之类的代码引用。

代码

$f = \make_saying("Howdy")

评估make_saying("Howdy"),并引用返回的值。所以你会得到一个指向 coderef 的引用,而不是 coderef 本身。

因此,它不能像&$f("world")那样调用,你需要取消引用一个额外的级别:&$$f("world")


闭包是绑定到特定环境的函数。

环境由所有当前可见的变量组成,所以闭包总是记住这个范围。在代码中

my $x;
sub foo {
  my $y;
  return sub { "$x, $y" };
}

foo$x 的闭包,因为外部环境由$x 组成。内部 sub 是对 $x$y 的闭包。

每次foo 执行时,我们都会得到一个新的$y,因此会得到一个新的闭包。每次调用时,都会返回一个不同的闭包。

当我们执行make_saying("Howdy") 时,$salute 变量被设置为Howdy。返回的闭包会记住这个范围。

当我们用make_saying("Greetings") 再次执行它时,make_saying 的主体再次被评估。 $salute 现在设置为 Greetings,并且内部 sub 关闭了这个变量。此变量与之前的 $salute 不同,后者仍然存在,但只能通过第一个闭包访问。

这两个欢迎程序已经关闭了单独的 $salute 变量。当它们执行时,它们各自的$salute仍在作用域内,它们可以访问和修改值。

【讨论】:

  • 我在这里慢慢了解这个概念。但仍然不是那么清楚。所以问题:如果我的理解是正确的,每次调用make_saying函数,都会形成一个新的$salute?正确的?但是那里$target有什么用呢?
  • 嘿,我想我明白了,如果我错了,请纠正我并忽略我的第一条评论。我现在是这样看的:首先$f 调用make_saying 然后make_saying 返回其子程序的地址,所以现在$f 指向它的子程序(注意当时$target 是空的,所以只有$salute初始化)。因此,当最后一条语句:$f->("world"); 不再调用make_saying 时,而是调用$f 指向的子程序,从而初始化$target。我说的对吗?
  • $target 是调用内部闭包时期望的参数名称。 Perl 的子例程没有以正常方式命名参数,这使这变得更加混乱。在例如PHP,它看起来像 function make_saying($salute) { $newfunc = function($target) { print "$salute, $target!\n"; }; return $newfunc; }make_saying 和匿名闭包,每个都有一个命名参数。 my $target = shift; 只是标准的 Perl 期望参数并调用它的方式 $target
  • @BelmarkCaday 是的,你现在的理解已经足够好了:调用$f 调用匿名子,而不是make_saying$target 并不特殊,它只是匿名子中的一个变量,一旦调用 $f 就会被初始化。我认为了解闭包的最佳方法是使用它们。
  • 哦,如果有人想要在 Perl 中命名参数,有各种模块支持这一点,例如Method::Signatures: use Method::Signatures; func make_saying($salute){ func($target){ print "$salute, $target!\n" } }syntax 'function'
【解决方案2】:

如果一个变量被分配给一个函数,它会自动 引用那个函数?

没有。例如函数make_saying 返回引用另一个函数。此类闭包没有名称,并且可以从其范围之外捕获变量(在您的示例中为变量 $salute)。

在上面的代码中,我可以改写 $f = \make_saying("Howdy") 吗? 我什么时候可以使用 & 因为我尝试使用它来传递 参数 (&$f("world")) 但它不起作用。

没有。 $f = \make_saying("Howdy") 不是你想的那样(详情请阅读 amon 帖子)。你可以写$f = \&make_saying;,意思是“放入$f 对函数make_saying 的引用”。你以后可以像这样使用它:

my $f = \&make_saying;
my $other_f = $f->("Howdy");
$other_f->("world");

最后,在上面的代码中,他** 是如何使用 world 和 地球人被附加在你好和问候这两个词上。

ma​​ke_saying 创建 my 变量进入 lamda (my $newfunc = sub);该 lambda 是从 ma​​ke_saying 返回的。它通过“关闭”来保存给定的单词“Howdy”(?抱歉不知道英文中的哪个单词)。

【讨论】:

  • 但是当我这样写$f = sub {1} 并打印 $f 时怎么会这样。我可以看到 $f 包含一个地址。?
  • @BelmarkCaday 你创建了匿名函数并将其放入$f。就是这样,当您打印它时,您会看到该函数的地址。如果你想执行它并打印它的结果,你应该调用它:$f = sub {1}; print $f->()
【解决方案3】:

每次调用子程序“make_saying”时,它:

  1. 创建一个不同的闭包

  2. 将接收到的参数分配给标量“$salute”

  3. 定义(创建但不执行)一个内部匿名子例程: 这就是为什么在那一刻没有为标量分配任何内容的原因 $target 也没有执行print "$salute, $target!\n"; 语句。

  4. 最终,子例程“make_saying”返回对内部匿名子例程的引用,该引用成为调用(特定)匿名子例程的唯一方法。

每次调用每个匿名子例程时,它都会:

  1. 将接收到的参数赋值给标量$target

  2. 还看到标量 $salute,它将具有在创建匿名子例程时分配的值(何时调用其父子例程 make_saying

  3. 最终执行语句print "$salute, $target!\n";

【讨论】:

    猜你喜欢
    • 2012-09-08
    • 1970-01-01
    • 2020-01-06
    • 2017-07-04
    • 2011-04-16
    • 2013-04-10
    • 2010-09-19
    相关资源
    最近更新 更多