标量结构§

有些类没有内部结构,要访问它们的各个部分,必须使用特定的方法。数字、字符串和其他一些单片类都包含在该类中。它们使用 $ 符号,尽管复杂数据结构也可以使用它。

my $just-a-number = 7;
my $just-a-string = "8";

有一个 Scalar 类,它用于在内部为使用 $ 符号声明的变量分配默认值。

my $just-a-number = 333;
say $just-a-number.VAR.^name# OUTPUT: «Scalar␤»

任何复杂数据结构都可以通过使用 项目上下文化器 $标量化

(123$(45))[3].VAR.^name.say# OUTPUT: «Scalar␤»

但是,这意味着它们将在其所在的上下文中被视为标量。您仍然可以访问其内部结构。

(123$(45))[3][0].say# OUTPUT: «4␤»

一个有趣的副作用,或者可能是预期的功能,是标量化保留了复杂结构的标识。

for ^2 {
     my @list = (11);
     say @list.WHICH;
} # OUTPUT: «Array|93947995146096␤Array|93947995700032␤»

每次分配 (1, 1) 时,创建的变量在 === 的意义上将是不同的;正如所示,内部指针表示的不同值被打印出来。然而

for ^2 {
  my $list = (11);
  say $list.WHICH
} # OUTPUT: «List|94674814008432␤List|94674814008432␤»

在本例中,$list 使用了标量符号,因此将是一个 Scalar。任何具有相同值的标量将完全相同,如打印指针时所示。

复杂数据结构§

复杂数据结构分为两大类:Positional,或列表式,以及 Associative,或键值对式,具体取决于您如何访问其一级元素。一般来说,复杂数据结构,包括对象,将是两者的结合,对象属性被归类为键值对。虽然所有对象都是 Mu 的子类,但一般来说,复杂对象是 Any 的子类的实例。虽然理论上可以混合使用 PositionalAssociative,但大多数适用于复杂数据结构的方法是在 Any 中实现的。

导航这些复杂数据结构是一个挑战,但 Raku 提供了几个可以在其上使用的函数:deepmapduckmap。虽然前者将按顺序访问每个元素,并执行传递的块所需的任何操作,

say [[12, [34]],[[56, [78]]]].deepmap*.elems );
# OUTPUT: «[[1 1 [1 1]] [1 1 [1 1]]]␤»

它返回 1,因为它进入更深层级并对它们应用 elemsduckmap 可以执行更复杂的操作

say [[12, [34]], [[56, [78]]]].duckmap:
   -> $array where .elems == 2 { $array.elems };
# OUTPUT: «[[1 2 2] [5 6 2]]␤»

在本例中,它深入结构,但如果元素不满足块中的条件(1, 2),则返回元素本身,如果满足条件(每个子数组末尾的两个 2),则返回数组的元素数量。

由于 deepmapduckmapAny 方法,因此它们也适用于关联数组

say %first => [12], second => [3,4] ).deepmap*.elems );
# OUTPUT: «{first => [1 1], second => [1 1]}␤»

只是在这种情况下,它们将应用于每个作为值的列表或数组,而保留键不变。

PositionalAssociative 可以相互转换。

say %first => [12], second => [3,4] ).list[0];
# OUTPUT: «second => [3 4]␤»

但是,在这种情况下,对于 Rakudo >= 2018.05,它每次运行都会返回不同的值。哈希将被转换为键值对的列表,但保证是无序的。您也可以执行相反的操作,只要列表的元素数量为偶数(奇数将导致错误)

say <a b c d>.Hash # OUTPUT: «{a => b, c => d}␤»

但是

say <a b c d>.Hash.kv # OUTPUT: «(c d a b)␤»

每次运行都会获得不同的值;kv 将每个 Pair 转换为列表。

复杂数据结构通常也是 Iterable。从它们中生成一个 迭代器 将允许程序逐个访问结构的第一级

.say for 'א'..'ס'# OUTPUT: «א␤ב␤ג␤ד␤ה␤ו␤ז␤ח␤ט␤י␤ך␤כ␤ל␤ם␤מ␤ן␤נ␤ס␤»

'א'..'ס' 是一个 Range,一个复杂的数据结构,在前面加上 for 将迭代直到列表耗尽。您可以通过覆盖 迭代器 方法(来自角色 Iterable)在您的复杂数据结构上使用 for

class SortedArray is Array {
  method iterator() {
    self.sort.iterator
  }
};
my @thing := SortedArray.new([3,2,1,4]);
.say for @thing# OUTPUT: «1␤2␤3␤4␤»

for 直接在 @thing 上调用 iterator 方法,使其按顺序返回数组的元素。有关 迭代的更多信息,请参阅专门介绍它的页面

函数式结构§

Raku 是一种函数式语言,因此函数是一流的数据结构。函数遵循 Callable 角色,它是四个基本角色中的第四个。Callable& 符号一起使用,尽管在大多数情况下为了简单起见省略了它;在 Callable 的情况下,始终允许省略此符号。

my &a-func= { (^($^þ)).Seq };
say a-func(3), a-func(7); # OUTPUT: «(0 1 2)(0 1 2 3 4 5 6)␤»

Block 是最简单的可调用结构,因为 Callable 不能实例化。在本例中,我们实现了一个记录事件并可以检索它们的块

my $logger = -> $event$key = Nil  {
  state %store;
  if ( $event ) {
    %store{ DateTime.newnow ) } = $event;
  } else {
    %store.keys.grep( /$key/ )
  }
}
$logger"Stuff" );
$logger"More stuff" );
say $loggerNil"2018-05-28" ); # OUTPUT: «(Stuff More stuff)␤»

一个 Block 具有一个 Signature,在本例中是两个参数,第一个是将要记录的事件,第二个是检索事件的键。它们将以独立的方式使用,但其目的是展示 状态变量 的使用,该变量从每次调用到下一次调用都保留下来。此状态变量封装在块中,无法从外部访问,除非使用块提供的简单 API:使用第二个参数调用块。可以克隆 Block

my $clogger = $logger.clone;
$clogger"Clone stuff" );
$clogger"More clone stuff" );
say $cloggerNil"2018-05-28" );
# OUTPUT: «(Clone stuff More clone stuff)␤» 

克隆将重置状态变量;而不是克隆,我们可以创建外观来更改 API。例如,消除使用 Nil 作为第一个参数来检索特定日期的日志的需要

my $gets-logs = $logger.assumingNil* );
$logger( %(changing => "Logs") );
say $gets-logs"2018-05-28" );
# OUTPUT: «({changing => Logs} Stuff More stuff)␤» 

assuming 围绕一个块调用进行包装,为我们需要的参数提供一个值(在本例中为 Nil),并将参数传递给我们使用 * 表示的其他参数。实际上,这对应于自然语言语句“我们正在调用 $logger假设第一个参数是 Nil”。我们可以稍微改变这两个块的外观,以明确它们实际上是在对同一个块进行操作。

my $Logger = $logger.clone;
my $Logger::logs = $Logger.assuming*Nil );
my $Logger::get = $Logger.assumingNil* );
$Logger::logs( <an array> );
$Logger::logs( %(key => 42) );
say $Logger::get"2018-05-28" );

虽然 :: 通常用于调用类方法,但它实际上是变量名称的有效部分。在本例中,我们按照惯例使用它们来简单地指示 $Logger::logs$Logger::get 实际上是在调用 $Logger,我们将其大写以使用类似类的外观。本教程的重点是,将函数用作一等公民,以及使用状态变量,允许使用某些有趣的模式,例如这种模式。

作为一等数据结构,可调用对象可以在任何其他类型的数据可以使用的任何地方使用。

my @regex-check = ( /<alnum>/, /<alpha>/, /<punct>/ );
say @regex-check.map: "33af" ~~ *;
# OUTPUT: «(「3」␤ alnum => 「3」 「a」␤ alpha => 「a」 Nil)␤»

正则表达式实际上是一种可调用对象。

say /regex/.doesCallable ); # OUTPUT: «True␤»

在上面的示例中,我们正在调用存储在数组中的正则表达式,并将它们应用于字符串文字。

可调用对象是通过使用 函数组合运算符 ∘ 组合的。

my $typer = -> $thing { $thing.^name ~ ' → ' ~ $thing };
my $Logger::withtype = $Logger::logs  $typer;
$Logger::withtypePair.new'left''right' ) );
$Logger::withtype( ¾ );
say $Logger::get"2018-05-28" );
# OUTPUT: «(Pair → left right Rat → 0.75)␤» 

我们正在将 $typer 与上面定义的 $Logger::logs 函数组合,得到一个记录对象类型前缀的函数,这对于过滤等操作很有用。实际上,$Logger::withtype 是一个复杂的数据结构,它由两个以串行方式应用的函数组成,但每个组合的可调用对象都可以保持状态,从而创建复杂的变换可调用对象,这是一种类似于面向对象领域中对象组合的设计模式。您必须在每个特定情况下选择最适合您问题的编程风格。

定义和约束数据结构§

Raku 有不同的方法来定义数据结构,但也有很多方法来约束它们,以便您可以为每个问题域创建最合适的数据结构。例如,but 将角色或值混合到值或变量中。

my %not-scalar := %(2 => 3but Associative[IntInt];
say %not-scalar.^name# OUTPUT: «Hash+{Associative[Int, Int]}␤» 
say %not-scalar.of;    # OUTPUT: «Associative[Int, Int]␤» 
%not-scalar{3} = 4;
%not-scalar<thing> = 3;
say %not-scalar;       # OUTPUT: «{2 => 3, 3 => 4, thing => 3}␤» 

在本例中,but 正在混合 Associative[Int, Int] 角色;请注意,我们正在使用绑定,以便变量的类型是定义的类型,而不是由 % 符号强加的类型;这个混合的角色显示在用大括号括起来的 name 中。这到底意味着什么?该角色包含两个方法,ofkeyof;通过混合角色,将调用新的 of(旧的 of 将返回 Mu,它是哈希的默认值类型)。但是,这就是它所做的全部。它并没有真正改变变量的类型,正如您所见,因为我们在接下来的几个语句中使用了任何类型的键和值。

但是,我们可以使用这种类型的混合为变量提供新的功能。

role Lastable {
  method last() {
    self.sort.reverse[0]
  }
}
my %hash-plus := %3 => 334 => 44but Lastable;
say %hash-plus.sort[0]; # OUTPUT: «3 => 33␤» 
say %hash-plus.last;    # OUTPUT: «4 => 44␤» 

Lastable 中,我们使用通用 self 变量来引用混合了该特定角色的任何对象;在本例中,它将包含混合了它的哈希;在其他情况下,它将包含其他内容(并且可能以其他方式工作)。这个角色将为它混合的任何变量提供 last 方法,为普通变量提供新的、可附加的功能。角色甚至可以使用 does 关键字添加到现有变量中

子集 也可以用来约束变量可能持有的可能值;它们是 Raku 对 逐步类型化 的尝试;它不是一个完整的尝试,因为子集在严格意义上不是真正的类型,但它们允许运行时类型检查。它为常规类型添加了类型检查功能,因此它有助于创建一个更丰富的类型系统,允许像此代码中显示的那样的事情。

subset OneOver where (1/$_).Int == 1/$_;
my OneOver $one-fraction = ⅓;
say $one-fraction# OUTPUT: «0.333333␤» 

另一方面,my OneOver $ = ⅔; 将导致类型检查错误。子集可以使用 Whatever,即 *,来引用参数;但这将在每次使用它时实例化为不同的参数,因此如果我们在定义中使用它两次,我们将得到一个错误。在本例中,我们正在使用主题单变量 $_ 来检查实例化。子集可以在 签名 中直接完成,而无需声明它。

无限结构和惰性§

人们可能会认为数据结构中包含的所有数据实际上都“存在”。但情况并非总是如此:在许多情况下,出于效率原因或仅仅因为不可能,数据结构中包含的元素只有在实际需要时才会出现。这种按需计算项目的做法被称为具象化或活化。

# A list containing infinite number of un-reified Fibonacci numbers: 
my @fibonacci = 11* + * … ∞;
 
# We reify 10 of them, looking up the first 10 of them with array index: 
say @fibonacci[^10]; # OUTPUT: «(1 1 2 3 5 8 13 21 34 55)␤» 
 
# We reify 5 more: 10 we already reified on previous line, and we need to 
# reify 5 more to get the 15th element at index 14. Even though we need only 
# the 15th element, the original Seq still has to reify all previous elements: 
say @fibonacci[14]; # OUTPUT: «987␤» 

上面我们对使用序列运算符创建的Seq进行了具象化,但其他数据结构也使用这个概念。例如,一个未具象化的Range仅仅是两个端点。在某些语言中,计算一个巨大范围的总和是一个漫长且占用内存的过程,但 Raku 可以立即计算出来。

say sum 1 .. 9_999_999_999_999# OUTPUT: «49999999999995000000000000␤»

为什么?因为总和可以在具象化范围的情况下计算出来;也就是说,无需找出它包含的所有元素。这就是这个特性存在的原因。你甚至可以使用gathertake来创建你自己的按需具象化对象。

my $seq = gather {
    say "About to make 1st element"take 1;
    say "About to make 2nd element"take 2;
}
say "Let's reify an element!";
say $seq[0];
say "Let's reify more!";
say $seq[1];
say "Both are reified now!";
say $seq[^2];
 
# OUTPUT: 
# Let's reify an element! 
# About to make 1st element 
# 1 
# Let's reify more! 
# About to make 2nd element 
# 2 
# Both are reified now! 
# (1 2)

根据上面的输出,你可以看到gather内部的打印语句只在我们具象化单个元素并查找元素时才执行。还要注意,元素只具象化了一次。当我们在示例的最后一行再次打印相同的元素时,gather内部的消息不再打印。这是因为构造使用了Seq缓存中已经具象化的元素。

请注意,上面我们将gather分配给了一个Scalar容器($ 符号),而不是Positional容器(@ 符号)。原因是@ 符号的变量大多是急切的。这意味着它们会立即具象化分配给它们的东西大多数情况下。它们唯一不这样做的时候是当这些项目被认为是is-lazy的时候,比如我们使用无穷大作为结束点的序列。如果我们将gather分配给一个@ 变量,它内部的say语句就会立即打印出来。

另一种完全具象化列表的方法是调用它的.elems。这就是为什么检查列表是否包含任何项目最好使用.Bool 方法(或者直接使用if @array { … }),因为你不需要具象化所有元素来找出它们中是否存在任何元素。

有时你确实希望在做某事之前完全具象化一个列表。例如,IO::Handle.lines返回一个Seq。以下代码包含一个错误;记住具象化,试着找出它。

my $fh = "/tmp/bar".IO.open;
my $lines = $fh.lines;
close $fh;
say $lines[0];

我们打开一个文件句柄,然后将.lines的返回值分配给一个Scalar变量,因此返回的Seq不会立即具象化。然后我们close文件句柄,并尝试从$lines打印一个元素。

代码中的错误是,当我们在最后一行具象化$lines Seq时,我们已经关闭了文件句柄。当Seq的迭代器试图生成我们请求的项目时,它会导致尝试从已关闭的句柄中读取的错误。因此,为了修复错误,我们可以将它分配给一个@ 符号的变量,或者在关闭句柄之前调用$lines.elems

my $fh = "/tmp/bar".IO.open;
my @lines = $fh.lines;
close $fh;
say @lines[0]; # no problem! 

我们也可以使用任何副作用是具象化的函数,比如上面提到的.elems

my $fh = "/tmp/bar".IO.open;
my $lines = $fh.lines;
say "Read $lines.elems() lines"# reifying before closing handle 
close $fh;
say $lines[0]; # no problem! 

使用eager也会具象化整个序列。

my $fh = "/tmp/bar".IO.open;
my $lines = eager $fh.lines# Uses eager for reification. 
close $fh;
say $lines[0];

自省§

像 Raku 这样的允许自省的语言,在类型系统中附带了一些功能,让开发者可以访问容器和值的元数据。这些元数据可以在程序中使用,根据它们的值执行不同的操作。顾名思义,元数据是从值或容器中通过元类提取的。

my $any-object = "random object";
my $metadata = $any-object.HOW;
say $metadata.^mro;                   # OUTPUT: «((ClassHOW) (Any) (Mu))␤» 
say $metadata.can$metadata"uc" ); # OUTPUT: «(uc uc)␤»

第一个 say 语句展示了元模型类的类层次结构,在本例中是 Metamodel::ClassHOW。它直接继承自 Any,这意味着可以调用该类中的任何方法;它还混合了多个角色,可以提供有关类结构和函数的信息。但是,该特定类的一个方法是 can,我们可以用它来查找对象是否可以使用 uc(大写)方法,显然是可以的。然而,在某些情况下,当角色直接混合到变量中时,这可能并不那么明显。例如,在 上面定义的 %hash-plus 的情况下

say %hash-plus.^can("last"); # OUTPUT: «(last)␤» 

在本例中,我们使用 HOW.method 的语法糖 ^method 来检查你的数据结构是否响应该方法;输出显示了匹配的方法名称,证明我们可以使用它。

另请参阅 这篇关于类内省的文章,了解如何访问类属性和方法,并使用它为类生成测试数据;这篇 Advent Calendar 文章详细介绍了元对象协议