迭代
Iterator 和 Iterable 角色
Raku 是一种函数式语言,但在处理复杂的数据结构时,函数需要保持住。特别是,他们需要一个可以应用于所有这些界面的统一接口。此接口由 Iterator 和 Iterable 角色提供。
Iterable
角色相对简单。它为迭代器方法提供了一个存根,该方法实际上是由诸如 for
之类的语句使用的。 for
会在它前面的变量上调用 .iterator
,然后为每个项目运行一次块。其他方法(如数组赋值)将使 Iterable
类以相同的方式运行。
class DNA does Iterable {
has $.chain;
method new ($chain where {
$chain ~~ /^^ <[ACGT]>+ $$ / and
$chain.chars %% 3 } ) {
self.bless( :$chain );
}
method iterator(DNA:D:){ $.chain.comb.rotor(3).iterator }
};
my @longer-chain = DNA.new('ACGTACGTT');
say @longer-chain.perl;
# OUTPUT: «[("A", "C", "G"), ("T", "A", "C"), ("G", "T", "T")]»
say @longer-chain».join("").join("|"); #OUTPUT: «ACG|TAC|GTT»
在这个示例中,它是 Iterable 中示例的扩展,显示了如何调用 .iterator
,只是在将创建的对象分配给位置变量 @long-chain
时调用此方法;这个变量是一个数组,我们在最后一个例子中对它进行操作。
(可能有点容易混淆)Iterator
角色比 Iterable
更复杂一点。首先,它提供了一个常量 IterationEnd
,但它提供了一系列方法,如 .pull-one
,它允许在几个上下文中进行更精细的迭代操作:添加或删除项目,或跳过它们以访问其他项目。实际上,该角色为所有其他方法提供了一个默认实现,因此唯一需要定义的方法就是 pull-one
,其中只提供了一个 stub
。虽然 Iterable
提供了高级变量循环,Iterator
提供了在循环的每次迭代中调用的低级函数。让我们用这个角色扩展前面的例子。
class DNA does Iterable does Iterator {
has $.chain;
has Int $!index = 0;
method new ($chain where {
$chain ~~ /^^ <[ACGT]>+ $$ / and
$chain.chars %% 3 } ) {
self.bless( :$chain );
}
method iterator( ){ self }
method pull-one( --> Mu){
if $!index < $.chain.chars {
my $codon = $.chain.comb.rotor(3)[$!index div 3];
$!index += 3;
return $codon;
} else {
return IterationEnd;
}
}
};
my $a := DNA.new('GAATCC');
.say for $a; # OUTPUT: «(G A A)(T C C)»
我们声明一个 DNA
类,它扮演两个角色,Iterator
和 Iterable
;该类将包含一个字符串,该字符串将被约束为长度为3的倍数且仅由 ACGT 组成。我们先来看看 pull-one
方法。每次发生新的迭代时都会调用这个,因此它必须保持最后一个的状态。 $.index
属性将在调用中保持该状态; pull-one
将检查链的末尾是否已到达,并将返回角色提供的 IterationEnd
常量。实际上,实现这种低级接口简化了 Iterable
接口的实现。现在迭代器将成为对象本身,因为我们可以在其上调用 pull-one
来依次访问每个成员;因此,.iterator
将回归自我;这是可能的,因为对象将同时是 Iterable
和 Iterator
。
这并非总是如此,并且在大多数情况下 .iterator
将必须构建要返回的迭代器类型,例如我们在前面的示例中所做的;但是,此示例显示了构建满足迭代器和可迭代角色的类所需的最少代码。
如何迭代:上下文化和主题变量
for
和其他循环将每次迭代中生成的项放入主题变量 $_
中,或将它们捕获到与块一起声明的变量中。这些变量可以直接在循环中使用,而不需要使用 ^twigil
来声明它们。
使用序列运算符时会发生隐式迭代。
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 中完成。其他 repeat和 while 循环也是可能的。
但是,总的来说,他们是沮丧的。 Raku 是一种功能和并发语言;在 Raku 中编码时,你应该以功能的方式看待循环:逐个处理迭代器产生的项目,即将一个项目提供给一个没有任何辅助效果的块。该功能视图还允许通过hyper或race自动线程方法轻松并行化操作。
如果您对旧的循环感觉更舒服,该语言允许您使用它们。但是,在可能的情况下尝试使用功能和并发迭代构造被认为是更多的p6y。
注意:由于版本6.d循环可以从最后一个语句的值中生成值列表。