主题
Perl 6 中的一些概念乍一看可能很奇怪。它们似乎难以理解,但这仅仅是因为它们是新的且不同的。它们不是只有西藏喇嘛才知道的深奥神秘概念。任何人都可以理解它们,但最好从一个常识性的解释开始。
本文探讨了“主题”和“主题化”的概念。这些词并不是来自特别令人讨厌的Vogon诗歌的引用。实际上,它们是语言学领域的常见术语……有些人可能会说,这甚至更糟糕。尽管如此,理解Perl中的主题的最好方法就是了解其来源。
语言学中的主题
人类语言中每一个更大的单位都有一个主题——无论是句子、段落、对话或任何其他较大的片段。主题是这一单位的核心思想。它是传达内容的焦点。母语者通常在思考时会很容易地确定当前的主题。
“我昨天看到Lister。”
“真的吗?他现在在忙什么?”
“哦,你知道的,又喝醉了,还在想那个可怕的Krissy Kochanski。”
等等……
如果有人问观察者这次对话是什么,他们就会立刻回答“Lister”。
语言学中的主题化词
主题化词简单来说就是标记某个事物或想法为当前主题的词。在英语中,我们有像“for”、“given”和“regarding”这样的主题化词
“今晚我们的第一个魔术,女士们先生们,我的搭档Kryten将尝试吃一个煮鸡蛋。”
“鉴于上帝是无限的,宇宙也是无限的,你想来一块烤茶蛋糕吗?”
“关于主题化词,我应该指出,这个句子是以一个主题化词开头的。”
Perl中的主题
现在我们需要将语言学的主题定义适应到Perl中。在Perl中,主题是代码块中最重要的变量。它可以是一个任意变量:标量、数组、哈希、对象。更准确地说,它是主题的底层存储位置。这可能听起来有些抽象,但这是一个重要的区别。变量实际上只是我们用来获取存储值的名称。单个存储位置可以有多个名称。在英语中,“Rimmer”、“he”和“the hologram”都可以出现在文本中,意味着同一个人。在Perl中,$_、$name、%characters{‘title’}以及无数其他变量都可以在代码段中以不同方式访问单个值。如果这个值是当前主题,那么所有与它相关的变量也都是。这将在以后变得重要。
这时,普通读者可能会想:“这很有趣,但我为什么要关心主题是什么?这么多年来,我过得很好,为什么现在才开始关心?”
答案是:这并不是必需的。没有人必须理解主题才能使用它,就像他们不必理解重力就能接住球一样。
为什么?这是一个非常简单的规则。我们可以称之为主题的第一定律:主题是$_。每当一个值成为当前主题时,$_就成为该值的另一个名称。我们说$_被别名到它。因此,要使用主题,只需在所有老地方使用$_,无论是显式还是隐式,比如chomp
、print
和替换,还有一些新地方,比如when
语句和一元点。
即便如此,了解主题仍然是个好主意。了解重力会使许多看似无关的事物突然变得合理。比如苹果落地、飞机坠毁、月亮和太阳的运动、棒球和过山车。主题也是如此。任何程序员都可以在不理解主题的情况下使用 $_。但当他们理解主题时,它就变成了一个逻辑系统,而不是一个“$_ 能做什么”的随机集合。我喜欢逻辑系统。
Perl 中的主题化
Perl 中的主题化器是一个关键字或结构,它使某个值成为当前主题。当前的主题化器包括 given
、for
、method
、rule
、->
、某些裸闭包和 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
循环是经典的主题化器。在大多数人都还没有为这种活动找到词之前,它就已经在主题化了。for
与 given
类似,但它不是创建单个主题,而是创建一系列主题,每个迭代一个。
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
也会取一个值,在这个例子中是数组的当前元素,并将其作为主题。
在简单的情况下,如这些例子,for
和 given
都会创建可读写性的 $_ 别名。这和 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);
与简单的 for
和 given
不同,箭头默认创建只读的别名。带有 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
是隐式发生的。
箭头还可以与不是话题化的结构组合,比如if
和while
,并允许它们进行话题化。
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 },
);
像grep
和map
这样的构造不再特殊,因为它们在块参数中使用$_。它们只是从裸块的正常行为中受益。
@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上打开问题或拉取请求来帮助我们。