Containers in Raku
在编程语言中,变量是将一个特定的值与编译器已知的名称关联起来的一种方式。例如,以变量声明及其赋值为例,my $x = "Hello"
。这就把值 42
和名字 $x
联系起来了,虽然这对大多数意图和目的来说是正确的,但在 Raku 中这并不是整个故事。
在 Raku 中,当编译器遇到变量声明 my $x
时,编译器会把它注册到某个内部符号表中。符号表一般提供了按名称查找对象的功能,并允许编译器从程序源代码中提到它的名称来获得一个对象。在 my $x="Hello"
的例子中,变量 $x
的词条垫(lexical pad)条目是一个指向 Scalar 类型的对象的指针,作为对象 “Hello” 的容器。形象地讲,可以用以下方式表示。
Table Container Value
------- +---+ +---------+
$x | * ---> | * | ---> | "Hello" |
------- +---+ +---------+
在图中,符号表中的单元格(标有 *
)与名称 $x
相关联。这就是所谓的绑定。名字经常被绑定到作为其他对象容器的对象上(例如,Scalar 作为对象 “Hello” 的容器),但不一定总是这样,我们将在下一节中看到。目前,只要说使用符号 $
的名字利用了 Scalar
容器,可以绑定到任何对象上就够了。
Item 容器
我们先说说无标号变量。在 Raku 中,无符号变量是一个没有符号的变量,它的值没有被容器化。它们是通过使用转义字符 \
来声明的,我们通过使用绑定操作符 (:=
) 来关联一个值。比如说:
my \y := 42;
say y; #=> «42»
形象地讲,可以这样描述:
Table Value
------- +---------+
y | * ---> | "Hello" |
------- +---------+
无符号变量与它们的值有别名,因此它们不会被绑定到作为其值容器的对象上。这就是名称 y
的情况。名称直接与值 42
绑定,因此在变量名称和绑定值之间没有容器。另一种解释是,现在 y
和 42
都代表了字面值 42
。这意味着,当一个名字被绑定到一个值之后,你不能再把它重新绑定到其他东西上。
42 := 45; #= Cannot use bind operator with this left-hand side
y := 45; #= Cannot use bind operator with this left-hand side
上面的原因是,代表整数 42
的对象永远不会改变为代表另一个不同的对象。在 Raku 中,值类型(及其实例对象),如 Int
(如 42
)和 Str
(如 "Hello"
)是不可改变的。
当一个值被绑定到一个变量后,我们如何使他可变呢?首先,我们可以尝试掌握一个 Scalar
容器,作为变量名和值之间的代理。我们不能通过 new
方法来实例化一个 Scalar
,但是符号 $
是这个任务的最佳选择;Scalar
容器类型默认与符号 $
相关联。我们将一个词法的 Scalar
容器绑定到变量上,而不是字面值。
my \z := my $;
say z; #=> «(Any)»
z := 42; # Cannot use bind operator with this left-hand side
然而,这并没有太大的帮助,从 Cannot use bind operator...
错误就可以看出。当我们声明变量 z
的时候,$
的值已经被绑定到了它上面,而试图重新绑定变量会导致和之前一样的错误。希望现在已经清楚了,但原因是我们试图重新绑定到一个值(即 Scalar
容器),我们不能这样做。然而,我们离目标不远了;我们想要的是能够替换一个容器的值,而不是替换绑定到变量的容器。这就是赋值运算符(=
)的作用,它是绑定运算符(:=
)的近亲。
与绑定运算符不同的是,绑定运算符使变量和它的绑定值成为同一种东西,而赋值运算符则是将右边的值放到赋值的左边的容器中。
我们再试一下。我们不尝试绑定到已经声明的变量,而是对它进行赋值。
my \z := my $;
z = 42;
say z; #=> «42»
z = 45;
say z; #=> «45»
如上所示,我们现在可以分配一个值,也可以用其他值替换它。我们甚至可以给容器添加一个类型约束。
my \z := my Int $; # Only values of type Int
z = 1; # OK!
z = 'T'; # Error: Type check failed in assignment;
# expected Int but got Str ("One")
scalar 符号
我们已经创建了一个变量,我们可以给它赋值,然后通过赋值来改变它。然而,这似乎是一个繁琐的过程,因为我们必须声明一个无符号的变量,然后为其绑定一个 Scalar
容器。如前所述,以符号 $
为前缀的变量会自动为其创建 Scalar
容器,这些变量被称为 scalar 变量。
my $x;
$x = 42;
my Int $y;
$y = $x * 2;
my Str $z;
$z = 'Hello';
我们也可以绑定 $
-sigilled 变量,在这种情况下,不会创建 Scalar
容器,变量是绑定值的别名。在这种状态下,$
-sigilled 变量很像无符号变量。前面没有提到,但事实上,无论使用绑定(:=
)还是赋值(=
),无符号变量总是绑定的。
my $t := 1; # bind 1 to $t, no Scalar container created
my \u := 2; # bind 2 to u
my \v = 2; # bind 2 to v
更多关于 Scalar 容器的东西
Scalar
容器将所有的操作委托给它们所包含的值。每当你使用容器时,它们就会使用其中的值,而且对于大多数目的来说,它们都是隐藏的,在普通使用 Raku 时是看不到的。例如,给定:
my $nvalue := "hello"; # binding, no Scalar container
my $cvalue = "hello"; # assignment, Scalar container
say $nvalue; #=> «hello»
say $cvalue; #=> «hello»
say $nvalue.substr(0, 2); #=> «he»
say $cvalue.substr(0, 2); #=> «he»
你很难分辨出 $nvalue
和 $cvalue
之间的区别。除非你利用容器所提供的功能,如对变量进行赋值、向函数传递 rw 对象等,否则你不会注意到它们的存在(或者没有)。为了检查一个变量是否与值或容器绑定,我们可以使用内省伪方法 VAR
,如果有的话,它会返回底层的 Scalar
对象。
my $nvalue := "hello";
my $cvalue = "hello";
say $nvalue.VAR; #=> «Str»
say $cvalue.VAR; #=> «Str», yep containers work hard to stay hidden.
say $nvalue.VAR.^name; #=> «Str»
say $cvalue.VAR.^name; #=> «Scalar»
到目前为止,我们已经能够将一个单一的值和一个容器绑定到一个变量上。那么将一个值的列表绑定到一个变量上呢?在 Raku 中,我们使用逗号和/或分号来声明一个值的字面量列表(List
)。
my $evens := 2, 4, 8;
say $evens; #=> «(2 4 8)»
say $evens.VAR; #=> «(2 4 8)»
say $evens.VAR.^name; #=> «List»
say $evens[0]; #=> «2»
say $evens[0].VAR; #=> «Int»
say $evens[0].VAR.^name; #=> «Int»
$even := 3; # Cannot use bind operator with this left-hand side
$evens[2] := 6; # Cannot use bind operator with this left-hand side
$evens.push(6); # Cannot use bind operator with this left-hand side
正如你所看到的,试图重新绑定一些值会给我们带来一个错误,不管这个值是来自 List
还是字面值。此外,我们不能通过添加新的值来修改列表。这就是 List
在 Raku 中是如何做到不可改变的;无论是列表本身还是它的值都不能被突变。
为了创建一个元素可以突变的列表,我们可以用一个单一的值来复制之前的过程:将列表中的每个槽(或者至少是那些我们需要突变的槽)容器化,然后将整个列表绑定到变量上。因此,我们最终得到的不是一个字面值的列表,而是一个可以分配给的 Scalar
对象的列表。
my $evens := my $ = 2, my $ = 4, my $ = 8;
say $evens; #=> «(2 4 8)»
say $evens.VAR.^name; #=> «List»
say $evens[0].VAR.^name; #=> «Scalar»
say $evens[0]; #=> «2»
$evens[2] = 6; # OK
$evens.push(7); # Cannot call 'push' on an immutable 'List'
我们已经将列表中的每一个项目容器化,而不是列表本身。因此,虽然我们可以突变它的每个元素,但不能修改列表。
每一个 Scalar
对象也可以被类型限制。
my $evens := (my Int $ = 2, my Int $ = 4, my Int $ = 8);
$evens[2] = 6; # OK!
$evens[1] = 'four'; # Typecheck failure
到目前为止,我们只给容器添加了类型约束,如 2
和 "Hi"
,然而我们可以为 List
做同样的事情。对于像 $evens
这样的变量,我们可能只需要它持有一个由其索引访问的事物列表。在 Raku 中,有 Positional
角色来完成这个任务。这和 List
、Array
等类型实现的角色是一样的,这些类型支持使用运算符 []
进行索引。我们可以约束 Scalar
容器,将 List
分配给它,然而我们不希望 List
容器化。相反,我们约束 List
所绑定的变量。
# This works
my Positional $evens := (my Int $ = 2, my Int $ = 4, my Int $ = 8);
# This doesn't work
my Positional $odds := 3;
# Type check failed in binding; expected Positional but got Int (2)
my Positional[Int] $ints := 3;
# Type check failed in binding; expected Positional[Int] but got Int (3)
变量 $evens
最终是一个 Positional
变量,其值是一个具有可变项的列表。值得重复的是,列表本身是不可变的,只有它的元素是可以变的。例如,你既不能从列表中添加也不能删除元素。
可调用的符号
对于支持通过操作符 ()
调用的事物,&
符号意味着一个 Callable
类型约束,它提供了同样的赋值快捷方式,它给你一个 Callable
,并为值创建一个 Scalar
。与 $
符号类似,这个符号也支持类似项目的赋值。
my &b := { $^x };
say &b.VAR; #=> «-> $x { #`(Block|94463244345248) ... }»
say &b.VAR.^name; #=> «Block»
my &c = { $^x };
say &c.VAR; #=> «-> $x { #`(Block|94463244345248) ... }»
say &c.VAR.^name; #=> «Scalar»
聚合容器
位置标志
在上一节中,声明一个 Positional
变量,其值是一个带有可变项的列表,看起来非常的啰嗦。幸运的是,Raku 提供了一些语法来简化它。
首先,我们不需要显式地用 Positional
来约束变量的类型。我们可以使用符号 @
来代替符号 $
来表示变量有一个 Positional
类型约束。
my @evens := 2 ; # Type check failed in binding;
# expected Positional but got Int (2)
my Int @ints := 1; # Type check failed in binding;
# expected Positional[Int] but got Int (2)
my @odds := 1,; # a single-element list,
# notice the trailing comma
其次,我们将用方括号包围我们的逗号分隔的列表。这告诉编译器要创建一个 Array
而不是 List
。与 List
不同的是,Array
本身是可以突变的,它的每个元素都是可以突变的,因为它们都被自动放入 Scalar
容器中,就像我们在上一节中手动做的那样。顺便说一下,我们可以用括号包围我们的列表,但只有在需要分组的情况下才需要;在 Raku
中,逗号而不是括号可以创建列表。
my @evens := [2, 4, 8];
say @evens.VAR; #=> «[2 4 8]»
say @evens.VAR.^name; #=> «Array»
say @evens[0].VAR.^name; #=> «Scalar»
@evens[2] = @evens[2] - 2; # OK!
@evens[3] = @evens[2] + 2; # OK!
@evens[4] = @evens[2] + 2; # OK!
say @evens; #=> «[2 4 6 8 10]»
我们的代码变得短了很多,但我们仍然可以通过使用赋值而不是绑定来多折腾几个字符。正如前面所演示的那样,赋值到 $
-sigilled 变量时,可以免费为你提供一个 Scalar
容器。在赋值给 @
-sigilled 变量时也是如此,它免费为你提供了一个 Array
容器。如果我们切换到赋值,我们之前的代码可以通过完全放弃方括号而变得更短。我们知道他们指示编译器创建一个 Array
,但在执行赋值时,这已经由 @
完成了。
my @evens = 2, 4, 8;
say @evens.VAR; #=> «[2 4 8]»
say @evens.VAR.^name; #=> «Array»
say @evens[0].VAR.^name; #=> «Scalar»
字面数组是用数组构造函数 []
来创建的,但是如果你像前面演示的那样将其赋值给一个 []
-sigilled 变量,就不需要这样做了。
say [2, 4, 8].VAR; #=> «[2 4 8]»
say [2, 4, 8].VAR.^name; #=> «Array»
say [2, 4, 8][0].VAR; #=> «2»
say [2, 4, 8][0].VAR.^name; #=> «Scalar»
关联符号
到目前为止,我们所做的一切都可以应用于 %
符号变量。%
符号意味着通过操作符 {}
为基于名称的查找提供关联类型约束,并且它为赋值提供了同样的快捷方式,它为变量提供了一个 Hash
容器,并为每个值创建了 Scalar
容器。
my %h := Map.new('a', 1, 'b', 2);
say %h.VAR; #=> «Map.new((a => 1, b => 2))»
say %h.VAR.^name; #=> «Map»
say %h<a>.VAR.^name; #=> «Int»
%h<a> = 12; # Cannot modify an immutable Int (1)...
Map
之于 Hash
,就像列表之于数组;Map
是不可改变的,Map
本身和它的值都不能突变。这是因为 Map
不对其值进行容器化。不像 List
有一个简单的 list
构造函数,没有 map
构造函数,所以我们必须实例化 Map
类。
为了得到一个可突变的 Map
(称为 Hash
),我们可以将 Map
赋值给 %
-sigilled变量,这类似于将 List
赋值给 @
-sigilled 变量以得到一个 Array
。
my %h = Map.new('a', 1, 'b', 2);
say %h.VAR; #=> «{a => 1, b => 2}»
say %h.VAR.^name; #=> «Hash»
say %h<a>.VAR.^name; #=> «Scalar»
%h<a> = 12; # OK!
%h<c> = 25; # OK!
可以使用哈希构造函数 %()
来创建字面哈希值,但是如果你要分配给一个 %
-sigilled 变量,就不需要这样做。
say %('a', 1, 'b', 2).VAR; #=> «{a => 1, b => 2}»
say %('a', 1, 'b', 2).VAR.^name; #=> «Hash»
say %('a', 1, 'b', 2)<a>.VAR; #=> «1»
say %('a', 1, 'b', 2)<a>.VAR.^name; #=> «Scalar»
容器化
一路走来,我们了解到对 $
-sigilled 变量的赋值可以给你一个免费的 Scalar
容器。这既适用于单个值(如一个数字、一个字符串等),也适用于集合值(如一个数字列表、一个字符串列表等)。
对于 $
-sigilled 变量来说,整个列表(或任何集合类型)的赋值仍然是一个单一的东西,即一个列表,它将其视为单一的东西。通过比较一个绑定到 $
-sigilled变量的 List
(在这种情况下,不涉及 Scalar
)和一个分配到 $
-sigilled 变量的 List
(在这种情况下,会自动创建一个 Scalar
容器),可以最好地证明这一点。
# Binding
my $list := (1, 2, 3);
say $list.raku; #=> «(1, 2, 3)»
say "Item: $_" for $list; #=> «Item: 1Item: 2Item: 3»
# Assignment
my $list = (1, 2, 3);
say $list.raku; #=> «$(1, 2, 3)»
say "Item: $_" for $list; #=> «Item: 1 2 3»
raku
方法给我们提供了一个额外的洞察力,它向我们展示了第二个 List
,在它前面有一个 $
,以表示它被容器化在一个 Scalar
中。更重要的是,当我们用 for
循环迭代我们的 List
时,第二个 List
的结果只是一次迭代:整个 List
作为一项。Scalar
容器名不虚传;无论项的复杂程度如何,标量变量都能容纳一项目。
回想一下,数组(和哈希)为它们的每个单独的值创建 Scalar
容器。这意味着,如果我们嵌套东西,即使我们选择存储在数组(或哈希)内部的单个列表或哈希,并尝试在其上进行迭代,它也会被视为只是一个单项。
my @things = (2, 4, 6), %(:France<Paris>, :Peru<Lima>);
say @things[0]; #=> «$(2, 4, 6)»
say @things[0].VAR.^name; #=> «Scalar»
say @things[1]; #=> «${:France("Paris"), :Peru("Lima")}»
say @things[1].VAR.^name; #=> «Scalar»
这证明了 Scalar
容器的一致性。单项,无论其结构如何,在 Scalar
容器内仍然是单项。例如,当你尝试扁平化一个 Array
的元素或将它们作为参数传递给一个 slurpy 参数时,就会出现这种行为。
my @things = (2, 4, 6), %(:France<Paris>, :Peru<Lima>);
my @mixed = 2, 4, 6, :France<Paris>, :Peru<Lima>;
say flat @things; #=> «((2, 4, 6), {France => 'Paris', Peru => 'Lima'})»
say flat @things[0]; #=> «(2 4 6)»
say flat @things[1]; #=> «{France => 'Paris', Peru => 'Lima'}»
my &p = -> *@args { @args };
say p @things; #=> «(2 4 6){France => 'Paris', Peru => 'Lima'}»
say p @mixed; #=> «2 4 6 France => Paris Peru => Lima»
绑定与赋值
从目前的讨论来看,下面的内容可能都很明显,但还是值得在这里总结一下。
每当你将一个右侧实体绑定到一个变量上时,就会跳过容器(Scalar
、Array
等)的创建,直接将实体绑定到变量上,而不管变量的 sigil 可能暗示了什么。在绑定操作中,sigil 给出的关于变量的唯一东西就是可以绑定到它的值的类型。sigil @
意味着 Positionitional
角色,只有实现该角色的类型(List
、Array
、Range
和 Buf
)的值才能被绑定到一个 @
符号变量上。另一方面,符号 %
意味着关联角色,只有实现这个角色的类型(Hash
, Map
等)的值才能被绑定到一个以 %
为标志的变量上。标号 &
意味着可调用角色。至于 $
,任何值都可以绑定到 $
-sigilled变量上,除非该变量被进一步约束。无标号变量没有标号,因此它们的绑定值并不限制于某些类型。
# anything goes with $
my $anythinga := 1; # OK!
my $anythingb := 1, 2; # OK!
my $anythingc := {A => 1, B => 2}; # OK!
my \anythinga := 1; # OK!
my \anythingb := 1, 2; # OK!
my \anythingc := {A => 1, B => 2}; # OK!
# only positionals for @
my @only-positionala := 1; # Error
my @only-positionalb := 1, 2; # OK!
my @only-positionalc := {A => 1, B => 2}; # Error
# only associatives for %
my %only-associativea := 1; # Error
my %only-associativeb := 1, 2; # Error
my %only-associativec := {A => 1, B => 2}; # OK!
另一方面,赋值1)提示编译器创建一个自动容器,这个容器由变量的 sigil 决定,2)在容器中存储任何值,最终3)将容器与变量绑定。
因此,在赋值过程中,sigil 的责任是双重的。
限制可以绑定到变量上的值的类型。对于符号 @
来说,这意味着只有实现 Positional
角色的值才能绑定到 @
符号变量上。
指示编译器为 sigil 创建相应的容器。对于 sigil $
,这意味着创建 Scalar
容器。而对于 sigil @
,则是创建 Array
容器。
去容器化
前面我们看到了 Scalar
是如何影响扁平化一个 Array
元素的行为,或者将它们作为参数粘贴到一个 slurpy 参数上。在这些情况下,我们希望对我们的列表和哈希进行去容器化,这可以通过使用去容器化方法(<>
)来实现。
my @things = (2, 4, 6), %(:France<Paris>, :Peru<Lima>);
.say for @things[0]; #=> «(2 4 6)»
.say for @things[0]<>; #=> «246»
.say for @things[1]; #=> «{France => 'Paris', Peru => 'Lima'}»
.say for @things[1]<>; #=> «France => 'Paris'Peru => 'Lima'»
要对一个数组中的每个元素进行去容器化,我们可以用超运算符 »
简单地对去容器化方法进行超运算。
my @things = (2, 4, 6), %(:France<Paris>, :Peru<Lima>);
say flat @things»<>; #=> «2 4 6 France => Paris Peru => Lima»
my &p = -> *@args { @args };
say p @things»<>; #=> «2 4 6 France => Paris Peru => Lima»
尽管如此,我们本可以通过简单地将最上层的列表绑定到变量上,来避免 Array
对我们的列表和哈希进行容器化。列表不会将它们的项目放入容器中,所以没有什么需要解容器化的。
my @things := (2, 4, 6), %(:France<Paris>, :Peru<Lima>);
say flat @things; #=> «2 4 6 France => Paris Peru => Lima»
my &p = -> *@args { @args };
say p @things; #=> «2 4 6 France => Paris Peru => Lima»
创建自定义容器
可以通过使用 Proxy
类来创建自定义容器。这个类有两个方法,允许你设置一个钩子,每当从容器中获取一个值(FETCH
)或设置一个值(STORE
)时,这个钩子就会执行。
我们使用构造函数 new
方法创建 Proxy
对象,并为 FETCH
和 STORE
方法提供两个命名参数。
-
每当从容器中读取一个值时,
FETCH
Callable
就会被调用。Callable
被调用时只有一个位置参数:Proxy
对象本身。 -
每当一个值被存储到容器中时(即在进行赋值时),
STORE
Callable
就会被调用。Callable
的第一个位置参数是Proxy
对象本身,第二个参数是给出要存储的值。
下面的例子说明了如何创建一个 Proxy
对象,该对象可以存储分配给它绑定的变量的所有偶数,并在读取变量的内容时返回它们。为了避免 Proxy
对象的 Scalar
容器化,我们将它绑定到变量上;或者我们可以使用一个无符号变量。
sub collect-evens {
my @evens;
Proxy.new:
STORE => method ($proxy: UInt \new) { @evens.push(new) if new %% 2 },
FETCH => method ($proxy: ) { @evens },
;
}
my $evens := collect-evens();
$evens = $_ for 0..10;
say $evens; #=> [0 2 4 6 8 10]
say $evens.VAR; #=> $[0 2 4 6 8 10]
say $evens.VAR.^name; #=> Proxy
鸣谢
如果没有下面这些写法,我是不可能写出这篇文章的。事实上,这篇文章本身主要是对那些文章中讨论的内容的复述,所以一定要读一读。