主题

Perl 6 中的一些概念乍一看可能很奇怪。它们似乎难以理解,但这仅仅是因为它们是新的且不同的。它们不是只有西藏喇嘛才知道的深奥神秘概念。任何人都可以理解它们,但最好从一个常识性的解释开始。

本文探讨了“主题”和“主题化”的概念。这些词并不是来自特别令人讨厌的Vogon诗歌的引用。实际上,它们是语言学领域的常见术语……有些人可能会说,这甚至更糟糕。尽管如此,理解Perl中的主题的最好方法就是了解其来源。

语言学中的主题

人类语言中每一个更大的单位都有一个主题——无论是句子、段落、对话或任何其他较大的片段。主题是这一单位的核心思想。它是传达内容的焦点。母语者通常在思考时会很容易地确定当前的主题。

“我昨天看到Lister。”

“真的吗?他现在在忙什么?”

“哦,你知道的,又喝醉了,还在想那个可怕的Krissy Kochanski。”

等等……

如果有人问观察者这次对话是什么,他们就会立刻回答“Lister”。

语言学中的主题化词

主题化词简单来说就是标记某个事物或想法为当前主题的词。在英语中,我们有像“for”、“given”和“regarding”这样的主题化词

“今晚我们的第一个魔术,女士们先生们,我的搭档Kryten将尝试吃一个煮鸡蛋。”

“鉴于上帝是无限的,宇宙也是无限的,你想来一块烤茶蛋糕吗?”

“关于主题化词,我应该指出,这个句子是以一个主题化词开头的。”

Perl中的主题

现在我们需要将语言学的主题定义适应到Perl中。在Perl中,主题是代码块中最重要的变量。它可以是一个任意变量:标量、数组、哈希、对象。更准确地说,它是主题的底层存储位置。这可能听起来有些抽象,但这是一个重要的区别。变量实际上只是我们用来获取存储值的名称。单个存储位置可以有多个名称。在英语中,“Rimmer”、“he”和“the hologram”都可以出现在文本中,意味着同一个人。在Perl中,$_、$name、%characters{‘title’}以及无数其他变量都可以在代码段中以不同方式访问单个值。如果这个值是当前主题,那么所有与它相关的变量也都是。这将在以后变得重要。

这时,普通读者可能会想:“这很有趣,但我为什么要关心主题是什么?这么多年来,我过得很好,为什么现在才开始关心?”

答案是:这并不是必需的。没有人必须理解主题才能使用它,就像他们不必理解重力就能接住球一样。

为什么?这是一个非常简单的规则。我们可以称之为主题的第一定律:主题是$_。每当一个值成为当前主题时,$_就成为该值的另一个名称。我们说$_被别名到它。因此,要使用主题,只需在所有老地方使用$_,无论是显式还是隐式,比如chompprint和替换,还有一些新地方,比如when语句和一元点。

即便如此,了解主题仍然是个好主意。了解重力会使许多看似无关的事物突然变得合理。比如苹果落地、飞机坠毁、月亮和太阳的运动、棒球和过山车。主题也是如此。任何程序员都可以在不理解主题的情况下使用 $_。但当他们理解主题时,它就变成了一个逻辑系统,而不是一个“$_ 能做什么”的随机集合。我喜欢逻辑系统。

Perl 中的主题化

Perl 中的主题化器是一个关键字或结构,它使某个值成为当前主题。当前的主题化器包括 givenformethodrule->、某些裸闭包和 CATCH 块,但这个角色阵容还在不断增长。

煤炭和开关

Perl 6 的开关结构 given 是主题化器的典型例子。它的唯一目的就是使传递给它的值成为块内的当前主题。

    given $name {
        when "Lister" { print "I'm a curryaholic." }
        when "Cat"    { print "Orange?! With this suit?!" }
        when "Rimmer" { print "4,691 irradiated haggis." }
    }

因此,在这个例子中,given 通过别名将 $name 设为当前主题。然后 when 语句与 $_ 进行比较。块结束后,$_ 恢复外部作用域中的值。在 Perl 6 中,$_ 只是一个普通的词法变量,每个主题化器都创建它自己的 $_ 变量,该变量在关联的块内是词法作用域的。

水果圈和 M&M's

for 循环是经典的主题化器。在大多数人都还没有为这种活动找到词之前,它就已经在主题化了。forgiven 类似,但它不是创建单个主题,而是创建一系列主题,每个迭代一个。

    for @orders {
        when /scone/ {
            print "Would you like some toast?";
        }
        when /croissant/ {
            print "Hot, buttered, scrummy toast?";
        }
        when /toast/ {
            print "Really? How about a muffin?";
        }
    }

就像 given 一样,for 也会取一个值,在这个例子中是数组的当前元素,并将其作为主题。

在简单的情况下,如这些例子,forgiven 都会创建可读写性的 $_ 别名。这和 Perl 5 一样:块内对 $_ 的任何更改都会修改原始值。

弓和箭

新改进的箭头 (->) 是最灵活的主题化器。它出现在各种不同的上下文中。单独的 -> 就像 sub 一样创建一个匿名子程序。

        -> $param { ... }

        # is the same as:

        sub ($param) { ... }

唯一的区别是 -> 不需要在其参数列表周围使用括号,以及 -> 会对其第一个参数进行主题化。

在下面的例子中,第一个表达式创建一个匿名子程序并将其存储在 $cleanup 中。当存储在 $cleanup 中的子程序执行时,$line 参数接收字符串参数并成为当前主题,所以 $line 和 $_ 都被别名为 $intro 的值。常用的操作,如 chomp、替换和 print,然后使用主题作为默认值。

    $cleanup = -> $line is rw {
        s:w/Captain Rimmer!/the bloke/;
        $line _= " who cleans the soup machine!";
        print;
    }

    $intro = "Fear not, I'm Captain Rimmer!";
    $cleanup($intro);

与简单的 forgiven 不同,箭头默认创建只读的别名。带有 is rw 属性标记了命名别名和 $_ 别名为可读写。如果没有这个属性,任何修改 $line 或 $_ 的语句都会导致编译时错误,就像它们被明确标记为 is constant 一样。

箭头不仅限于单独使用。它还可以与其他主题化器结合使用。当它这样做时,它为当前主题创建一个命名别名。

    for @lines -> $line is rw {
        s:w/Captain Rimmer!/the bloke/;
        $line _= " who cleans the soup machine!";
        print;
    }

for 遍历数组时,它依次将每个元素别名为 $line 和 $_。这取代了 Perl 5 中别名循环变量的方法。

    # Perl 5
    for my $line (@lines) {
        $line =~ s/Captain Rimmer!/the bloke/;
        $line .= " who cleans the soup machine!";
        print $line;
    }

尽管如此,Perl 6 的方法有一些额外的优点。由于箭头将 $line 和 $_ 别名为当前值,它不仅与默认构造函数(如 print)一起工作,而且在需要显式命名的变量时,它还提供了一个比 $_ 更有意义的名称。

第一个 for 的例子和匿名子程序引用的例子非常相似。唯一的区别是其中一个被存储在变量中以供以后调用,另一个被附加到 for 上。实际上,for 的例子所做的只是将循环的块替换为匿名子程序引用。这是 Perl 6 方法的第二个优点。因为 $line 现在是子程序的参数,它自动被词法作用域到块中。这里的 my 是隐式发生的。

箭头还可以与不是话题化的结构组合,比如ifwhile,并允许它们进行话题化。

    if %people{$name}{'details'}{'age'} -> $age {
        print "$age already?\n";
        if $age > 3000000 {
            print "How was your stasis?\n";
        } elsif $age < 10 {
            print "How 'bout a muffin?\n";
        }
    }

因为if检查数据结构元素的真值,箭头也把那个值别名为$age和$_。这个例子可以每次需要年龄值时直接访问多层嵌套的哈希,但简短的别名要方便得多。

这个特性实际上只有在简单的真值测试中才有用。以下例子中测试的真值不是3或$counter,而是复杂条件的结果,$counter > 3

    if $counter > 3 -> $value {
        # do something with $value
    }

结果将是真或假,但如果它是假的,代码块将永远不会执行。事实上,当真值测试为假时,$value根本不会别名为任何东西。它根本不存在。一个具有真值的词法作用域变量会产生相同的效果。

    if $counter > 3 {
        my $value = 1;
        # do something with $value
    }

所以箭头不是一种万能工具,如Ronco水果削皮器、酸奶喷射器,什么都能做。当它有用时,它的确非常有用,但如果没有用……嗯……别用它。:)

疯狂中的方法

方法会话题化它们的调用者。调用者是方法被调用的对象。快速重复10遍这句话,任何人都可以看出为什么需要一个名字。设计团队选择了“调用者”。当调用者是像$self这样的命名参数,或者被隐式指定时,方法会话题化调用者。

    method sub_ether ($self: $message) {
        .transmit( .encode($message) );
    }

并且当它被隐式指定时

    method sub_ether {
        .transmit( .encoded_message );
    }

    method sub_ether (: $message) {
        .transmit( .encode($message) );
    }

这在简短的方法中很有用。一元点符号只是另一个默认构造。任何不带显式对象的方法调用都将在当前话题上执行。在前面的例子中,.transmit$self.transmit$_.transmit完全相同。

所有恐惧的根源

与方法和箭头不同,普通子例程默认不会话题化。以下例子要么什么也不打印,要么打印出一个在子例程定义的词法作用域中的游荡$_。

    sub eddy ($space, $time) {
        print;
    }

然而,子例程可以通过is topic属性的帮助来进行话题化。该属性将参数标记为子例程块中的话题。它可以附加到列表中的任何参数,但一次只能附加到不超过一个参数。

    sub eddy ($space, $time is topic) {
        print;
    }

默认情况下会使用当前话题的内建函数,如print,非常有用。如果用户定义的子例程表现得一样,那不是很好吗?但是,由于$_现在只是一个普通的词法变量,子例程通常不能从其调用者那里访问话题。它只能访问其定义的作用域中的变量。

is given属性让子例程可以访问其调用者的话题。它访问调用者作用域中的话题,并将其绑定到属性的参数。

    sub print_quotes is given($default) {
        print "Random Quote: ";
        print $default;
    }
    ...
    given $quote {
        print_quotes;
    }

is given属性也可以出现在具有完整参数列表的子例程上。唯一的限制是属性的参数不能与完整列表中的任何参数同名。

    sub print_quotes (*@quotes) is given($default) {
        print "Random Quote: ";
        if ( @quotes.length > 0 ) {
            print @quotes;
        } else {
            print $default;
        }
    }

(真正的内建print函数模拟将使用多方法分派,但这超出了本文的范围。)

属性的参数可以取任何名字,但如果参数名为$_或者附加了is topic属性的参数,将设置调用者的话题,使其成为子例程中的话题。

    sub print_quotes is given($_) {  
                # alias $_ to caller's $_
        print;  # prints the value of caller's $_
    }
    # or
    sub print_quotes is given($default is topic) { 
                # alias $default to caller's $_
                # and to $_ within the subroutine
        print;  # prints $default, the value of caller's $_
    }

Perl规则!

规则中的语法规则和闭包会话题化它们的对象状态。这很方便,因为这意味着状态对象上的方法可以使用一元点符号语法。

    m:each/ aardvark { print .pos } /

规则的州对象类似于方法中的$self对象。它是一个语法类的实例。命名规则实际上只是对状态对象调用的命名方法,匿名规则和规则内的闭包实际上只是对状态对象调用的匿名方法。遗憾的是,这仅仅是一点诱人的信息,但实际上并没有什么用,但完整的解释可能需要一整篇文章。尽管如此,了解到状态对象类似于$self对象,是朝着正确方向迈出的一步。

try中的CATCH-er

CATCH块总是主题化错误变量$!。这简化了异常捕获语法,因为CATCH块充当自己的switch语句。

    CATCH {
        when Err::WrongUniverse {
            try_new_universe();
        }
    }

这比等效的语法要整洁得多

    CATCH {
        if $!.isa(Err::WrongUniverse) {
            try_new_universe();
        }
    }

赤裸裸的真相

裸闭包主题化它们的第一个参数。如果块使用占位符变量,主题也被别名为Unicode字母表中的第一个变量。主题在块中是词法作用域的,但它是可读写的参数,所以在块中对$_的修改将修改原始值。

    %commands = (
        add  => { $^a + $^b },
        incr => { $_  + 1 },
    );

grepmap这样的构造不再特殊,因为它们在块参数中使用$_。它们只是从裸块的正常行为中受益。

    @names = map { chomp; split /\s*/; } @input;

嵌套本能

嵌套主题化者稍微复杂一些。以下示例从$name作为主题开始,第一个case与它匹配。在case中,print默认为当前主题。第二个case更复杂一些;它包含一个循环。在循环中还有一个print语句。这个也默认为当前主题,嗯……主题是$name还是$quote?

    given $name {
        when /Rimmer/ {
            print;
            print rimmer_quote();
        }
        when /Kryten/ {
            for kryten_quotes() -> $quote {
                print;
            }
        }
    }

答案来自于几个简单的规则

  • 一次只有一个主题。

一系列嵌套的主题化者不会创建主题的集合。解释器不必通过复杂的选择来知道当前的主题是什么。永远不会存在任何歧义。脚本或模块可能有一系列的不同的主题,但一次只有一个。

  • 主题遵循主题化者的词法作用域。

对于程序员来说,确定当前的主题永远不会比追踪到最近的主题化者更复杂。主题的作用域始终限制于创建它的主题化者的作用域。所以上面的示例将打印内部主题,$quote。

Perl 5中等效的嵌套主题化者会打印外部主题。这是因为主题化者永远不会同时创建一个$_别名和一个命名别名。这是Perl 5中一个相当常见的技巧,使用带for循环的命名别名来避免覆盖$_。这个技巧不再有效。这把我们带到了第三个规则

  • 为了保留外部主题,使用命名别名。

这只是旧技巧的倒置。不是用命名别名与内部主题化者来避免覆盖外部主题,而是用命名别名与外部主题化者在主题改变后访问它。

        when /Kryten/ {
            for kryten_quotes() -> $quote {
                print $name;
                print;
            }

这更符合人类的思维方式。给值得保留的东西起名字比给要丢弃的东西起名字更有意义。

方法也有相同的问题,但在它们的情况下,这意味着即使在简单的方法中,一元点确实很有用,但在有嵌套主题化者的复杂方法中,最好使用命名参数调用调用者。

    method locate ($self: *@characters) {
        .cleanup_names(@characters);
        for @characters -> $name {
            .display_location($name);
        }
        .change_location('Holly');
    }

.display_location方法不会在$self上调用方法,它会尝试在$name上调用方法,并失败(除非$name是一个有.display_location方法的对象)。代码必须将方法作为$self.display_location()调用。在其他方法调用前添加$self会使代码更清晰,但这完全是一个风格问题。

多个别名

另一个问题是,主题化标记符不仅限于单个别名。一个for循环可以一次迭代多个参数通过一个数组

    for @characters -> $role1, $role2, $role3 {
        ...
    }

一次迭代多个数组,一个接一个,每次取几个参数

    for @humans, @betelgeusians, @vogons -> $role1, $role2 {
        ...
    }

或者同时迭代多个数组

    for @characters; @locations -> $name; $place {
        ...
    }

但不管代码多复杂,主题保持不变。游戏的规则是

  • 只有一个主题。

这个规则看起来很熟悉。甚至可以被称为“主题的第二定律”。在嵌套主题化标记符中,限制意味着来自不同作用域的两个主题永远不会同时可访问。在多个别名中,意味着虽然主题化标记符可以创建多个别名,但只有一个别名可以是主题。

  • 主题是第一个参数。

这个规则使得选择主题变得容易。在前两个例子中,主题是 $role1,在第三个例子中是 $name。

有一个例外。属性 is topic 可以选择任何参数作为主题,代替默认的第一个参数。

    for @characters -> $role1, $role2 is topic, $role3 {
        ...
    }

最后的边界

关于主题的内容大概就是这些。希望这篇文章能让你觉得“哇,这很简单!”。但如果不是,请记住一个想法:第一个定律,“主题是 $_”。下次当话题转向Perl 6和主题时,这个简单的翻译将使它易于理解。

标签

反馈

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