第二天-Raku: 符号, 变量和容器

Sigils, Variables, and Containers

第二天-Raku: 符号, 变量和容器

对容器的基本理解对于在 Raku 中进行愉快的编程是至关重要的。它们无处不在,不仅影响你获得的变量类型,还决定了 ListMap 在迭代时的行为方式。

今天,我们将学习什么是容器,以及如何使用它们,但是首先,我希望你暂时忘记你对 Raku 的符号和变量的所有知识或怀疑,特别是如果你来自 Perl 5 的背景。 忘记一切。

把钱拿出来

在 Raku 中,变量以 $ 符号为前缀,用绑定运算符(:=)赋值。 像这样:

my $foo := 42;
say "The value is $foo"; # OUTPUT: «The value is 42␤»

如果你已经按照我的建议来忘记你所知道的一切,那么学习 ListHash 类型也是一样:

my $ordered-things := <foo bar ber>;
my $named-things   := %(:42foo, :bar<ber>);

say "$named-things<foo> 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 类型,就像 ListArray

my Positional $list := (my $ = 1, my $ = '2', my $ = 3.0);

不知你咋想的,但是这对我来说看起来非常冗长。 幸运的是,Raku 有语法来简化它!

Position@lly

首先,让我们摆脱变量的显式类型约束。 在 Raku 中,您可以使用 @ 而不是 $ 作为符号来表示您希望变量受到角色 Positional 的类型约束:

my @list := 42;
# OUTPUT: «Type check failed in binding;
#   expected Positional but got Int (42) […] »

其次,我们将使用方括号来代替圆括号来存储我们的 List。 这告诉编译器创建一个 Array 而不是一个 ListArrays 是可变的,它们将每个元素自动粘贴到 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<ber>;
say %hash;  # OUTPUT: «{bar => ber, foo => 42}␤»

my &reversay = sub { $^text.flip.say }
reversay '6 lreP ♥ I'; # OUTPUT: «I ♥ Raku␤»

# store a different Callable in the same variable
&reversay = *.uc.say;  # a WhateverCode object
reversay 'I ♥ Raku'; # 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 循环迭代我们的 Lists 时,第二个 List 结果只有一个迭代:整个 List 作为一个项目! Scalar 没有辜负它的名字。

这种行为不仅仅是学术上的兴趣。 回想一下,Arrays(和Hashes)为它们的值创建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}]␤»

正是这种行为可以将 Raku 初学者推上墙,特别是那些来自 Perl 5 自动展平语言的人。然而,现在我们知道为什么会出现这种行为,我们可以改变它!

Decont

如果 Scalar 容器是罪魁祸首,我们所要做的就是删除它。 我们需要将我们的列表和哈希值去容器化,或者简称为 “decont”。 在你的 Raku 之旅中,你可以找到几种方法来完成这个工作,但是为此设计的一个方法就是 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

如果你讨厌符号,Raku 会得到一些你可以微笑的东西:无符号的变量。 只要在声明中加一个反斜杠的前缀,表示你不想要讨厌的符号:

my \Δ = 42;
say Δ²; # OUTPUT: «1764␤»

你不会得到任何这样的变量的自由 Scalar,因此,在声明期间,绑定或赋值给他们没有任何区别。 它们的行为类似于将值绑定到 $ -sigiled 变量的行为,包括绑定 Scalars 并使变量可变:

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

容器提供的一个很棒的功能是默认值。 你可能听说过在 Raku 中,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

可以给 ArrayHash 容器赋予默认值,如果你希望你的容器在字面上包含 Nil,当没有值时,只需要指定 Nil 作为默认值:

my @a is default<meow> = 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<ber>;
say %h<bar foos>; # OUTPUT: «(ber Nil)␤»

%h<bar>:delete;
say %h<bar>       # OUTPUT: «Nil␤»

容器的默认值有一个默认的默认值:容器上的显式类型约束:

say my Int $y; # OUTPUT: «(Int)␤»
say my Mu  $z; # OUTPUT: «(Mu)␤»

say my Int $i where *.is-prime; # OUTPUT: «(<anon>)␤»
$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 对象,该方法需要两个必需的命名参数:STOREFETCH,每个都带一个 Callable

每当从容器中读取一个值时,FETCHCallable 被调用,这可能比直接看到的次数多出现一次:在上面的代码中,当容器通过调度和例程这两个调用渗透时,FETCHCallable 被调用10次。 Callable 被调用一个单一的位置参数:Proxy 对象本身。

无论何时将值存储到我们的容器中(例如,使用赋值运算符(=)),STORE Callable 都会被调用。 Callable 的第一个位置参数是 Proxy 对象本身,第二个参数是存储的值。

我们希望 STOREFETCH 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 模块。

这就是全部,伙计

那关于这一切。 在 Raku 中你将会看到的剩下的动物是 “twigils”:名称前带有两个符号的变量,但是就容器而言,它们的行为与我们所介绍的变量相同。 第二个符号只是表示附加信息,如变量是隐含的位置参数还是命名参数…

sub test { say "$^implied @:parameters[]" }
test 'meow', :parameters<says the cat>;
# 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␤»
}

然而,这是另一天的旅程。

结论

Raku 有一个丰富的变量和容器系统,与 Perl 5 有很大的不同。理解它的工作方式是非常重要的,因为它会影响列表和哈希行为的迭代和展开方式。

赋值给变量提供了有价值的快捷方式,例如提供ScalarArrayHash 容器,具体取决于符号。 如果您需要,绑定到变量允许您绕过这样的快捷方式。

在 Raku 中存在无符号变量,它们与具有绑定功能的 $ -sigiled 变量具有相似的行为。 当用作参数时,这些变量的行为就像应用了 is raw trait一样。

最后,容器可以有默认值,可以创建自己的自定义容器,可以绑定到变量或从例程返回。

节日快乐!

Raku 

comments powered by Disqus