Perl subs 不是词法范围的设计缺陷吗?

Posted

技术标签:

【中文标题】Perl subs 不是词法范围的设计缺陷吗?【英文标题】:Is it a design flaw that Perl subs aren't lexically scoped? 【发布时间】:2011-09-23 02:49:57 【问题描述】:

  sub a 
    print 1;
  

a;

一个错误,是吗?

a 不应从外部获得。

它在 Perl 6* 中工作吗?

* 抱歉我还没有安装。

【问题讨论】:

请选择更好的主题行。 不,您提供的代码在 Perl 6 中不起作用。我在最新版本的 Rakudo 中运行它并收到此错误:“找不到 sub &a”。 不,这不是错误(对于原始标题)或设计缺陷(对于修改后的标题)。有些语言支持嵌套函数定义,有些则不支持。 Perl 5 没有。这两个类别都没有因此而“有缺陷”。 本地函数用 local 声明: local *a = sub print 1 ; a() a() 设计缺陷?不,设计选择?是的。设计缺陷是当一个决定使一般情况比它需要的更复杂时。设计选择是当一个决定优先考虑几个合理的情况之一。 Perl 不允许嵌套函数的决定有助于闭包而不需要子引用;一般情况使简单的事情变得容易。更难的是本地化函数名称的不太普遍的情况。它保持不变(没有影响)的是词法范围代码引用的能力(这是嵌套 subs 的一种替代方法)。 local 是另一种选择。 【参考方案1】:

您是在问为什么 sub 在块外可见?如果是这样,那是因为编译时 sub 关键字将 sub 放在 main 命名空间中(除非您使用 package 关键字创建新的命名空间)。您可以尝试类似


  my $a = sub 
    print 1;
  ;
  $a->(); # works

$a->(); # fails

在这种情况下,sub 关键字不是创建一个 sub 并将其放入 main 命名空间,而是创建一个匿名子例程并将其存储在词法范围的变量中。当变量超出范围时,它不再可用(通常)。

要了解更多信息,请查看perldoc perlsub

另外,您知道您可以检查 Perl 解析器查看您的代码的方式吗?使用 -MO=Deparse 标志运行 perl,就像在 perl -MO=Deparse yourscript.pl 中一样。您的原始代码解析为:

sub a 
    print 1;

;;
a ;

先编译sub,然后运行一个没有代码的块,然后调用a

对于我在 Perl 6 中的示例,请参阅:Success、Failure。请注意,在 Perl 6 中,取消引用是 . 而不是 ->

编辑:我已经添加了another answer,关于对 Perl 5.18 预期的词法子例程的新实验性支持。

【讨论】:

@JaredFarrish 有帮助吗? FWIW,您也可以使用 our 而不是 my 获得相同的变量包范围。 @ikegami,还有一件事。 perl -MO=Deparse -e ' sub x $x my $x' 解析为 OP,因此我认为它按原样呈现,以防子声明形成闭包。我想检查闭包的存在是很困难的,因此B::Deparse 采取了简单的方法:如果在同一范围内的子例程之前声明了一个词法变量,它会显示该范围内的子例程声明。 你说的都是真的。 (嗯,除了困难的部分。很明显,Deparse 可以非常准确地分辨出捕获的潜艇和没有捕获的潜艇之间的区别。)只是你的论点是错误的。 Deparse 的输出并未显示您所说的内容。 Deparse 不显示代码的编译或执行顺序。 (它也没有反驳它。结论只是没有从前提中得出。) 本地函数用 local 声明: local *a = sub print 1 ; a() a()【参考方案2】:

在 Perl 6 中,subs 确实是词法范围的,这就是代码抛出错误的原因(正如一些人已经指出的那样)。

这有几个有趣的含义:

嵌套命名的 subs 可以作为适当的闭包(另请参阅:perl 5 中的“将不会保持共享”警告) 将子从模块导入到词法范围中 内置函数在程序周围的外部词法范围(“设置”)中提供,因此重写就像声明或导入同名函数一样简单 由于 lexpad 在运行时是不可变的,编译器可以在编译时检测对未知例程的调用(niecza 已经这样做了,Rakudo 只在“优化器”分支中)。

【讨论】:

【参考方案3】:

子程序是包作用域的,而不是块作用域的。

#!/usr/bin/perl
use strict;
use warnings;

package A;
sub a 
    print 1, "\n";

a();
1;

package B;
sub a 
    print 2, "\n";

a();
1;

【讨论】:

【参考方案4】:

Perl 中的命名子例程被创建为全局名称。其他答案显示了如何通过将匿名子分配给词法变量来创建词法子例程。另一种选择是使用local 变量来创建动态范围的子。

两者之间的主要区别在于调用方式和可见性。动态作用域的 sub 可以像命名的 sub 一样被调用,并且它也将是全局可见的,直到离开它定义的块。

use strict;
use warnings;
sub test_sub 
    print "in test_sub\n";
    temp_sub();



    local *temp_sub = sub 
        print "in temp_sub\n";
    ;
    temp_sub();
    test_sub();

test_sub();

这应该打印出来

in temp_sub
in test_sub
in temp_sub
in test_sub
Undefined subroutine &main::temp_sub called at ...

【讨论】:

我很高兴有人做对了。但你落后 21 票。这有多蹩脚? @tchrist,好的,所以有动态范围的子例程和词法范围的匿名子例程。是否有一个“正确”的主要原因。除非有什么理由更喜欢前者,否则我更喜欢后者;通过传递包含它们的变量来传递这些词法范围的子非常容易。可以有闭包生成器函数为我构建我的代码。如果我选择,我可以将它们设为local(即local *name=$anon_sub),就我而言,这只是让我在不取消引用的情况下调用它们;谁在乎? 可读性论证:在 Ven'Tatsu 的示例中,需要一些时间来解读某个子例程何时存在以及以何种形式存在。包含匿名子例程的词法范围变量很容易查看和跟踪它们的来源。最后是新手的说法。 OP 提出了一个 Perl 程序员理解为比 OP 预期的更深层次的问题。为什么不向新的 Perl 程序员展示更多 DWIM 的方法,让他稍后了解本地类型球。我并不是说我的更好,但至少同样有效,我挑战被称为错误和跛脚。 @JoelBerger 函数有名称。考虑方法调用。你必须使用local 来完成这项工作。 @JoelBerger 我是这样解释的:包是用来查找东西的,范围是用来隐藏东西的。 函数 names (以及格式名称和文件处理)实际上)在 Perl 中总是绑定到一个包。然而,变量可以绑定到包或范围。一个名字是一个全球性的想法;另请参见符号解除引用。一旦名称的含义模型变得清晰,混乱就会消失。【参考方案5】:

冒着被@tchrist 再次责骂的风险,为了完整起见,我添加了另一个答案。尚未发布的 Perl 5.18 预计将包含词法子例程作为实验性功能。

Here is a link to the relevant documentation. 同样,这是非常实验性的,它不应该用于生产代码,原因有两个:

    可能还没有很好地实现 可能会被删除,恕不另行通知

所以,如果你想玩这个新玩具,但你已经被警告了!

【讨论】:

多么可怕的想法。请注意,这些潜艇上没有提及“本地潜艇”或原型。 @downvoter,不要因为我不喜欢这个想法而对我投反对票,我的回答是对的。 更新:Lexical subs 确实作为实验性功能进入了 5.18:metacpan.org/module/RJBS/perl-5.18.0/pod/…【参考方案6】:

如果您看到代码编译、运行并打印“1”,那么您没有遇到错误。

您似乎期望子例程只能在定义它们的词法范围内调用。那会很糟糕,因为这意味着一个人将无法调用在其他文件中定义的子例程。也许您没有意识到每个文件都在其自己的词法范围内进行评估?这允许像

my $x = ...;
sub f  $x 

【讨论】:

【参考方案7】:

是的,我认为这是一个设计缺陷——更具体地说,最初选择使用动态范围而不是 Perl 中的词法范围,这自然会导致这种行为。但并非所有语言设计者和用户都会同意。所以你问的问题没有明确的答案。

词法作用域是在 Perl 5 中添加的,但作为一个可选功能,您总是需要特别指出它。我完全同意这种设计选择:向后兼容性很重要。

【讨论】:

以上是关于Perl subs 不是词法范围的设计缺陷吗?的主要内容,如果未能解决你的问题,请参考以下文章

Perl 词法范围变量在声明中的效率

Perl 和契约式设计

Perl 变量范围问题

PerlCritic 示例:循环迭代器不是词法的

cv::Mat 类是不是存在设计缺陷?

Perl 运算符是“发现的”而不是设计的?