本节解释了原始数据、变量和容器在 Raku 中如何相互关联。解释了 Raku 中使用的不同类型的容器,以及对它们适用的操作,如分配、绑定和扁平化。最后讨论了更高级的主题,如自引用数据、类型约束和自定义容器。

有关 Raku 中各种有序容器的更深入讨论,请参见 列表、序列和数组 的概述;有关无序容器,请参见 集合、包和混合

什么是变量?§

有些人喜欢说“一切都是对象”,但实际上变量在 Raku 中不是用户公开的对象。

当编译器遇到像 my $x 这样的变量作用域声明时,它会在某个内部符号表中注册它。此内部符号表用于检测未声明的变量并将变量的代码生成与正确的作用域绑定。

在运行时,变量显示为词法垫或简称lexpad中的一个条目。这是一个按作用域的数据结构,它为每个变量存储一个指针。

my $x 的情况下,变量 $x 的 lexpad 条目是指向类型为 Scalar 的对象的指针,通常称为容器

标量容器§

虽然 Scalar 类型的对象在 Raku 中随处可见,但您很少直接看到它们作为对象,因为大多数操作都会进行 *解容器化*,这意味着它们作用于 Scalar 容器的内容而不是容器本身。

在类似以下的代码中:

my $x = 42;
say $x;

赋值 $x = 42 将指向 Int 对象 42 的指针存储在词法域条目指向的标量容器中。

赋值运算符要求左侧的容器存储其右侧的值。这究竟意味着什么取决于容器类型。对于 Scalar,这意味着“用新值替换先前存储的值”。

请注意,子例程签名允许传递容器。

sub f($a is rw{
    $a = 23;
}
my $x = 42;
f($x);
say $x;         # OUTPUT: «23␤»

在子例程内部,$a 的词法域条目指向与 $x 在子例程外部指向的相同容器。这就是为什么对 $a 的赋值也会修改 $x 的内容。

同样,如果一个例程被标记为 is rw,它可以返回一个容器。

my $x = 23;
sub f() is rw { $x };
f() = 42;
say $x;         # OUTPUT: «42␤»

对于显式返回,必须使用 return-rw 而不是 return

返回容器是 is rw 属性访问器的工作方式。所以

class A {
    has $.attr is rw;
}

等同于

class A {
    has $!attr;
    method attr() is rw { $!attr }
}

标量容器对于类型检查和大多数只读访问是透明的。.VAR 使它们可见

my $x = 42;
say $x.^name;       # OUTPUT: «Int␤» 
say $x.VAR.^name;   # OUTPUT: «Scalar␤»

而参数上的 is rw 要求存在可写的标量容器。

sub f($x is rw{ say $x };
f 42;
CATCH { default { say .^name''.Str } };
# OUTPUT: «X::Parameter::RW: Parameter '$x' expected a writable container, but got Int value␤»

可调用容器§

可调用容器在 Routine 调用的语法和存储在容器中的对象的 CALL-ME 方法的实际调用之间架起了一座桥梁。声明容器时需要使用符号 &,执行 Callable 时必须省略。默认类型约束是 Callable

my &callable = -> $ν { say "$ν is "$ν ~~ Int ?? "whole" !! "not whole" }
callable(⅓);   # OUTPUT: «0.333333 is not whole␤» 
callable(3);   # OUTPUT: «3 is whole␤»

引用存储在容器中的值时必须提供符号。这反过来允许 Routine 作为 参数 传递给调用。

sub f() {}
my &g = sub {}
sub caller(&c1&c2){ c1c2 }
caller(&f&g);

绑定§

除了赋值,Raku 还支持使用 := 运算符进行 *绑定*。将值或容器绑定到变量时,会修改变量的词法域条目(而不仅仅是它指向的容器)。如果您写

my $x := 42;

那么 $x 的词法域条目将直接指向 Int 42。这意味着您不能再对其进行赋值

my $x := 42;
$x = 23;
CATCH { default { say .^name''.Str } };
# OUTPUT: «X::AdHoc: Cannot assign to an immutable value␤»

您也可以将变量绑定到其他变量

my $a = 0;
my $b = 0;
$a := $b;
$b = 42;
say $a;         # OUTPUT: «42␤»

在这里,在初始绑定之后,$a$b 的词法域条目都指向同一个标量容器,因此对一个变量的赋值也会改变另一个变量的内容。

您之前已经见过这种情况:这正是标记为 is rw 的签名参数所发生的情况。

无符号变量和具有 is raw 特性的参数始终绑定(无论使用 = 还是 :=

my $a = 42;
my \b = $a;
b++;
say $a;         # OUTPUT: «43␤» 
 
sub f($c is raw{ $c++ }
f($a);
say $a;         # OUTPUT: «44␤»

标量容器和列表式事物§

Raku 中有许多位置容器类型,它们具有略微不同的语义。最基本的是 List,它由逗号运算符创建。

say (123).^name;    # OUTPUT: «List␤»

列表是不可变的,这意味着您不能更改列表中的元素数量。但如果其中一个元素恰好是一个标量容器,您仍然可以对其进行赋值

my $x = 42;
($x12)[0= 23;
say $x;                 # OUTPUT: «23␤» 
($x12)[1= 23;     # Cannot modify an immutable value 
CATCH { default { say .^name''.Str } };
# OUTPUT: «X::Assignment::RO: Cannot modify an immutable Int␤»

因此,列表并不关心其元素是值还是容器,它们只是存储和检索传递给它们的内容。

列表也可以是惰性的;在这种情况下,末尾的元素将按需从迭代器生成。

一个 Array 就像一个列表,除了它强制所有元素都是容器,这意味着您始终可以对元素进行赋值

my @a = 123;
@a[0= 42;
say @a;         # OUTPUT: «[42 2 3]␤»

@a 实际上存储了三个标量容器。@a[0] 返回其中一个,赋值运算符用新值 42 替换存储在该容器中的整数值。

对数组变量进行赋值和绑定§

对标量变量和数组变量的赋值都执行相同的操作:丢弃旧值,并输入一些新值。

尽管如此,观察它们的不同之处很容易。

my $x = 42say $x.^name;   # OUTPUT: «Int␤» 
my @a = 42say @a.^name;   # OUTPUT: «Array␤»

这是因为 Scalar 容器类型很好地隐藏了自己,但 Array 并没有这样做。此外,对数组变量的赋值是强制性的,因此可以将非数组值赋值给数组变量。

要将非 Array 放入数组变量中,可以使用绑定。

my @a := (123);
say @a.^name;               # OUTPUT: «List␤»

绑定到数组元素§

有趣的是,Raku 支持绑定到数组元素。

my @a = (123);
@a[0:= my $x;
$x = 42;
say @a;                     # OUTPUT: «[42 2 3]␤»

如果您已经阅读并理解了之前的解释,现在该思考这怎么可能了。毕竟,绑定到变量需要该变量的 lexpad 条目,虽然数组有一个,但每个数组元素都没有 lexpad 条目,因为您无法在运行时扩展 lexpad。

答案是,绑定到数组元素是在语法级别识别的,而不是为正常的绑定操作发出代码,而是对数组调用一个特殊方法(称为 BIND-KEY)。此方法处理绑定到数组元素。

请注意,虽然支持,但通常应避免将未容器化的内容直接绑定到数组元素。这样做可能会在稍后使用数组时产生违反直觉的结果。

my @a = (123);
@a[0:= 42;         # This is not recommended, use assignment instead. 
my $b := 42;
@a[1:= $b;         # Nor is this. 
@a[2= $b;          # ...but this is fine. 
@a[12:= 12;    # runtime error: X::Bind::Slice 
CATCH { default { say .^name''.Str } };
# OUTPUT: «X::Bind::Slice: Cannot bind to Array slice␤»

混合使用列表和数组的操作通常会防止这种情况意外发生。

扁平化、项目和容器§

Raku 中的 %@ 符号通常表示迭代结构的多个值,而 $ 符号仅表示一个值。

my @a = 123;
for @a { };         # 3 iterations 
my $a = (123);
for $a { };         # 1 iteration

@ 符号变量在列表上下文中不会扁平化。

my @a = 123;
my @b = @a45;
say @b.elems;               # OUTPUT: «3␤»

有一些操作会扁平化不在标量容器中的子列表:贪婪参数 (*@a) 和对 flat 的显式调用。

my @a = 123;
say (flat @a45).elems;  # OUTPUT: «5␤» 
 
sub f(*@x{ @x.elems };
say f @a45;             # OUTPUT: «5␤»

您还可以使用 | 创建一个 Slip,将列表引入另一个列表。

my @l := 12, (34, (56)), [78, (910)];
say (|@l1112);    # OUTPUT: «(1 2 (3 4 (5 6)) [7 8 (9 10)] 11 12)␤» 
say (flat @l1112# OUTPUT: «(1 2 3 4 5 6 7 8 (9 10) 11 12)␤»

在第一种情况下,@l 的每个元素都作为结果列表的相应元素被滑动。另一方面,flat扁平化所有元素,包括包含的数组的元素,除了 (9 10)

如上所述,标量容器会阻止这种扁平化。

sub f(*@x{ @x.elems };
my @a = 123;
say f $@a45;            # OUTPUT: «3␤»

@ 字符也可以用作前缀,将参数强制转换为列表,从而移除标量容器。

my $x = (123);
.say for @$x;               # 3 iterations

但是,decont 运算符 <> 更适合对非列表项进行解容器化。

my $x = ^Inf .grep: *.is-prime;
say "$_ is prime" for @$x;  # WRONG! List keeps values, thus leaking memory 
say "$_ is prime" for $x<># RIGHT. Simply decontainerize the Seq 
 
my $y := ^Inf .grep: *.is-prime# Even better; no Scalars involved at all

方法通常不关心它们的调用者是否在标量中,因此

my $x = (123);
$x.map(*.say);              # 3 iterations

映射到三个元素的列表,而不是一个元素。

自引用数据§

容器类型,包括 ArrayHash,允许您创建自引用结构。

my @a;
@a[0= @a;
put @a.raku;
# OUTPUT: «((my @Array_75093712) = [@Array_75093712,])␤»

虽然 Raku 不会阻止您创建和使用自引用数据,但这样做可能会导致您陷入循环,试图转储数据。作为最后的手段,您可以使用 Promise 来 处理 超时。

类型约束§

任何容器都可以具有类型约束,形式为 类型对象子集。两者都可以放在声明符和变量名之间,或者放在特征 of 之后。约束是变量的属性,而不是容器的属性。

subset Three-letter of Str where .chars == 3;
my Three-letter $acronym = "ÞFL";

在这种情况下,类型约束是(编译时定义的)子集 Three-letter

Scalar 容器的默认类型约束是 Mu。容器上类型约束的自省由 .VAR.of 方法提供,对于 @% 符号变量,该方法提供值的约束。

my Str $x;
say $x.VAR.of;  # OUTPUT: «(Str)␤» 
my Num @a;
say @a.VAR.of;  # OUTPUT: «(Num)␤» 
my Int %h;
say %h.VAR.of;  # OUTPUT: «(Int)␤»

定义约束§

容器还可以强制变量被定义。在声明中放入一个笑脸。

my Int:D $def = 3;
say $def;   # OUTPUT: «3␤» 
$def = Int# Typecheck failure

您还需要在声明中初始化变量,毕竟它不能保持未定义状态。

也可以使用 默认定义变量 pragma 在作用域中声明的所有变量上强制执行此约束。来自其他语言的人,在这些语言中变量总是被定义的,可能需要看一下。

自定义容器§

为了提供自定义容器,Raku 使用了 Proxy 类。它的构造函数接受两个参数,FETCHSTORE,它们指向在从容器中获取或存储值时调用的方法。容器本身不执行类型检查,其他限制(如只读)也可以被打破。因此,返回的值必须与它绑定的变量的类型相同。我们可以使用 类型捕获 来处理 Raku 中的类型。

sub lucky(::T $type{
    my T $c-value# closure variable 
    return-rw Proxy.new(
        FETCH => method () { $c-value },
        STORE => method (T $new-value{
            X::OutOfRange.new(what => 'number'got => '13'range => '-∞..12, 14..∞').throw
                if $new-value == 13;
            $c-value = $new-value;
        }
    );
}
 
my Int $a := lucky(Int);
say $a = 12;    # OUTPUT: «12␤» 
say $a = 'FOO'# X::TypeCheck::Binding 
say $a = 13;    # X::OutOfRange 
CATCH { default { say .^name''.Str } };