Iterator 和 Iterable 角色

Raku 是一种函数式语言,但在处理复杂的数据结构时,函数需要保持住。特别是,他们需要一个可以应用于所有这些界面的统一接口。此接口由 IteratorIterable 角色提供。

Iterable 角色相对简单。它为迭代器方法提供了一个存根,该方法实际上是由诸如 for 之类的语句使用的。 for 会在它前面的变量上调用 .iterator,然后为每个项目运行一次块。其他方法(如数组赋值)将使 Iterable 类以相同的方式运行。

  1. class DNA does Iterable {
  2. has $.chain;
  3. method new ($chain where {
  4. $chain ~~ /^^ <[ACGT]>+ $$ / and
  5. $chain.chars %% 3 } ) {
  6. self.bless( :$chain );
  7. }
  8. method iterator(DNA:D:){ $.chain.comb.rotor(3).iterator }
  9. };
  10. my @longer-chain = DNA.new('ACGTACGTT');
  11. say @longer-chain.perl;
  12. # OUTPUT: «[("A", "C", "G"), ("T", "A", "C"), ("G", "T", "T")]
  13. »
  14. say @longer-chain».join("").join("|"); #OUTPUT: «ACG|TAC|GTT
  15. »

在这个示例中,它是 Iterable 中示例的扩展,显示了如何调用 .iterator,只是在将创建的对象分配给位置变量 @long-chain 时调用此方法;这个变量是一个数组,我们在最后一个例子中对它进行操作。

(可能有点容易混淆)Iterator 角色比 Iterable 更复杂一点。首先,它提供了一个常量 IterationEnd,但它提供了一系列方法,如 .pull-one,它允许在几个上下文中进行更精细的迭代操作:添加或删除项目,或跳过它们以访问其他项目。实际上,该角色为所有其他方法提供了一个默认实现,因此唯一需要定义的方法就是 pull-one,其中只提供了一个 stub。虽然 Iterable 提供了高级变量循环,Iterator 提供了在循环的每次迭代中调用的低级函数。让我们用这个角色扩展前面的例子。

  1. class DNA does Iterable does Iterator {
  2. has $.chain;
  3. has Int $!index = 0;
  4. method new ($chain where {
  5. $chain ~~ /^^ <[ACGT]>+ $$ / and
  6. $chain.chars %% 3 } ) {
  7. self.bless( :$chain );
  8. }
  9. method iterator( ){ self }
  10. method pull-one( --> Mu){
  11. if $!index < $.chain.chars {
  12. my $codon = $.chain.comb.rotor(3)[$!index div 3];
  13. $!index += 3;
  14. return $codon;
  15. } else {
  16. return IterationEnd;
  17. }
  18. }
  19. };
  20. my $a := DNA.new('GAATCC');
  21. .say for $a; # OUTPUT: «(G A A)
  22. (T C C)
  23. »

我们声明一个 DNA 类,它扮演两个角色,IteratorIterable;该类将包含一个字符串,该字符串将被约束为长度为3的倍数且仅由 ACGT 组成。我们先来看看 pull-one 方法。每次发生新的迭代时都会调用这个,因此它必须保持最后一个的状态。 $.index 属性将在调用中保持该状态; pull-one 将检查链的末尾是否已到达,并将返回角色提供的 IterationEnd 常量。实际上,实现这种低级接口简化了 Iterable 接口的实现。现在迭代器将成为对象本身,因为我们可以在其上调用 pull-one 来依次访问每个成员;因此,.iterator 将回归自我;这是可能的,因为对象将同时是 IterableIterator

这并非总是如此,并且在大多数情况下 .iterator 将必须构建要返回的迭代器类型,例如我们在前面的示例中所做的;但是,此示例显示了构建满足迭代器和可迭代角色的类所需的最少代码。

如何迭代:上下文化和主题变量

for 和其他循环将每次迭代中生成的项放入主题变量 $ 中,或将它们捕获到与块一起声明的变量中。这些变量可以直接在循环中使用,而不需要使用 ^twigil 来声明它们。

*

使用序列运算符时会发生隐式迭代。

  1. say 1,1,1, { $^a²+2*$^b+$^c } * > 300; # OUTPUT: «(1 1 1 4 7 16 46 127 475)

生成块正在运行一次,而完成序列的条件,在这种情况下,术语大于300,则不满足。这具有运行循环的副作用,但也创建了输出列表。

这可以通过使用 gather/take 块来更系统地完成,这是一种不同类型的迭代构造,而不是在 sink 上下文中运行,每次迭代都返回一个项目。这个 Advent Calendar 教程解释了这种循环的用例;实际上,gather 不是一个循环结构,而是一个语句前缀,它收集 take 生成的项并从中创建一个列表。

*

*

经典循环以及为什么我们不喜欢它们

经典循环,循环变量递增,可以通过 loop 关键字在 Raku 中完成。其他 repeatwhile 循环也是可能的。

但是,总的来说,他们是沮丧的。 Raku 是一种功能和并发语言;在 Raku 中编码时,你应该以功能的方式看待循环:逐个处理迭代器产生的项目,即将一个项目提供给一个没有任何辅助效果的块。该功能视图还允许通过hyperrace自动线程方法轻松并行化操作。

如果您对旧的循环感觉更舒服,该语言允许您使用它们。但是,在可能的情况下尝试使用功能和并发迭代构造被认为是更多的p6y。

注意:由于版本6.d循环可以从最后一个语句的值中生成值列表。

*