原文: http://blogs.perl.org/users/zoffix_znet/2018/06/perl-6-colonpairoscopy.html
如果我选择 Raku 编程语言中最普遍的构造,那肯定是 colonpair。哈希构造函数,命名参数和 parameters,副词和正则表达式修饰符 - 都涉及到 colonpair。在构建 colonpair 的时候有很多捷径也就不足为奇了,因为它的应用范围太广泛了。
今天,我们就来学习一下所有的这些东西。如果这篇文章中的某些部分让你抓耳挠腮,不要担心,你不需要一下子就学会所有的东西!我们将研究最简单的和更高级的语言结构。
第一部分: 创建
Colonwhaaaa?
Colonpair 的名字来自于(通常) Pair 对象构造函数,并且(通常)里面有一个冒号。以下是一些 colonpairs 的例子:
:foo,
:$bar,
:meow<moo>,
heh => hah
最后一个没有冒号,但由于它和其他 colonpairs 基本上是一样的,我个人认为它也是一个 colonpair。
我们可以通过查看它们的 ^name 方法名看到这些 colonpairs 构成了 Pair 对象:
say :foo.^name; # OUTPUT: «Pair»
但是,在参数列表中使用时,这些 colonpairs 被特殊处理了以表示命名参数。我们将在文章的后面讨论这部分内容。
简写
在我们深入研究之前,你可以先看一下这个基本还算完整的构造 colonpair 的方法清单。我知道,这看起来是一个巨大的列表,但这正是我们阅读这篇文章的原因–学习构成所有这些排列组合的一般模式。
# 标准的, take-any-type, 非便捷形式
:nd(2).say; # OUTPUT: «nd => 2»
:foo('foo', 'bar').say; # OUTPUT: «foo => (foo bar)»
:foo( %(:42a, :foo<a b c>) ).say;
# OUTPUT: «foo => {a => 42, foo => (a b c)}»
# 也可以使用胖箭头记法:
# (parentheses around them are here just for the .say call)
(nd => 2).say; # OUTPUT: «nd => 2»
(foo => ('foo', 'bar') ).say; # OUTPUT: «foo => (foo bar)»
(foo => %(:42a, :foo<a b c>) ).say; # OUTPUT: «foo => {a => 42, foo => (a b c)}»
# 布尔
:foo .say; # OUTPUT: «foo => True»
:!foo.say; # OUTPUT: «foo => False»
# 无符号整数
:2nd .say; # OUTPUT: «nd => 2»
:1000th.say; # OUTPUT: «th => 1000»
# Strings and Allomorphs (看起来像数字的字符串是 Str + numeric 类型)
:foo<bar> .say; # OUTPUT: «foo => bar»
:bar<42.5> .say; # OUTPUT: «bar => 42.5»
:bar<42.5>.perl.say; # OUTPUT: «:bar(RatStr.new(42.5, "42.5"))»
# Positionals
:foo['foo', 42.5] .say; # A mutable Array: OUTPUT: «foo => [foo 42.5]»
:foo<foo bar 42.5>.say; # An immutable List: OUTPUT: «foo => (foo bar 42.5)»
# angled brackets give you allomorphs!
# Callables
:foo{ say "Hello, World!" }.say;
# OUTPUT: «foo => -> ;; $_? is raw { #`(Block|82978224) ... }»
# Hashes; keep 'em simple so it doesn't get parsed as a Callable
:foo{ :42a, :foo<a b c> }.say; # OUTPUT: «foo => {a => 42, foo => (a b c)}»
# Name and value from variable
:$foo; # same as :foo($foo)
:$*foo; # same as :foo($*foo)
:$?foo; # same as :foo($?foo)
:$.foo; # same as :foo($.foo)
:$!foo; # same as :foo($!foo)
:@foo; # same as :foo(@foo)
:@*foo; # same as :foo(@*foo)
:@?foo; # same as :foo(@?foo)
:@.foo; # same as :foo(@.foo)
:@!foo; # same as :foo(@!foo)
:%foo; # same as :foo(%foo)
:%*foo; # same as :foo(%*foo)
:%?foo; # same as :foo(%?foo)
:%.foo; # same as :foo(%.foo)
:%!foo; # same as :foo(%!foo)
:&foo; # same as :foo(&foo)
:&*foo; # same as :foo(&*foo)
:&?foo; # same as :foo(&?foo)
:&.foo; # same as :foo(&.foo)
:&!foo; # same as :foo(&!foo)
让我们把这些拆开来仔细看看吧!
标准的,任意类型,非简写方式
Colonpair 的"标准"形式包括一个冒号(:
),一个有效的项(term),作为创建的 Pair 对象的 .key,然后是一组小括号,里面是 Pair 的 .value 表达式。
:nd(2).say; # OUTPUT: «nd => 2»
:foo('foo', 'bar').say; # OUTPUT: «foo => (foo bar)»
:foo( %(:42a, :foo<a b c>) ).say; # OUTPUT: «foo => {a => 42, foo => (a b c)}»
只要 key 是一个有效的标识符,所有其他形式的 colonpairs 都可以用这种方式编写。而对于无效的标识符,你可以简单地使用 .new
方法 – Pair.new('key','value')
— 或"胖箭头"语法。
胖箭头语法
如果你曾经使用过 Perl 5,就不需要再向你介绍这种语法了:写一个 key - 如果它是一个有效的标识符,它会被自动加引号,所以在这些情况下,你可以省略引号,然后你写下 =>
,再写下值。如果 key 不是有效的标识符,那么 key 周围的引号是必须的, 胖箭头是唯一涉及运算符的语法,可以让你用这样的键构造 Pairs:
# (outer parentheses are here just for the .say call)
(nd => 2).say; # OUTPUT: «nd => 2»
(foo => ('foo', 'bar') ).say; # OUTPUT: «foo => (foo bar)»
(foo => %(:42a, :foo<a b c>) ).say; # OUTPUT: «foo => {a => 42, foo => (a b c)}»
("the key" => "the value").say; # OUTPUT: «the key => the value»
这种形式在参数列表以及无符号变量和常量中的行为有一些额外的规则,我们将在文章后面看到。
布尔值简写
现在我们开始进入捷径!命名参数最常见的用途是什么?可能要数指定布尔标志了。
如果总是要写成 :foo(True)
, 那就很烦人了,所以有一种简写方式:完全忽略掉值,如果你想要 :foo(False)
,则省略值,并将取反运算符放在冒号后面:
# Booleans
:foo .say; # OUTPUT: «foo => True»
:!foo.say; # OUTPUT: «foo => False»
# Equivalent calls:
some-sub :foo :!bar :ber;
some-sub foo => True, bar => False, ber => True;
简写形式要短得多。这也是你在副词和正则表达式语法中可能看到的形式,例如 m//
quoter 上的 :g
副词和正则表达式中的 :s
/ :!s
有意义的空白修饰符:
say "a b c def g h i" ~~ m:g/:s \S \S [:!s \S \s+ \S]/;
# OUTPUT: «(「a b c d」 「f g h i」)»
这里还有我个人提供的另一个技巧:因为 Bool 类型是一个 Int,所以可以使用布尔简写键来指定 Int 值 1
和 0
:
# set `batch` to `1`:
^4 .race(:batch).map: { sleep 1 };
say now - ENTER now; # OUTPUT: «1.144883»
然而,为了清晰起见,你可能希望使用无符号整数的 colonpair 简写方式,它不是很长。
无符号整数简写方式
当你用正则表达式匹配东西时,Raku 编程语言可以让你获得第 n 个匹配:
say "first second third" ~~ m:3rd/\S+/;
# OUTPUT: «「third」»
你现在可能已经猜到了:m//
quoter 中 m 后面的 :3rd
是副词,写成无符号整数简写方式中的 colonpair。这种形式由冒号和 key 的名称组成,中间是不带引号的无符号整数值。没有符号,没有小数点,甚至不允许在数字之间用下划线分隔符。
这种简写方式的主要用途是用于带有序号后缀的东西,例如 :1st
,:2nd
,:9th
等。它提供了很好的可读性,但就我个人而言,我对使用此语法的所有无符号整数值没有任何保留,无论键的名字是什么。当你第一次遇到这样的语法时,感觉有点不伦不类,但它很快就会让你成长:
some-sub :1st :2nd :3rd :42foo :100bar;
^4 .race(:1batch).map: { sleep 1 };
Hash/Array/Callable 简写方式
使用标准的 colonpair 格式,你可能会注意到有些形式的括号太多了:
:foo(<42>) # value is an IntStr allomorph
:foo(<a b c>) # value is a List
:foo([<a b c>]) # value is an Array
:foo({:42foo, :100bar}) # value is a Hash
:foo({.contains: 'meows'}) # the value is a Callable
在这些形式中,你可以简单地省略外面的小括号, 让里面的括号和花括号完成他们的工作:
:foo<42> # value is an IntStr allomorph
:foo<a b c> # value is a List
:foo[<a b c>] # value is an Array
:foo{:42foo, :100bar} # value is a Hash
:foo{.contains: 'meows'} # the value is a Callable
它看起来干净多了,写起来也简单多了。Hash 和 Callable 都使用与语言中其他地方的 {...}
构造相同的简单规则:如果内容为空,或者包含以 Pair 字面值或 %-sigiled 开头的单个列表变量,并且不使用 $_
变量或占位符参数,则创建哈希; 否则会创建一个块(可调用)。
尖括号形式(:foo<...>
)遵循与语言中其他地方使用的尖括号引号相同的规则:
:foo< 42 >.value.^name.say; # OUTPUT: «IntStr»
:foo<meows>.value.^name.say; # OUTPUT: «Str»
:foo<a b c>.value.^name.say; # OUTPUT: «List»
请记住,这两种形式并不相同:
:42foo
:foo<42>
第一个创建一个 Int 对象,而第二个创建一个 IntStr 对象,这是一个异形。这种差异对于关注对象相等性的东西很重要,例如集合运算符。
Sigiled 便捷写法
我发现在其他语言中编写代码时很痛苦的一件事就是这样的结构:
my $the-thing-with-a-thing = …
…
some-sub the-thing-with-a-thing => $the-thing-with-a-thing;
将你的变量命名为你希望将该变量作为值传递给某个命名参数的方式相当常见。 Raku 编程语言为这种情况提供了恰到好处的 colonpair 简写。只需在变量名前加一个冒号来构造一个冒号对,其中键名与变量的名字相同(不包括sigil),而值则是该变量的值。唯一的问题是变量必须有一个 sigil,所以你不能在 sigilless或常量的情况下使用这个简写。
my $the-thing-with-a-thing = …
…
some-sub :$the-thing-with-a-thing;
你会注意到,上面的语法看起来像是如何声明一个带有这样一个命名参数的参数 - 一致性是一件好事。支持所有可用的 sigils 和 twigils,这使得此简写的完整变体列表如下所示:
# Name and value from variable
:$foo; # same as :foo($foo)
:$*foo; # same as :foo($*foo)
:$?foo; # same as :foo($?foo)
:$.foo; # same as :foo($.foo)
:$!foo; # same as :foo($!foo)
:@foo; # same as :foo(@foo)
:@*foo; # same as :foo(@*foo)
:@?foo; # same as :foo(@?foo)
:@.foo; # same as :foo(@.foo)
:@!foo; # same as :foo(@!foo)
:%foo; # same as :foo(%foo)
:%*foo; # same as :foo(%*foo)
:%?foo; # same as :foo(%?foo)
:%.foo; # same as :foo(%.foo)
:%!foo; # same as :foo(%!foo)
:&foo; # same as :foo(&foo)
:&*foo; # same as :foo(&*foo)
:&?foo; # same as :foo(&?foo)
:&.foo; # same as :foo(&.foo)
:&!foo; # same as :foo(&!foo)
当前可用的 colonpair 简写列表到此为止。正如你所看到的,大量的简写被缩减为几个简单的模式。但是,这可能不是所有的简写方式。
未来
虽然目前还不能使用,但在未来的语言版本中, 以下两种简写可能会成为语言的一部分。
第一个是间接查找简写方式。如果你有一个命名变量, 并且该变量的名字在另一个变量中,则可以使用间接查找构造访问第一个变量的值:
my $foo = "bar";
my %bar = :42foo, :70bar;
say %::($foo); # OUTPUT: «{bar => 70, foo => 42}»
如果你眯起眼镜,间接查找有点像带符号的变量,而带符号的变量的 colonpair 简写是存在的,所以语言的一致性和支持间接查找 colonpair 简写方式是有意义的,它看起来像这样,其中 $foo
的值包含 colonpair 的键的名称。
:%::($foo)
这种形式目前在 R#1532 中被列为未实现的功能,所以它很有可能在某一天出现。
第二个可能的未来结构是 :.foo
格式,它在 RFC R#1462 中被提出。这种形式在 $_
topical 变量上调用 .foo
方法,并使用返回值作为创建的 Pair 的值,而方法的名称就是键的名称。
当你把一个对象的属性值传递给另一个具有相似命名属性的对象时,这种形式会频繁出现,所以像这样:
Some::Other.new: :foo(.foo) :bar(.bar) :ber(.ber) with $obj
可以简写成这种形式:
Some::Other.new: :.foo :.bar :.ber with $obj
在写这篇文章的时候,这个 RFC 已经被拒绝了,但是你永远不知道是否会有更多的人呼吁引入这种语法。
第二部分: 使用
现在我们熟悉了如何编写所有形式的 colonpair,让我们来看看它们的一些可用用途,特别是那些有特殊规则的用途。
参数
要指定一个参数应该是一个命名的而不是位置参数,只需使用带符号的变量冒号对(colonpair)简写:
sub meow($foo, :$bar) {
say "$foo is positional and $bar is named";
}
meow 42, :100bar; # 42 is positional and 100 is named
meow :100bar, 42; # 42 is positional and 100 is named
由于参数需要某种变量来绑定他们的东西,所以几乎所有其他形式的 colonpair 都不能用于参数中。这意味着,例如,你不能声明无符号的命名参数,而必须明确地使用 is raw trait 来获得原始性:
sub meow (\foo, :$bar is raw) {
(foo, $bar) = ($bar, foo)
}
my $foo = 42;
my $bar = 100;
meow $foo, :$bar;
say [$foo, $bar]; # OUTPUT: «[100 42]»
在参数中还有一种标准的 colonpair 形式, 用于将多个命名参数别名为同一名称和参数解构:
sub meow (:st(:nd(:rd(:$nth))), Positional :list(($, $second, |))) {
say [$nth, $second];
}
meow :3rd, :list<a b c>; # OUTPUT: «[3 b]»
专业提示:如果你使用的是 Rakudo 编译器,你可能希望轻松使用别名。使用超过 1 级深度的别名会导致编译器切换到慢速路径绑定器,正如其名称所暗示的,速度慢大约 10 倍。
你可以使用的一个技巧是使用多个参数,每个参数的别名最多只有1级,然后将它们合并到主体中:
sub meow (:st(:$nd is copy), :rd(:$nth)) {
$nd //= $nth;
}
参数列表
在参数列表中使用 colonpair 值得一个单独的章节,因为这个规则很微妙,足以在语言陷阱部分获得一席之地。这个规则涉及的问题是, 程序员可能希望在参数列表中以命名参数或位置参数的形式传递 Pair 对象。
在大多数情况下,colonpairs 将作为命名参数传递:
sub args {
say "Positional args are: @_.perl()";
say "Named args are: %_.perl()";
}
args :foo, :50bar, e => 42;
# OUTPUT:
# Positional args are: []
# Named args are: {:bar(50), :e(42), :foo}
要传递一个 Pair 对象作为位置参数传递,你可以执行以下任何一项:
① 用圆括号包裹整个 colonpair 对。
② 在 colonpair 对上调用一些方法,比如 .self 或 .Pair ; 像在 =>
运算符上使用 R meta op 这种奇怪的东西也适用。
③ 用 foo => bar
语法把键引起来。
④ 在 foo => bar
语法中,使用一个不是有效标识符的键。
⑤ 把你的 Pair 放在一个列表中然后用 |
运算符把它塞进该列表中。
以下是代码形式的选项列表:
my @pairs := :42foo, :70meow;
args :foo.Pair, (:50bar), "baz" => "ber", e R=> 42, 42 => 42, |@pairs;
# OUTPUT:
# Positional args are: [:foo, :bar(50), :baz("ber"),
# 42 => 2.718281828459045e0, 42 => 42, :foo(42), :meow(70)]
# Named args are: {}
如果你来自其他语言, 比如 Perl 5, 使用胖箭头(=>
)来分隔键/值,那么编号(3)尤其值得关注。这个结构体只有在键不加引号并且是一个有效的标识符时才会作为一个命名参数被传递。
如果你必须使用这些构造之一,但又希望将它们作为命名参数而不是位置参数来传递,只需将它们包裹在圆括号中,并使用 |
前缀将它们"塞"进去。对于我们在前面的例子中已经塞进去的 Pair 列表,你需要先把它强制变成一个 Capture 对象,因为塞进 Capture 的 Pair - 与列表不同 - 最终会成为命名参数,当 Capture 被塞进参数列表的时候:
my @pairs := :42foo, :70meow;
args |(:foo.Pair), |(:50bar), |("baz" => "ber"),
|(e R=> 42), |(42 => 42), |@pairs.Capture;
# OUTPUT:
# Positional args are: []
# Named args are: {"42" => 42, :bar(50),
# :baz("ber"), :foo(42), :meow(70)}
同样的 slipping 技巧可以用来有条件地提供命名参数。
sub foo (:$bar = 'the default') { say $bar }
my $bar;
foo |(:bar($_) with $bar); # OUTPUT: «the default»
$bar = 42;
foo |(:bar($_) with $bar); # OUTPUT: «42»
如果 $bar
没有被定义(defined),那么 with 语句修饰符将返回 Empty,当使用 |
时, 最终将是空的,允许参数达到其默认值。因为 |(:)
看起来像一个侧身的忍者,所以把这种技术称为"忍者参数"。
自动引起, 以 =>
的形式
眼尖的观众可能已经注意到上一节中的 e => 42
colonpair 对使用字母 e
作为键,然而在反转形式 e R=> 42
中, e
变成了 2.718281828459045e0
,因为核心语言把 e
定义为欧拉数。
在 e => 42
形式中, e
仍然是一个普通字符串 e, 原因是 =>
构造会自动引用有效标识符的键,所以它们最终总是会变成字符串,即使存在同名的常量、无符号变量或子例程:
my \meows = '🐱';
sub ehh { rand }
say %(
meows => 'moo',
ehh => 42,
τ => 'meow',
); # OUTPUT: «{ehh => 42, meows => moo, τ => meow}»
有很多方法可以避免这种自动引用行为, 但我只给你看一种足够好的方法: 在键的周围加上一对小括号。
my \meows = '🐱';
sub ehh { rand }
say %(
(meows) => 'moo',
(ehh) => 42,
(τ) => 'meow',
);
# OUTPUT: «{0.58437052771857 => 42, 6.283185307179586 => meow,
# 🐱 => moo}»
简单!
结论
这就是关于 colonpair 所有的知识。我们了解了它们可以用来构造 Pair 对象,作为副词使用,以及用于指定命名参数和参量。
我们学习了各种简写,例如只用键来表示于布尔值 True
,把否定运算符或无符号整数放在冒号和键之间以指定布尔值 False
或 Int
值。我们还了解到,如果在值的周围已经存在一组花括号,方括号或尖括号,那么 colonpair 值上的括号可以省略, 并且在变量名前添加一个冒号将会创建一个将使用该变量名称作为键的 colonpair。
在本文的后半部分,我们讨论了在指定命名参数时可用的 colonpair 语法,将 colonpair 作为命名参数或位置参数传递的特殊性,以及如何通过使用小括号中包裹来避免键的自动引用。
我希望你能发掘这些信息。
-Ofun