使用Iterable和Iterator角色实现可迭代类

5
假设我们有以下类来实现Iterable角色:
class Word-Char does Iterable {
    has @.words;

    method !pairize($item) {
        return $item => $item.chars;
    }

    method iterator( Word-Char:D: ) {
        @!words.map({self!pairize($_)}).rotor(1).iterator
    }
}

在对象构造期间,我可以将对象分配给一个Positional变量,并在该变量上进行迭代:

my @words = Word-Char.new: words => <the sky is blue>;
.say for @words;

输出:

(the => 3)
(sky => 3)
(is  => 2)
(blue => 4)

然而,如果该对象正在传递中,我该如何确保它仍然是可迭代的呢?

my $w = Word-Char.new: words => <the sky is blue>;
sub f( $w ) {
    .say for $w
}
f($w);

输出:

Word-Char.new(words => ["the", "sky", "is", "blue"])

目标:

通过使用Iterable, Iterator或两者都使用,如果可能的话,我希望能够在任何地方迭代实现这些角色的类的实例对象。目前,我知道通过在对象构造期间将实例对象分配给一个Positional变量,我可以获得类提供的可迭代项,但这不是我想要的。相反,我想传递对象本身,并在任何时候需要的地方迭代它。


2
根据我的回答,在使用for循环时会调用iterator方法。 - raiph
4个回答

6
处理扮演迭代器角色的标量值时,实现你尝试的操作最简单的方法是告诉perl6你的标量值是可迭代的。你可以在结尾处加上[]来完成这个操作。那么你的示例代码看起来像这样:
my $w = Word-Char.new: words => <the sky is blue>;
.say for $w[]

另外一件事...

你的迭代代码有一个bug,它在返回IterationEnd之前没有重置。快速修复如下所示:

class Word-Char does Iterable does Iterator {
    has @.words;
    has Int $!index = 0;

    method !pairize($item) {
        return $item => $item.chars;
    }

    method iterator() {self}
    method pull-one( --> Mu ) {
        if $!index < @!words.elems {
            my $item = @!words[$!index];
            $!index += 1;
            return self!pairize($item);
        }
        else {
            $!index = 0;
            return IterationEnd;
        }
    }
}

然而,这意味着您必须将所有迭代逻辑(及其属性)与主类一起保留。另一种方法是使用匿名类,而不是使用self

class Word-Char does Iterable {
    has @.words;

    method !pairize($item) {
        return $item => $item.chars;
    }

    method iterator() {
        my @words = @!words;

        class :: does Iterator {
            has $.index is rw = 0;

            method pull-one {
              return IterationEnd if $!index >= @words.elems;
              @words[$!index++];
            }
        }.new;
    } 
}

上述方法的优点在于,你可以保持迭代逻辑更加清晰且独立于对象的其他部分。你也不需要担心重置状态的问题。

欢迎来到 SO/SE Xliff!我认为这是我第一次看到匿名类的一个非常好的用例。 - user0721090601
使用匿名类的解决方案无法编译。错误在于.pull-one()方法中出现了Attribute $!index not declared in class Word-Char - Fernando Santagata
@FernandoSantagata - 哎呀!你说得对。我已经更新了代码。 - Xliff

4

好的,不清楚你想要实现什么,但让我们尝试一下。 第二个示例的主要问题是您将定位(使用w)更改为标量。只需再次使用@w即可解决问题。

my @w = Word-Char.new: words => <the sky is blue>;
sub f( @w ) {
    .say for @w
}
f(@w);

这将完全以相同的方式工作,因为@w仍然是Positional,因此可迭代。当您调用$w时,标量只返回其唯一项,即对象,并打印出来。如果您想在此对象上使用标量符号并同时迭代它,则需要将其也变成Iterator

我可能误解了迭代的含义。我以为我是将该属性赋予类,因此其任何实例都可以在任何地方进行迭代,而不仅仅是在对象构造期间。通过在对象构造期间将对象分配给“Positional”,我可以获取对象的可迭代项,但我希望能够传递对象。这就是为什么我问:“如果对象正在传递怎么办?我如何确保它仍然是可迭代的?” - uzluisf
我原本想传递对象,然后将其分配给Positional以获取其可迭代项,但似乎行不通。就像这样 sub f($w) { my @items = $w; .say for @items; }。我会尝试使用Iterator角色并回报结果。 - uzluisf
@uzlxxxx,有Iterable和Iterator这两个概念让人感到有点混淆;而且Iterable还有一个iterator方法。不过文档中的示例应该相当清晰了。使其成为Iterable的是它作为Positional的事实。如果它不再是Positional,那么它就无法扮演Iterable的角色。 - jjmerelo
你说得对。我认为这是文档中可能需要进一步改进的地方。它之所以成为“可迭代对象”,是因为它是一个“位置对象”。难道说“它之所以成为‘可迭代对象’,是因为它被分配给了一个‘位置对象’”不更准确吗?在我的看法中,当对象被分配给一个“位置对象”后,我们失去了对象本身,但保留了其可迭代元素。感谢您的澄清。 - uzluisf
@uzlxxxx 是的,有点类似,但可迭代的是 Positional 而不是对象本身... - jjmerelo

2
在 #perl6 上,jnthn 提供了几种方法。其中一些的行为不如我所预期。
根据 jjmerelo 的建议,我更新了类如下:
class Word-Char does Iterable does Iterator {
    has @.words;
    has Int $!index = 0;

    method !pairize($item) {
        return $item => $item.chars;
    }

    method iterator() {self}
    method pull-one( --> Mu ) {
        if $!index < @!words.elems {
            my $item = @!words[$!index];
            $!index += 1;
            return self!pairize($item);
        }
        else {
            return IterationEnd;
        }
    }
}

1. 将对象绑定到位置参数

# Binding to a Positional
my @w01 := Word-Char.new: words => <the sky is blue>;

这会产生以下错误:
Type check failed in binding; expected Positional but got Word-Char...

2. 在迭代点使用|

my $w = Word-Char.new: words => <the sky is blue>;

for |$w {
    .say
}

=begin comment
Word-Char.new(words => ["the", "sky", "is", "blue"])
=end comment

“|”对似乎保持其标量特性的对象没有影响,因此“for”不会迭代它。
3. 使用无标记变量。
my \w = Word-Char.new: words => <the sky is blue>;

for w {
    .say
}

=begin comment
he => 3
sky => 3
is => 2
blue => 4
=end comment

到目前为止,这是最干净的方法,它做了我所期望的事情。
4. 不要使类可迭代,而是添加一个返回可迭代对象的方法。
事实上,这是我的第一种方法,但我觉得它不太好。无论如何,为了使其工作,我们需要更新我们的类并添加一个返回可迭代对象的方法。我选择的方法名是LOOP-OVER,只是为了使它与其他所有内容区分开来。
class Word-Char {
    has @.words;

    method !pairize($item) {
        return $item => $item.chars;
    }

    method LOOP-OVER {
        gather for @!words -> $word {
            take self!pairize($word)
        }
    }
}

my $w = Word-Char.new: words => <the sky is blue>;

for $w.LOOP-OVER {
    .say
}

=begin comment
he => 3
sky => 3
is => 2
blue => 4
=end comment

但是,如果我们依赖于多个类进行迭代处理,我们该如何确保它们实现了相同的方法?最直接的方法是组合一个角色(例如Iterationable),该角色实现一个存根LOOP-OVER方法,在这种情况下。
role Iterationable {
    method LOOP-OVER { ... }
}

class Word-Char does Iterationable {
    has @.words;

    method !pairize($item) {
        return $item => $item.chars;
    }

    method LOOP-OVER {
        gather for @!words -> $word {
            take self!pairize($word)
        }
    }
}

class Names does Iterationable {
    has @.names;

    method LOOP-OVER {
        gather for @!names -> $name {
            take $name.split(/\s+/)».tc.join(' ')
        }
    }
}

class NotIterable {
    has @.items
}

my @objs =
    Word-Char.new(words => <the sky is blue>), 
    Names.new(names => ['Jose arat', 'elva  delorean', 'alphonse romer']),
    NotIterable.new(items => [5, 'five', 'cinco', 'cinq'])
;

for @objs -> $obj {
    if $obj.can('LOOP-OVER') {
        put "» From {$obj.^name}: ";
        for $obj.LOOP-OVER {
            .say
        }
    }
    else {
        put "» From {$obj.^name}: Cannot iterate over it";
    }
}

=begin comment
» From Word-Char: 
the => 3
sky => 3
is => 2
blue => 4
» From Names: 
Jose Arat
Elva Delorean
Alphonse Romer
» From NotIterable: Cannot iterate over it
=end comment

正如jnthn所述,要使用哪种方法(至少是可行的方法)很大程度上取决于手头的问题。

关于#1,让一个类实现Positional其实相当容易,如果你想要将其用作这样的话,我建议你这样做。你只需要实现3个方法:elemsAT-POSEXISTS-POS。由于你有一个内部数组,你可以将事情委托给它:method elems { @words.elems }; method AT-POS($i) { @words[$i] }; method EXISTS-POS($i) { @words[$i]:exists },这将使你的用户的生活变得更加轻松(即使你最终不会将其变成可迭代对象,它也可能会起作用,但我还没有测试过)。 - user0721090601

1
另一个(有些杂乱)解决方案是:


class Word-Char does Iterator {
  has @.words;
  has Int $.index is rw = 0;

  method pull-one() {
    LEAVE { $!index++ }
    return $!index < @!words.elems
           ?? (@!words[$!index] => @!words[$!index].chars)
           !! IterationEnd;
  }
}

my $w = Word-Char.new: words => <the sky is blue>;
my $seq = Seq.new($w).cache;
sub f( $w ) {
  .say for $w[]
}
f($seq);
$w.index = 0;
f($seq);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接