解释学 2

编者按:本文档已过时,但保留在此以供历史参考。有关当前设计信息,请参阅概要 2

exegesis:n. 对文本的解释和说明,尤指神圣的经文

这是与Larry的“启示录”通谕平行的一系列文章的第一篇(编号为2,以保持与那些启示录同步)。这些文章将展示Perl 6设计的每一部分,并在注释程序中演示新的语法和语义。

因此,无需多言,让我们编写一些Perl 6代码

        # bintree - binary tree demo program 
        # adapted from "Perl Cookbook", Recipe 11.15

        use strict;
        use warnings;
        my ($root, $n);

        while ($n++ < 20) { insert($root, int rand 1000) }

        my int ($pre, $in, $post) is constant = (0..2);

        print "Pre order:  "; show($root,$pre);  print "\n";
        print "In order:   "; show($root,$in);   print "\n";
        print "Post order: "; show($root,$post); print "\n";

        $*ARGS is chomped;
        $ARGS prompts("Search? ");
        while (<$ARGS>) {
            if (my $node = search($root, $_)) {
                print "Found $_ at $node: $node{VALUE}\n";
                print "(again!)\n" if $node{VALUE}.Found > 1;
            }
            else {
                print "No $_ in tree\n";
            }
        }

        exit;

        #########################################

        sub insert (HASH $tree is rw, int $val) {
            unless ($tree) {
                my %node;
                %node{LEFT}   = undef;
                %node{RIGHT}  = undef;
                %node{VALUE}  = $val is Found(0);
                $tree = %node;
                return;
            }
            if    ($tree{VALUE} > $val) { insert($tree{LEFT},  $val) }
            elsif ($tree{VALUE} < $val) { insert($tree{RIGHT}, $val) }
            else                        { warn "dup insert of $val\n" }
        }

        sub show {
            return unless @_[0];
            show(@_[0]{LEFT}, @_[1]) unless @_[1] == $post;
            show(@_[0]{RIGHT},@_[1])     if @_[1] == $pre;
            print @_[0]{VALUE};
            show(@_[0]{LEFT}, @_[1])     if @_[1] == $post;
            show(@_[0]{RIGHT},@_[1]) unless @_[1] == $pre;
        }

        sub search (HASH $tree is rw, *@_) {
            return unless $tree;
            return search($tree{@_[0]<$tree{VALUE} && "LEFT" || "RIGHT"}, @_[0])
                unless $tree{VALUE} == @_[0];
            $tree{VALUE} is Found($tree{VALUE}.Found+1);
            return $tree;
        }

它是Perl,吉姆,相当熟悉

程序以熟悉的方式开始

        use strict;
        use warnings;
        my ($root, $n);

        while ($n++ < 20) { insert($root, int rand 1000) }

这里没有新东西。事实上,尽管它展示了许多新特性,但总体上,这个程序看起来和感觉非常像Perl 5代码。

考虑到Perl 6是从数百名忠实的Perl 5程序员提出的建议中成长起来的,并通过发明Perl 5的思想过滤,这并不令人惊讶。

正如RFC 28所建议的,Perl肯定会保持Perl的样子。

需要声明任何变量吗?

Perl 6中的变量声明可以像上面的$root$n那样简单,但也可以更复杂

        my int ($pre, $in, $post) is constant = (0..2);

这里我们声明了三个共享相同类型(int)和相同属性(constant)的变量。类型化的词法变量也是Perl 5的一个特性,但为Perl的内建类型命名是新的。

类型指定告诉编译器,$pre$in$post将只用于存储整数值。由于int是小写的,因此规格还告诉编译器可以优化变量的实现,因为我们承诺不会对它们进行bless或赋予任何运行时属性。如果在程序中违反了这个承诺,则会得到编译时或运行时错误(具体取决于编译器是否可以静态检测到恶意行为)。

如果我们不愿意放弃bless的祝福或运行时属性的有用性,我们会这样写

        my INT ($pre, $in, $post) is constant = (0..2);

在这种情况下,我们会得到三个不太优化但完全功能的Perl标量。

在这种情况下,int/INT的区别几乎没有什么实际意义。然而,与

        my int @hit_count is dim(100,366,24);

相比

        my INT @hit_count is dim(100,366,24);

因此,用苗条的原生整数替换了近一百万个厚重的标量。

财产就是偷窃

上面的声明中的is constantis dim是编译时属性规范。这些特定的属性是Perl 6的标准属性,但你也可以自行定义)。is dim属性告诉Perl有关数组的(固定!)维度。is constant属性指定初始化后,前面的变量不能被赋值,也不能更改其值。

此外,constant属性是向编译器的一个提示,表明它可能能够通过直接内联其值来优化变量,从而将其完全删除。当然,这只有在我们从不将其视为真实变量的情况下才可行(例如,引用它们或对它们进行bless)。

在明确的情况下,is关键字是可选的,因此我们可以这样写

        my int ($pre, $in, $post) constant = (0..2);

拉里还在考虑一个建议,即提供are作为is的同义词,所以你可能甚至可以将声明写成这样

        my int ($pre, $in, $post) are constant = (0..2);

我们将很快使用is运算符的一个重要特性,即它返回其操作数。所以

        $submarine is Colour('yellow')

的结果是$submarine,而不是'yellow'

更多类似的内容

三个对show的调用也和Perl 5中的完全一样

        print "Pre order:  "; show($root,$pre);  print "\n";
        print "In order:   "; show($root,$in);   print "\n";
        print "Post order: "; show($root,$post); print "\n";

幸运的是,在这个系列文章中,我们将看到很多这样的内容。

少啃一点,以便能够咀嚼

你是否曾经厌倦了编写

        while (<>) {            # Common Perl 5 idiom
                chomp;
                ...

如果输入行能自动删除末尾的换行符会更好吗?在Perl 6中,可以实现。我们只需设置全局变量$*ARGS引用的输入句柄的chomped属性

        $*ARGS is chomped;

这会导致对该句柄的任何正常读取(见输出输入)都会自动在返回的字符串之前进行预chomp。当然,像大多数其他全局标点符号变量一样,$/已经被从Perl 6中删除,因此要删除的尾随字符序列由句柄的自己的insep输入分隔符)属性指定。

$*ARGS中的星号表示该变量是特殊全局命名空间中的变量。如果省略星号,它可能仍然是特殊全局命名空间中的变量——除非你声明了同名的词法或包变量。如果你这样称呼它有帮助,可以将*读作“标准”。

顺便说一下,它被称为$*ARGS,因为它允许我们访问传递给Perl 6程序的参数(就像Perl 5中的ARGV文件句柄提供了对程序……呃……参数v的访问)。

输出输入

在原版的Cookbook程序中,下一行是

        for (print "Search? "; <>; print "Search? ") {

这突出了一个在Perl 5中没有令人满意的解决方案的常见情况。也就是说:反复提示输入并将其读取到$_中,直到EOF。在Perl 6中,终于有了干净的方式来做这件事——使用另一个属性

        $ARGS prompts("Search? ");
        while (<$ARGS>) {

首先你会注意到,关于菱形运算符死亡的报道被大大夸大了。是的,尽管第二次启示录预言了它的灭亡,但自从应用了规则#2以来,尖括号仍然存在!

当然,它们在Perl 6中略有不同,因为它们需要一个句柄对象(通常存储在一个变量中),但这在Perl 5中也是可能的。

与此同时,那个提示是什么?嗯,Perl 6的解决方案是允许输入句柄有一个与之关联的字符串,它在尝试读取数据之前打印出来。

等一下!,我听到你反对,进行输出的输入句柄??实际上,你几十年来一直在使用这样的句柄。在大多数语言中,每次你从标准输入进行读取时,输入操作的第一件事就是刷新标准输出缓冲区。这就是为什么像

        print "nuqneH? ";
        $request = <>;

正确地预打印提示,即使它不以换行符结束。

所以输入和输出机制已经在秘密地保持着关系。这里唯一的改变是现在你可以允许输入句柄在刷新缓冲区之前添加一些东西。这是通过prompts属性实现的。如果一个输入句柄有这个属性,它的值会在输入句柄读取之前写入到$*OUT。所以我们可以用

        for (print "Search? "; <>; print "Search? ";) {         # Perl 5 (or 6)

替换

        $ARGS prompts("Search? ");                              # Perl 6
        while (<$ARGS>) {

技术上,这应该是

        $ARGS is prompts("Search? ");

但那太痛苦了。幸运的是,在上下文中(如这个例子),is是可选的——可以推断出来。

注意,由于is操作返回其操作数(即使is是不可见的),我们也可以使用相当优雅的

        while (<$ARGS prompts("Search? ")>) {

实际上,这一行版本可能更常用,因为prompts属性的值可能在循环的某个地方被更改,并且这会在每次迭代时重置它。

提示机制的精确语义尚未确定,因此也可能使用子程序引用作为动态提示(句柄会在每次读取之前调用子程序并预打印返回值)。

我们之前没见过吗?(第一部分)

请求并读取了一个值之后,搜索和报告代码几乎是完全熟悉的

            if (my $node = search($root, $_)) {
                print "Found $_ at $node: $node{VALUE}\n"
                print "(again!)\n" if $node{VALUE}.Found > 1;
            }
            else {
                print "No $_ in tree\n"
            }
        }

唯一的Perl 6特性是使用用户定义的Found属性来报告重复搜索。

$node{VALUE}.Found的调用在通常情况下是一个方法调用(在Perl 6中->写成.)。但由于$node{VALUE}只是一个普通的未绑定整数,因此没有Found方法可以调用。因此,Perl将请求视为属性查询,并返回(到对应的属性的一个别名)。

看这里!还有这里!

在Perl 6中,子程序可以(可选地)指定正确的参数列表(与Perl 5允许的并非邪恶只是被误解的参数上下文原型不同)。

例如,insert子程序声明它接受两个参数

        sub insert (HASH $tree is rw, int $val) {

第一个参数指定第一个参数必须是一个哈希的引用,并将其分配给词法变量$tree。将第一个参数定义为哈希引用意味着,任何试图以其他方式使用它(例如,尝试通过它进行子程序调用,尝试传递显式的数组引用等)的尝试都可以被捕获并受到惩罚——在编译时。

重要的是要理解,默认情况下,命名参数不是@_的元素。具体来说,尽管每个参数都是通过引用传递给相应的参数(为了效率),但参数变量本身被自动声明为constant,因此尝试对其赋值会导致编译时错误。这是为了减少人们意外“伤害自己”的发生。

当然,作为Perl,当我们真正需要针对那些脚趾进行瞄准时,我们是可以做到的。为了允许对命名参数进行赋值——这些赋值传播回原始参数——我们需要使用标准的rwread-write)属性来声明参数。然后它成为原始参数的一个完全可赋值的别名,在这个例子中,它允许我们自动初始化它(见我们不需要那些讨厌的反斜杠)。

@_参数数组在Perl 6中仍然可用,但仅在我们以Perl 5的方式声明子程序时——没有参数列表。见一个老式的表演)。

insert的第二个参数被定义为接受一个整数值。通过使用类型int而不是INT,我们再次明确承诺不会对引用做奇怪的事情(至少,不在insert的主体内部)。编译器可能能够利用这些信息以某种方式优化子程序的代码。

符号是终身的,而不仅仅是值类型

很久以前,当地球是新生的,Perl是年轻而纯洁的时候,与变量关联的符号($@%)描述了它的值。例如

        print $x;                       # $x evaluates to scalar
        print $y[1];                    # $y[1] evaluates to scalar
        print $z{a};                    # $z{a} evaluates to scalar
        print $yref->[1];               # $yref->[1] evaluates to scalar
        print $zref->{a};               # $zref->{a} evaluates to scalar
        print @y;                       # @y evaluates to list
        print @y[2,3];                  # @y[2,3] evaluates to list
        print @z{'b','c'};              # @z{'b','c'} evaluates to list
        print @{$yref}[2,3];            # @{$yref}[2,3] evaluates to list
        print @{$zref}{'b','c'};        # @{zyref}{'b','c'} evaluates to list
        print %z;                       # %z evaluates to hash

无论所引用的变量的实际类型如何,访问时前面有一个$意味着结果将是一个标量;前面有一个@意味着一个列表;前面有一个%,一个哈希。

但是,那条蛇进入了花园,向Perlkind提供了苦果——子程序和方法调用

        print $subref->();
        print $objref->method();

现在,前导的$不再指示返回值的类型。因此,在各地的Perl初学者课程中,出现了巨大的哀号和咬牙切齿。

Perl 6让我们回到了一个恩典状态——虽然是一个不同的恩典状态——其中每个变量类型都有一个真正的符号,它永远也不会偏离。

在Perl 6中,标量总是$开头,数组总是@开头(即使访问其元素或切片它也是如此),哈希总是%开头(即使访问其条目或切片它也是如此)。

换句话说,符号不再(有时)表示结果的类型。相反,它(总是)告诉您您正在操作的是哪种类型的变量,无论您正在做什么。

insert子程序有几个这种新语法的示例。最明显的是在子程序开始处自动创建一个空的子树。

            unless ($tree) {
                my %node;
                %node{LEFT}   = undef;
                %node{RIGHT}  = undef;
                %node{VALUE}  = $val

尽管我们在访问%node哈希条目,但变量保留其%符号,哈希访问大括号简单地附加到完整变量名。

同样,要访问数组的元素,我们只需将数组访问方括号附加到变量名:@array[1]。这与Perl 5的语法有显著不同。在Perl 5中,@array[1]@array数组的一个元素切片;在Perl 6中,它是对单个元素的直接访问(不涉及切片)。

这意味着,当然,Perl 6将需要一些修改后的数组切片语义。Larry计划利用这个机会增强Perl的切片功能,并支持多维数组的任意切片和切块。但这将是未来的某个“启示录”。

目前,您只需要知道,如果您在方括号中放入单个标量,您将获得单个元素查找;如果您在方括号中放入列表,您将获得切片。

我们之前见过吗?(第二部分)

%node条目的最后赋值还有一个小小的转折。被赋值的值(副本)也被赋予了Found属性,初始化为零

                %node{VALUE}  = $val is Found(0);

再次,这是因为当使用is设置属性时,操作的结果是左操作数(在这种情况下是$val),不是属性的新的值。

事实上,尽管我们当时忽略了这个事实,但这正是该语法实际上工作的唯一原因。表达式$ARGS prompts("Search? ")设置了句柄的提示,然后返回$ARGS,它成为菱形操作符的操作数,从而通过该句柄执行提示和读取操作。

        while (<$ARGS prompts("Search? ")>) {

我们不需要任何讨厌的反斜杠

一旦初始化了新的%node,需要将其引用赋给作为第一个参数传递的变量(如果不确定为什么,请参阅面向对象的Perl的第12.3.2节,以获取该树操作技术的详细解释)。

在Perl 5中,修改原始参数需要将赋值给$_[0](即Perl 6中的@_[0]),但由于我们声明$treerw,我们可以直接对其赋值,从而使原始参数相应地更改

哦,(你可能这么想),他刚刚成为经典错误之一:在标量上下文中,哈希返回使用桶与分配桶的比率!

                $tree = %node;

在Perl 5中可能是这样,但在Perl 6中,这种几乎无用的行为已经与假发、有缺陷的鞭子和DSL提供商一样消失了。相反,当在标量上下文中评估时,哈希(或数组)返回对其自身的引用。因此,上述代码行是正确的。

好吧,(你现在在想),如果数组也这样做,我如何获取数组的长度呢? 答案是在数字上下文中,数组引用现在评估为数组的长度。因此,Perl 5代码的翻译是

        while (@queue > 0) {    # scalar eval of @queue yields length

        while (@queue > 0) {    # scalar eval of @queue yields ref to array
                                # ref to array in numeric context yields length

同样,在布尔上下文中,数组包含任何元素时都评估为真,因此Perl 5代码的翻译

        while (@queue) {    # scalar eval of @queue yields length

        while (@queue) {    # boolean eval of @queue yields true if not empty

狡猾,不是吗?

你说 %node{VALUE},但我说 $tree{VALUE}

当我们在加载新节点时,我们写 %node{VALUE} 以访问其 'VALUE' 条目。现在由于 $tree 持有对 %node 的引用,我们需要一种访问相同条目的方法。

在Perl 5中,这将是这样

        $tree->{VALUE}        # Perl 5 entry access through hash ref in $tree

由于在Perl 6中 -> 的拼写是 .,因此它变成了

        $tree.{VALUE}         # Perl 6 entry access through hash ref in $tree

然而,由于直接散列访问语法现在使用了一个完全不同的符号 – %node{VALUE},因此 . 在那里不需要用于消除歧义,因此可以省略

        $tree{VALUE}          # Perl 6 entry access through hash ref in $tree

这就是散列引用访问通常会被写成的方式

            if    ($tree{VALUE} > $val) { insert($tree{LEFT},  $val) }
            elsif ($tree{VALUE} < $val) { insert($tree{RIGHT}, $val) }
            else                        { warn "dup insert of $val\n" }
        }

这实际上比最初看起来要简单得多。例如,在Haven’t we met before? (part 1))中,你注意到

        if (my $node = search($root, $_)) {
            print "Found $_ at $node: $node{VALUE}\n"

已经使用了这种新语法吗?

在Perl 5中,这将是一个(非常常见)的错误——第二行将打印 %node 的条目,而我们实际上想要的是 %{$node} 的条目。但在Perl 6中,它只是按照我们的意图执行。

当然,通过其他类型的引用进行访问也将允许省略 .$arr_ref[$index]$sub_ref(@args)

这里有一个方便的转换表

        Access through...       Perl 5          Perl 6
        =================       ======          ======
        Scalar variable         $foo            $foo
        Array variable          $foo[$n]        @foo[$n]
        Hash variable           $foo{$k}        %foo{$k}
        Array reference         $foo->[$n]      $foo[$n] (or $foo.[$n])
        Hash reference          $foo->{$k}      $foo{$k} (or $foo.{$k})
        Code reference          $foo->(@a)      $foo(@a) (or $foo.(@a))
        Array slice             @foo[@ns]       @foo[@ns]
        Hash slice              @foo{@ks}       %foo{@ks}

老式的展示

show 子例程说明了参数列表的可选性。在这里,我们完全省略了参数规范,并得到了熟悉的“接受任意数量的参数并将它们全部放入 @_”语义。

确实,除了DWIM(Do What I Mean)数组访问语法外,show 子例程基本上是Perl 5的

        sub show {
            return unless @_[0];
            show(@_[0]{LEFT}, @_[1]) unless @_[1] == $post;
            show(@_[0]{RIGHT},@_[1])     if @_[1] == $pre;
            print @_[0]{VALUE};
            show(@_[0]{LEFT}, @_[1])     if @_[1] == $post;
            show(@_[0]{RIGHT},@_[1]) unless @_[1] == $pre;
        }

我们认为,从5到6的迁移将会有这样的正常体验:Perl仍然是Perl……只是更加强大。

当然,show 子例程本身就是相当有趣的Perl,所以如果你对称地保护左子树和右子树的遍历重复不是你的维护梦想,这也是使用Perl的新 case 语句的理想位置。

但那要到第4次启示录才会揭晓,所以如果你能看看这个小红灯……<FLASH>…谢谢。

搜索我

search 子例程的参数列表很有趣,因为它混合了旧的和新的Perl语义

        sub search (HASH $tree is rw, *@_) {

两个参数都被明确声明了,但第二个声明(*@_)导致剩余的参数被收集在 @_ 中。那里的 @_ 没有什么 魔法:如果第二个声明是 *@others,其余的参数就会出现在 @others 中。

第二个参数中的星号告诉Perl 6,相应的参数位置是普通的列表上下文,因此那里的任何参数(或之后的参数)都应该被视为单个列表并分配给相应的参数变量。这相当于Perl 5的 @ 原型。

相比之下,参数声明 @param 相当于Perl 5的 \@ 原型,并明确要求将数组变量作为相应的参数。

请注意,因为我们从第二个参数开始收集参数到 @_,所以我们要查找的值(即第二个参数)被称为 @_[0],而不是 @_[1]

            return search($tree{@_[0]<$tree{VALUE} && "LEFT" || "RIGHT"}, @_[0])
                unless $tree{VALUE} == @_[0];

Haven’t we met before? (part 3)

search 的倒数第二行是所有Perl 6操作的地方。确定我们已经到达了所需的节点后,我们将返回它。但我们也需要增加其 Found 属性,我们是这样做的

            $tree{VALUE} is Found($tree{VALUE}.Found+1);

这突出了访问属性的三种方法中的两种:读写 . 语法和写操作符 is

如果一个属性像方法一样被访问,可以通过传递新值作为参数来设置其值。无论是否传递了这样的值,操作的结果都是属性本身的别名(即左值)。因此,我们也可以这样递增值的 Found 属性

        $tree{VALUE}.Found($tree{VALUE}.Found+1);

或者,像这样

        $tree{VALUE}.Found++;

另一方面,is 语法只能设置属性,因为 is 操作返回其左操作数(拥有该属性的引用),而不是属性本身的值。然而,这对于在 return 语句中最后时刻设置属性非常有用

        return $result is Verified;

另一种非常常见的用法是返回零但为真和非零但为假的值

        sub my_system ($shell_command) {
                ...
                return $error is false if $error;
                return 0 is true;
        }

访问属性的第三种方式是通过 prop 元属性,它返回一个引用,指向包含所有属性的哈希

        $tree{VALUE}.prop{Found}++;

您还可以使用此功能来列出一个引用所赋予的所有 属性

        for (keys %{$tree.prop}) {
            print "$_: $tree{VALUE}.prop{$key}\n";
        }

顺便说一下,在《启示录2》中,Larry 嘲笑地将 prop 元属性称为 btw,但在现代治疗技术的帮助下,他已经克服了这个想法。

对先前主题的补充说明

本文介绍了 Perl 6 将提供的一些重要新特性。但不要让所有这些新特性吓到你。Perl 总是提供能力以您自己的水平并以最适合您的风格进行编程。这不会改变,即使最适合您的风格是 Perl 5。

这里几乎每个新特性都是 可选的,如果您选择不使用它们,您仍然可以用接近 Perl 5 的方式编写相同的程序。就像这样

        use strict;
        use warnings;
        my ($root, $n);

        while ($n++ < 20) { insert($root, int rand 1000) }

        my ($pre, $in, $post) = (0..2);

        print "Pre order:  "; show($root,$pre);  print " \n";
        print "In order:   "; show($root,$in);   print " \n";
        print "Post order: "; show($root,$post); print " \n";

        for (print "Search? "; <$ARGS>; print "Search? ") {
            chomp;
            if (my $node = search($root, $_)) {
                print "Found $_ at $node: $node{VALUE}\n";
                print "(again!)\n" if $node{FOUND} > 1;
            }
            else {
                print "No $_ in tree\n";
            }
        }

        exit;

        #########################################

        sub insert {
            unless (@_[0]) {
                @_[0] = { LEFT  => undef, RIGHT => undef,
                          VALUE => @_[1], FOUND => 0,
                        };
                return;
            }
            if    (@_[0]{VALUE} > @_[1]) { insert(@_[0]{LEFT},  @_[1]) }
            elsif (@_[0]{VALUE} < @_[1]) { insert(@_[0]{RIGHT}, @_[1]) }
            else                         { warn "dup insert of @_[1]\n"  }
        }

        sub show  {
            return unless @_[0];
            show(@_[0]{LEFT}, @_[1]) unless @_[1] == $post;
            show(@_[0]{RIGHT},@_[1])     if @_[1] == $pre;
            print @_[0]{VALUE};
            show(@_[0]{LEFT}, @_[1])     if @_[1] == $post;
            show(@_[0]{RIGHT},@_[1]) unless @_[1] == $pre;
        }

        sub search {
            return unless @_[0];
            return search(@_[0]{@_[1]<@_[0]{VALUE} && "LEFT" || "RIGHT"}, @_[1])
                unless @_[0]{VALUE} == @_[1];
            @_[0]{FOUND}++;
            return @_[0];
        }

实际上,这只有 40 个字符(在 1779 个字符中)是纯 Perl 5。而且,几乎所有的差异都是数组元素查找开头使用 @ 而不是 $

即使在没有自动 p52p6 翻译器的情况下,也具有 98% 的向下兼容性……相当不错!

标签

反馈

这篇文章有什么问题?请通过在 GitHub 上打开问题或拉取请求来帮助我们。