らんだむな記憶

blogというものを体験してみようか!的なー

イテレータ

イテレータとかいうやつがある。forのiとかjとかkみたいなやつというか、C++に触れた最初の頃はよく分からんやつだった。とにかく2000年頃は大分キモぃやつだった。

[main.cpp]

#include <iostream>
#include <vector>
#include <boost/foreach.hpp>

int main(void)
{
    // C++11様々な初期化。
    std::vector<const char*> laugh {"ひひひ", "ふふふ", "いーっひっひ"};

    // 古典的な記述。げげげって気分。やる気なくなる。
    std::vector<const char*>::iterator it;
    for (it = laugh.begin(); it != laugh.end(); ++it) {
        std::cout << *it << std::endl;
    }

    // がくがくぶるぶる
    BOOST_FOREACH(const char*& s, laugh) {
         std::cout << s << std::endl;
    }

    // C++11で大分よくなったもんだ
    for (auto la : laugh) {
        std::cout << la << std::endl;
    }

    return 0;
}

$ g++ --std=c++11 -I/opt/boost_1_58_0/include main.cpp

すればどれも同じように振る舞う。とはいえ、C++11のそれはさておき、他のやつはどうにもハードルが高い。

これが、LLの世界になるとイテレータ何それ?の勢いで知らずに使っている。

pythonとか、

for i in range(5):
    print i

ほら、使っちゃった。
自前でイテレータの機能を持たせるなら、

#! /usr/bin/env python
# -*- coding: utf-8 -*-

# イテレータを実装してみる
class Laugh:
    def __init__(self):
        self.__message = [ "ひひひ", "いーっひっひ", "あーっはっは", "うふふふ", "おほほほ", "えへへ" ]
        self.__cur = 0

    def __iter__(self):
        return self

    def next(self):
        if self.__cur + 1 > len(self.__message):
            raise StopIteration
            
        message = self.__message[ self.__cur ]
        self.__cur += 1

        return message

# 素直な書き方
for la in Laugh():
    print la

# 敢えて展開するとこんな感じ?
# どこかで、forはこれの構文糖だ、みたいなのを見たような... うろ覚え
laugh = Laugh()
while True:
    try:
        print next(laugh)
    except StopIteration:
        break

ここで、イテレータを倍速で動かすぜとかしたい場合、2番目のベタベタなのをヒントに、

# イテレータを倍速で動かす
# 明示的にnext呼ぶとこで末端にいくとStopIterationが出ちゃう...
laugh = Laugh()
for l in laugh:
    print next(laugh)

とすると、細かいことはさておき、1個飛ばしで舐めていく。

ついでにジェネレータを使うと、結構へんてこなノリでイテレータが回せる。

# 徐々にイライラしそうなジェネレータ
def Eee():
    i = 0
    while True:
        yield "え" + "ー" * i
        i += 1

eee = Eee()
for i in range(5):
    print next(eee)

なんだか自由だ。

rubyもあっさりイテレータを使っていて、

#! /usr/bin/ruby -Ku

[1, 2, 3].each do |n|
    print "#{n}\n"
end

で、ほら回った。
さて... イテレータを倍速で回したいなぁ...。まず、ちょっとEnumeratorで包んでみよう。

a = [1, 2, 3, 4, 5]

a_enum = Enumerator.new { |y|
    i = 0
    loop {
        raise StopIteration if i >= a.size

        y << a[i]
        i += 1
    }
}

a_enum.each do |n|
    print "#{n}\n"
end

これはいける。
では、お次はpythonでやったように、nextメソッドで1つ飛ばしで...

a = [1, 2, 3, 4, 5]

a_enum = Enumerator.new { |y|
    i = 0
    loop {
        raise StopIteration if i >= a.size

        y << a[i]
        i += 1
    }
}

a_enum.each do |n|
    print "#{n}\n"
    print "#{a_enum.next}\n"
    puts "*****"
end

ん?出力が...

$ ./test.rb
1
1
*****
2
2
*****
3
3
*****
4
4
*****
5
5
*****

なんか、変...。

$ ./test.rb
1
2
*****
3
4
*****
...

を期待してるんだけどなぁ...。
で、そういう時は公式ドキュメントを見る。
class Enumerator (Ruby 1.9.3)

next -> object

「次」のオブジェクトを返します。

現在までの列挙状態に応じて「次」のオブジェクトを返し、列挙状態を1つ分進めます。 列挙が既に最後へ到達している場合は、 StopIteration 例外を発生します。このとき列挙状態は変化しません。 つまりもう一度 next を呼ぶと再び例外が発生します。

nextメソッドによる外部列挙の状態他のイテレータメソッドによる内部列挙には影響を与えません。 ただし、 IO#each_line のようにおおもとの列挙メカニズムが副作用を 伴っている場合には影響があり得ます。

なんじゃこりゃ...。
少なくとも、a_enum.nextはa_enum.eachが触っているのとは別のイテレータを独立に触っているとしか...。
結局のところは、前者は外部イテレータを、後者は内部イテレータというものを触っているらしく、公式ドキュメントは「干渉しないから安心してね♪」と言っているようだ。
まつもと直伝 プログラミングのオキテ 第5回(2) | 日経クロステック(xTECH)にちろっと書いてあるが、プログラミング言語 Ruby | まつもと ゆきひろ, David Flanagan, 卜部 昌平 (監訳), 卜部 昌平 (監訳), 長尾 高弘 |本 | 通販 | Amazonの「5.3.5 外部イテレータ」のあたりに色々書いてある。C++でよく「++it」で次の要素を読み込んだりしていたから、同じノリでいけるんだと思ってたら全然うまくいかなくって悩んだ。

上記の例は強引で意味不明だが、もともとやりたかったのは、特定の条件でイテレータを進めたかったというもので、処理全体をすべて外部イテレータで統一することで実現できた。まぁ...内部イテレータをいじくれるのも変な気はするし仕方ないのかな...。

イテレータの奥の深さを知った1日だった。

本当はこんなことしなくても良かったのだが、pythonの本でジェネレータというものを流し読みしたので、なんとなくジェネレータとかyieldっぽいことがしたいなぁ(しかもpythonではなくrubyで)と思って悪ノリしたらドツボにはまったという悲しいお話。