第二天-Perl 6: 符号, 变量和容器
对容器的基本理解对于在 Perl 6 中进行愉快的编程是至关重要的。它们无处不在,不仅影响你获得的变量类型,还决定了 List
和 Map
在迭代时的行为方式。
今天,我们将学习什么是容器,以及如何使用它们,但是首先,我希望你暂时忘记你对 Perl 6 的符号和变量的所有知识或怀疑,特别是如果你来自 Perl 5 的背景。 忘记一切。
把钱拿出来
在 Perl 6 中,变量以 $
符号为前缀,用绑定运算符(:=
)赋值。 像这样:
my $foo := 42;
say "The value is $foo"; # OUTPUT: «The value is 42
»
如果你已经按照我的建议来忘记你所知道的一切,那么学习 List
和 Hash
类型也是一样:
my $ordered-things := ;
my $named-things := %(:42foo, :bar);
say "$named-things bottles of $ordered-things[2] on the wall";
# OUTPUT: «42 bottles of ber on the wall
»
.say for $ordered-things; # OUTPUT: «foo
bar
ber
»
.say for $named-things; # OUTPUT: «bar => ber
foo => 42
»
了解这一点,你可以写出各种各样的程序,所以如果你开始觉得有太多的东西需要学习,记住你不需要一次学习所有东西。
我们祝你有一个愉快的列表圣诞
让我们试着用我们的变量做更多的事情。 想要更改列表中的值并不罕见。 到目前为止我们的表现如何呢?
my $list := (1, 2, 3);
$list[0] := 100;
# OUTPUT: «Cannot use bind operator with this left-hand side […] »
尽管我们可以绑定到变量,但是如果我们试图绑定到某个值,我们会得到一个错误,无论这个值是来自 List
还是只是一个字面值:
1 := 100;
# OUTPUT: «Cannot use bind operator with this left-hand side […] »
这就是为什么列表是不可变的。 然而,这是一个实现愿望的季节,所以我们希望有一个可变的List
!
我们需要掌握的是一个 Scalar
对象,因为绑定操作符可以使用它。 顾名思义,一个 Scalar
存储一个东西。 你不能通过 .new
方法实例化一个 Scalar
,但是我们可以通过声明一些词法变量来得到它们。 不需要费心给他们的名字:
my $list := (my $, my $, my $);
$list[0] := 100;
say $list; # OUTPUT: «(100 (Any) (Any))
»
输出中的 (Any)
是容器的默认值(稍后一点)。 上面,似乎我们设法在 List
创建后将一个值绑定到列表的元素上,我们不是吗? 确实我们做了,但是...
my $list := (my $, my $, my $);
$list[0] := 100;
$list[0] := 200;
# OUTPUT: «Cannot use bind operator with this left-hand side […] »
绑定操作用一个新的值(100
)代替 Scalar
容器,所以如果我们试图再次绑定,我们又回到了原来的方括号那个,试图绑定到一个值,而不是一个容器。
我们需要一个更好的工具。
That's Your Assignment
绑定运算符有一个表亲:赋值运算符(=
)。 我们不用一个绑定操作符替换我们的 Scalar
容器,而是使用赋值操作符来赋值或者“存储”我们在容器中的值:
my $list := (my $ = 1, my $ = 2, my $ = 3);
$list[0] = 100;
$list[0] = 200;
say $list;
# OUTPUT: «(200 2 3)
»
现在,我们可以从一开始就指定我们的原始值,并且可以随时用其他值替换它们。 我们甚至可以变得时髦,并在每个容器上放置不同的类型约束:
my $list := (my Int $ = 1, my Str $ = '2', my Rat $ = 3.0);
$list[0] = 100; # OK!
$list[1] = 42; # Typecheck failure!
# OUTPUT: «Type check failed in assignment;
# expected Str but got Int (42) […] »
这有些放纵,但有一件事可以使用类型约束:$list
变量。 我们将其限制为 Positional
角色,以确保它只能保持 Positional
类型,就像 List
和 Array
:
my Positional $list := (my $ = 1, my $ = '2', my $ = 3.0);
不知你咋想的,但是这对我来说看起来非常冗长。 幸运的是,Perl 6 有语法来简化它!
Position@lly
首先,让我们摆脱变量的显式类型约束。 在 Perl 6 中,您可以使用 @
而不是 $
作为符号来表示您希望变量受到角色 Positional
的类型约束:
my @list := 42;
# OUTPUT: «Type check failed in binding;
# expected Positional but got Int (42) […] »
其次,我们将使用方括号来代替圆括号来存储我们的 List
。 这告诉编译器创建一个 Array
而不是一个 List
。 Array
s 是可变的,它们将每个元素自动粘贴到 Scalar
容器中,就像我们在前一节中手动操作一样:
my @list := [1, '2', 3.0];
@list[0] = 100;
@list[0] = 200;
say @list;
# OUTPUT: «[200 2 3]
»
我们的代码变得更短了,但我们可以折腾更多的字符。 就像赋值给$
-sigiled 变量而不是绑定一样,你可以赋值给@
-sigiled 变量来获得一个自由的 Array
。 如果我们切换到赋值,我们可以完全摆脱方括号:
my @list = 1, '2', 3.0;
好,简洁。
类似的想法背后是 %
- 和 &
符号化的变量。 %
sigil 意味着 Associative
角色的类型约束,并为赋值提供相同的快捷方式(给你一个 Hash
),并为这些值创建 Scalar
容器。 对于角色 Callable
和赋值的 &
-sigiled变量类型 - 行为类似于 $
sigils,给出一个可以修改其值的自由 Scalar
容器:
my %hash = :42foo, :bar;
say %hash; # OUTPUT: «{bar => ber, foo => 42}
»
my &reversay = sub { $^text.flip.say }
reversay '6 lreP ♥ I'; # OUTPUT: «I ♥ Perl 6
»
# store a different Callable in the same variable
&reversay = *.uc.say; # a WhateverCode object
reversay 'I ♥ Perl 6'; # OUTPUT: «I ♥ PERL 6
»
The One and Only
之前我们知道赋值给 $
-sigiled 变量会给你一个免费的 Scalar
容器。 由于标量,顾名思义,只包含一个东西......如果你把一个 List
放到 Scalar
中会发生什么? 毕竟,当你试图这样做的时候,宇宙仍然没有被扼杀:
my $listish = (1, 2, 3);
say $listish; # OUTPUT: «(1 2 3)
»
这样的行为可能使 Scalar
看起来似乎是一个用词不当,但它确实把整个列表视为一个东西。 我们可以通过几种方式观察其差异。 我们来比较绑定到 $
-sigiled 变量的 List
(所以不包含 Scalar
)和赋值给 $
-sigiled 变量(自动 Scalar
容器)的 List
:
# Binding:
my $list := (1, 2, 3);
say $list.perl;
say "Item: $_" for $list;
# OUTPUT:
# (1, 2, 3)
# Item: 1
# Item: 2
# Item: 3
# Assignment:
my $listish = (1, 2, 3);
say $listish.perl;
say "Item: $_" for $listish;
# OUTPUT:
# $(1, 2, 3)
# Item: 1 2 3
.perl
方法给了我们一个额外的见解,并在第二个 List
之前显示了一个 $
,以表明它在 Scalar
中是集装箱化的。 更重要的是,当我们用 for
循环迭代我们的 List
s 时,第二个 List
结果只有一个迭代:整个 List
作为一个项目! Scalar
没有辜负它的名字。
这种行为不仅仅是学术上的兴趣。 回想一下,Array
s(和Hash
es)为它们的值创建Scalar
容器。 这意味着如果我们嵌套东西,即使我们选择一个单独的列表或散列在里面存储着 Array
(或 Hash
),并试图迭代它,它将只被视为一个单一的项目:
my @stuff = (1, 2, 3), %(:42foo, :70bar);
say "List Item: $_" for @stuff[0];
say "Hash Item: $_" for @stuff[1];
# OUTPUT:
# List Item: 1 2 3
# Hash Item: bar 70
# foo 42
同样的推理(即 Scalar
容器中的列表和散列是单个项目)适用于当您试图压扁 Array
的元素或将它们作为参数传递给 slurpy 参数时:
my @stuff = (1, 2, 3), %(:42foo, :70bar);
say flat @stuff;
# OUTPUT: «((1 2 3) {bar => 70, foo => 42})
»
-> *@args { @args.say }(@stuff)
# OUTPUT: «[(1 2 3) {bar => 70, foo => 42}]
»
正是这种行为可以将 Perl 6 初学者推上墙,特别是那些来自 Perl 5 自动展平语言的人。然而,现在我们知道为什么会出现这种行为,我们可以改变它!
Decont
如果 Scalar
容器是罪魁祸首,我们所要做的就是删除它。 我们需要将我们的列表和哈希值去容器化,或者简称为 “decont”。 在你的 Perl 6 之旅中,你可以找到几种方法来完成这个工作,但是为此设计的一个方法就是 decont methodop(<>
):
my @stuff = (1, 2, 3), %(:42foo, :70bar);
say "Item: $_" for @stuff[0]<>;
say "Item: $_" for @stuff[1]<>;
# OUTPUT:
# Item: 1
# Item: 2
# Item: 3
# Item: bar 70
# Item: foo 42
它很容易记住:它看起来像一个被挤压的盒子(一个被踩踏的容器)。 在通过索引到 Array
中检索我们的容器化项目之后,我们附加了 decont 并从 Scalar
容器中移除了内容,导致我们的循环遍历它们中的每个项目。
如果您希望一次去除 Array
中的每个元素,只需使用超运算符(»
,或 >>
,如果您更喜欢使用 ASCII)就可以使用 decont:
my @stuff = (1, 2, 3), %(:42foo, :70bar);
say flat @stuff»<>;
# OUTPUT: «(1 2 3 bar => 70 foo => 42)
»
-> *@args { @args.say }(@stuff»<>)
# OUTPUT: «[1 2 3 bar => 70 foo => 42]
»
随着容器被删除,我们的列表和散列就像我们想要的那样变平。 当然,我们可以避免使用 Array
,而将原始 List
绑定到变量上。 由于 List
没有把它们的元素放入容器,所以没有任何东西可以去除:
my @stuff := (1, 2, 3), %(:42foo, :70bar);
say flat @stuff;
# OUTPUT: «(1 2 3 bar => 70 foo => 42)
»
-> *@args { @args.say }(@stuff)
# OUTPUT: «[1 2 3 bar => 70 foo => 42]
»
不要让它溜走
当我们在这里的时候,值得注意的是,当他们想要执行decont(我们不是在传递参数给 Callable
的时候使用它)时,许多人使用 slip运算符(|
):
my @stuff = (1, 2, 3), (4, 5);
say "Item: $_" for |@stuff[0];
# OUTPUT:
# Item: 1
# Item: 2
# Item: 3
虽然它可以完成工作,但可能会引入微妙的 bugs,这些 bug 可能很难追查到。 尝试在这里找到一个,在一个程序中迭代了一个无限的非负整数列表,并打印那些素数:
my $primes = ^∞ .grep: *.is-prime;
say "$_ is a prime number" for |$primes;
放弃? 这个程序会导致内存泄漏... 非常缓慢。 尽管我们遍历了无限的项目列表,但这不是问题,因为 .grep
方法返回的 Seq
对象不会保留已经迭代的项目,因此内存使用永远不会增长。
有问题的部分是我们的 |
slip 操作符。 它将我们的 Seq
转换成一个 Slip
,这是一个 List
类型,并且保存我们已经消耗的所有的值。 如果您希望在 htop
中看到增长,那么这个程序的修改版本会更快地增长:
# CAREFUL! Don't consume all of your resources!
my $primes = ^∞ .map: *.self;
Nil for |$primes;
让我们再试一次,但是这次使用 decont 方法 op:
my $primes = ^∞ .map: *.self;
Nil for $primes<>;
内存使用现在是稳定的,程序可以坐在那里迭代直到时间结束。 当然,因为我们知道这是 Scalar
容器导致的容器化,我们希望在这里避免它,所以我们可以简单地将 Seq
绑定到变量上:
my $primes := ^∞ .map: *.self;
Nil for $primes;
I Want Less
如果你讨厌符号,Perl 6 会得到一些你可以微笑的东西:无符号的变量。 只要在声明中加一个反斜杠的前缀,表示你不想要讨厌的符号:
my \Δ = 42;
say Δ²; # OUTPUT: «1764
»
你不会得到任何这样的变量的自由 Scalar
,因此,在声明期间,绑定或赋值给他们没有任何区别。 它们的行为类似于将值绑定到 $
-sigiled 变量的行为,包括绑定 Scalar
s 并使变量可变:
my \Δ = my $ = 42;
Δ = 11;
say Δ²; # OUTPUT: «121
»
一个更常见的地方,你可能会看到这样的变量是作为例程的参数,在这里,这意味着你想把 is raw
trait 应用到参数上。 这在 +
positional slurpy 参数的含义也是存在的(不需要反斜杠),如果它是 is raw
的,意味着你将不会得到不需要的 Scalar
容器,因为它是一个 Array
,因为它具有 @
sigil:
sub sigiled ($x is raw, +@y) {
$x = 100;
say flat @y
}
sub sigil-less (\x, +y) {
x = 200;
say flat y
}
my $x = 42;
sigiled $x, (1, 2), (3, 4); # OUTPUT: «((1 2) (3 4))
»
say $x; # OUTPUT: «100
»
sigil-less $x, (1, 2), (3, 4); # OUTPUT: «(1 2 3 4)
»
say $x; # OUTPUT: «200
»
Defaulting on Default Defaults
容器提供的一个很棒的功能是默认值。 你可能听说过在 Perl 6 中,Nil
表示缺少一个值,而不是一个值。 容器默认值就是它的作用:
my $x is default(42);
say $x; # OUTPUT: «42
»
$x = 10;
say $x; # OUTPUT: «10
»
$x = Nil;
say $x; # OUTPUT: «42
»
一个容器的默认值是使用 is default
trait 给它的。 它的参数是在编译时计算的,每当容器缺少一个值时,就使用结果值。 由于 Nil
的工作是表明这一点,因此将 Nil
分配到容器中将导致容器包含其默认值,而不是 Nil
。
可以给 Array
和 Hash
容器赋予默认值,如果你希望你的容器在字面上包含 Nil
,当没有值时,只需要指定 Nil
作为默认值:
my @a is default = 1, 2, 3;
say @a[0, 2, 42]; # OUTPUT: «(1 3 meow)
»
@a[0]:delete;
say @a[0]; # OUTPUT: «meow
»
my %h is default(Nil) = :bar;
say %h; # OUTPUT: «(ber Nil)
»
%h:delete;
say %h # OUTPUT: «Nil
»
容器的默认值有一个默认的默认值:容器上的显式类型约束:
say my Int $y; # OUTPUT: «(Int)
»
say my Mu $z; # OUTPUT: «(Mu)
»
say my Int $i where *.is-prime; # OUTPUT: «()
»
$i.new; # OUTPUT: (exception) «You cannot create […]»
如果没有明确的类型约束,默认的默认值是一个 Any
类型的对象:
say my $x; # OUTPUT: «(Any)
»
say $x = Nil; # OUTPUT: «(Any)
»
请注意,您可能在可选参数的例程签名中使用的默认值不是容器默认值,将 Nil
分配给子例程参数或分配给参数不会使用签名中的默认值。
自定义
如果容器的标准行为不适合您的需求,您可以使用 Proxy
类型创建自己的容器:
my $collector := do {
my @stuff;
Proxy.new: :STORE{ @stuff.push: @_[1] },
:FETCH{ @stuff.join: "|" }
}
$collector = 42;
$collector = 'meows';
say $collector; # OUTPUT: «42|meows
»
$collector = 'foos';
say $collector; # OUTPUT: «42|meows|foos
»
接口有点笨重,但它完成了工作。我们使用 .new
方法创建 Proxy
对象,该方法需要两个必需的命名参数:STORE
和 FETCH
,每个都带一个 Callable
。
每当从容器中读取一个值时,FETCH
Callable
被调用,这可能比直接看到的次数多出现一次:在上面的代码中,当容器通过调度和例程这两个调用渗透时,FETCH
Callable
被调用10次。 Callable
被调用一个单一的位置参数:Proxy
对象本身。
无论何时将值存储到我们的容器中(例如,使用赋值运算符(=
)),STORE
Callable
都会被调用。 Callable
的第一个位置参数是 Proxy
对象本身,第二个参数是存储的值。
我们希望 STORE
和 FETCH
Callable
共享 @stuff
变量,所以我们使用 do
statement prefix 和一个代码块来很好地包含它。
我们将我们的 Proxy
绑定到一个变量,其余的只是正常的变量用法。输出显示我们的自定义容器提供的改变过的行为。
Proxies 也可以方便地作为返回值来提供具有可变属性的额外行为。例如,这里有一个属性,从外部看来只是一个正常的可变属性,但实际上强制它的值从 Any
任何类型变为 Int
类型:
class Foo {
has $!foo;
method foo {
Proxy.new: :STORE(-> $, Int() $!foo { $!foo }),
:FETCH{ $!foo }
}
}
my $o = Foo.new;
$o.foo = ' 42.1e0 ';
say $o.foo; # OUTPUT: «42
»
很甜蜜! 如果你想要一个更好的接口的 Proxy
与一些更多的功能,请检查 Proxee 模块。
这就是全部,伙计
那关于这一切。 在 Perl 6 中你将会看到的剩下的动物是 “twigils”:名称前带有两个符号的变量,但是就容器而言,它们的行为与我们所介绍的变量相同。 第二个符号只是表示附加信息,如变量是隐含的位置参数还是命名参数...
sub test { say "$^implied @:parameters[]" }
test 'meow', :parameters;
# OUTPUT: «meow says the cat
»
...或者该变量是私有属性还是公共属性:
with class Foo {
has $!foo = 42;
has @.bar = 100;
method what's-foo { $!foo }
}.new {
say .bar; # OUTPUT: «[100]
»
say .what's-foo # OUTPUT: «42
»
}
然而,这是另一天的旅程。
结论
Perl 6 有一个丰富的变量和容器系统,与 Perl 5 有很大的不同。理解它的工作方式是非常重要的,因为它会影响列表和哈希行为的迭代和展开方式。
赋值给变量提供了有价值的快捷方式,例如提供Scalar
,Array
或Hash
容器,具体取决于符号。 如果您需要,绑定到变量允许您绕过这样的快捷方式。
在 Perl 6 中存在无符号变量,它们与具有绑定功能的 $
-sigiled 变量具有相似的行为。 当用作参数时,这些变量的行为就像应用了 is raw
trait一样。
最后,容器可以有默认值,可以创建自己的自定义容器,可以绑定到变量或从例程返回。
节日快乐!