The Iterator and Iterable roles§

Raku 是一种函数式语言,但函数在处理复杂数据结构时需要一些东西来保存。特别是,它们需要一个统一的接口,可以以相同的方式应用于所有数据结构。其中一种接口由 IteratorIterable 角色提供。

Iterable 角色相对简单。它提供了一个 iterator 方法的占位符,该方法是 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.raku;
# OUTPUT: «[("A", "C", "G"), ("T", "A", "C"), ("G", "T", "T")]␤» 
 
say  @longer-chain».join("").join("|"); # OUTPUT: «ACG|TAC|GTT␤» 

在这个例子中,它是 Iterable 中示例的扩展,展示了 for 如何调用 .iterator,只有当创建的对象被分配给 Positional 变量 @longer-chain 时,iterator 方法才会在适当的上下文中被调用;这个变量是一个 Array,我们在最后一个例子中像这样操作它。

Iterator 角色(可能有点令人困惑的名字)比 Iterable 更复杂。首先,它提供了一个常量 IterationEnd。然后,它还提供了一系列方法,例如 .pull-one,它允许在多个上下文中进行更精细的迭代操作:添加或删除项目,或跳过它们以访问其他项目。事实上,该角色为所有其他方法提供了默认实现,因此唯一需要定义的方法正是 pull-one,该方法只由该角色提供了一个占位符。虽然 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 类,它执行两个角色,IteratorIterable;该类将包含一个字符串,该字符串将被限制为长度为 3 的倍数,并且仅由 ACGT 组成。

让我们看一下 pull-one 方法。每次发生新的迭代时都会调用它,因此它必须保持上次迭代的状态。$.index 属性将在调用之间保存该状态;pull-one 将检查链的末尾是否已到达,并将返回角色提供的 IterationEnd 常量。事实上,实现这个底层接口简化了 Iterable 接口的实现。现在迭代器将是对象本身,因为我们可以调用它的 pull-one 来依次访问每个成员;因此 .iterator 将只返回 self;这是可能的,因为该对象将同时是 IterableIterator

这并不总是必须的,在大多数情况下,.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 生成的项目并从中创建列表的语句前缀。

Classic 循环以及我们不喜欢它们的原因§

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

但是,一般来说,它们是不鼓励的。Raku 是一种函数式和并发语言;在 Raku 中编码时,你应该以函数式的方式看待循环:逐个处理迭代器产生的项目,也就是说,将一个项目馈送到一个块中,而没有任何副作用。这种函数式视图也允许通过 hyperrace 自动线程方法轻松地并行化操作。

如果您更习惯使用传统的循环,该语言允许您使用它们。但是,在 Raku 中,尽可能使用函数式和并发迭代结构被认为是最佳实践。

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