数据流

标量结构

某些类没有任何内部结构, 访问它们的一部分必须使用特定的方法。数字,字符串和其他一些整体类包含在该类中。他们使用 $ sigil,虽然复杂的数据结构也可以使用它。

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

有一个 Scalar 类,它在内部用于为使用 $ sigil 声明的变量赋值。

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

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

(1, 2, 3, $(4, 5))[3].VAR.^name.say; # OUTPUT: «Scalar␤» 

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

(1, 2, 3, $(4, 5))[3][0].say; # OUTPUT: «4␤» 

有一个有趣的副作用,或者可能是故意的特性,是标量化保留了复杂结构的同一性。

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

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

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

在这种情况下,$list 使用的是 Scalar sigil,因此将是一个 Scalar。任何具有相同值的标量都将完全相同,如打印指针时所显示的那样。

复杂数据结构

根据你如何访问其第一级元素, 复杂的数据结构分为两大类: Positional, 或类列表结构 Associative, 或类键值对儿结构。 通常, 复杂数据结构, 包括对象, 会是两者的组合, 使对象属性变为键值对儿。而所有的对象都是 Mu 的子类, 通常复杂对象是 Any 子类的实例。 虽然理论上可以在不这样做的情况下混合使用 “Positional” 或 “Associative”,但是大多数适用于复杂数据结构的方法都是在 “Any” 中实现的。

操纵这些复杂的数据结构是一项挑战,但 Raku 提供了一些可用于它们身上的函数:deepmapduckmap。而前者会按顺序切换每个元素,无论块传递的是什么。

say [[1, 2, [3, 4]],[[5, 6, [7, 8]]]].deepmap( *.elems );
# OUTPUT: «[[1 1 [1 1]] [1 1 [1 1]]]␤» 

这返回 1 因为它进入更深层次并将 elems 应用于元素,deepmap 可以执行更复杂的操作:

say [[1, 2, [3, 4]], [[5, 6, [7, 8]]]].duckmap:
   -> $array where .elems == 2 { $array.elems };
# OUTPUT: «[[1 2 2] [5 6 2]]␤» 

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

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

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

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

PositionalAssociative 可以相互转换。

say %( first => [1, 2], 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 的。 从中生成 iterator 将允许程序逐个访问结构的第一级:

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

'א'..'ס' 是一个 Range, 一个复杂数据结构, 把 for 放在它前面会迭代直到列表元素耗尽。你可以通过重写 iterator 方法(来自角色 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 角色,这是基础角色四重奏中的第 4 个元素。 Callable& sigil 一起使用,尽管在大多数情况下,为了简单起见,它被省略了; 在 Callables 的情况下,总是允许消除这种 sigil。

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.new( now ) } = $event;
  } else {
    %store.keys.grep( /$key/ )
  }
}
$logger( "Stuff" );
$logger( "More stuff" );
say $logger( Nil, "2018-05-28" ); # OUTPUT: «(Stuff More stuff)␤» 

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

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

克隆将重置状态变量; 代替克隆,我们可以创建改变 API 的 façades。例如,无需使用 Nil 作为第一个参数来检索特定日期的日志:

my $gets-logs = $logger.assuming( Nil, * );
$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.assuming( Nil, * );
$Logger::logs( <an array> );
$Logger::logs( %(key => 42) );
say $Logger::get( "2018-05-28" );

尽管 :: 通常用于调用类方法,但它实际上是变量名称的有效部分。在这种情况下,我们通常使用它们来简单地指示 $Logger::logs$Logger::get 实际上是在调用 $Logger,我们已经大写使用了类似于类的外观。本教程的重点是,使用函数作为一等公民,以及使用状态变量,允许使用某些有趣的设计模式,例如这个。

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

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

正则表达式实际上是一种 callable 类型:

say /regex/.does( Callable ); # OUTPUT: «True␤» 

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

使用函数组合运算符∘组成 Callables:

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

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

Defining and constraining data structures

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

my %not-scalar := %(2 => 3) but Associative[Int, Int];
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] 角色中; 请注意我们正在使用绑定,以便变量的类型是所定义的,而不是 % sigil 强加的类型; 这个混合角色显示在用花括号包围的 name 中。 它的真实意义是什么? 该角色包括两个方法,ofkeyof; 通过混合角色,将调用新的 of(旧的 of 将返回 Mu,这是 Hashes 的默认值类型)。 然而,就是这样。 它并没有真正改变变量的类型,因为你可以看到,因为我们在接下来的几个语句中使用了任何类型的键和值。

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

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

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

Subsets 也可用于约束变量可能包含的值; 他们是 Raku 尝试渐进类型; 它不是一个完整的尝试,因为子集在严格意义上不是真正的类型,但它们允许运行时类型检查。它为常规类型添加了类型检查功能,因此它有助于创建更丰富的类型系统,允许类似以下代码中显示的内容:

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

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

无限结构和惰性

可以假设数据结构中包含的所有数据实际上都是那里。情况不一定如此:在许多情况下,出于效率原因或仅仅因为不可能,数据结构中包含的元素只有在实际需要时才会跳存。这种按需对项的计算称为 reification.。

# A list containing infinite number of un-reified Fibonacci numbers: 
my @fibonacci = 1, 1, * + * … ∞;
 
# 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␤» 

为什么? 因为不用具体化范围总就可以计算总和; 也就是说,不用弄清楚它包含的所有元素。这就是此功能存在的原因。你甚至可以使用 gather and take 按需具体化:

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 里面的 print 语句只有当我们在查找元素时确定各个元素时才会执行。另请注意,这些元素只被修改了一次。当我们在示例的最后一行再次打印相同的元素时,就不再打印 gather 内的消息。这是因为该构造使用了来自 Seq 缓存的已经确定的元素。

请注意,上面我们将 gather 赋值给 Scalar 容器( $ sigil),而不是 Positional (@ sigil)。原因是 @-sigiled 变量主要是eager。这意味着他们大部分时间立即明确分配给他们的东西。他们唯一没有这样做的时候知道这些项是 is-lazy,就像我们用无穷大生成序列作为终点一样。如果我们将 gather 赋值给 @-variable,那里面的 say 语句就会被立即打印出来。

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

有些时候你确实需要在做某事之前完全具体化列表。例如,IO::Handle.lines 返回 Seq。以下代码包含错误; 记住具体化,试着发现它:

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

我们打开 filehandle,然后分配 .linesScalar 变量,因此返回的 Seq 不会立刻被具体化。 然后我们 close 文件句柄,并尝试从 $lines 打印一个元素。

代码中的错误是在我们在最后一行具体化 $lines Seq 时,我们已经关闭文件句柄。 当 Seq 的 iterator 试图生成我们请求的项时,会导致尝试从关闭的句柄中读取的错误。 因此,要修复错误,我们可以在关闭句柄之前分配给 @-sigiled 变量或在 $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 文章详细描述了元对象协议