自社で日報・顧客管理などのデータベースを簡単設計できる「使えるくらうど」シリーズ― アサクラソフト株式会社

制作ブログ

イテレータの話

小原はプログラマなので、たまにはプログラミングの話題をしてみたいと思います。

以下はプログラマ向けの記事となります。(プログラマ以外の方、もうしわけありません。)


はじめに

今回はループを抽象化したもの、「イテレータ」について書きたいと思います。

前提知識としてクラスベースのオブジェクト指向クロージャあたりがあると良いと思います。

できるだけ特定の言語に偏らず共通理解できる部分を書ければと思っています。

 
ループ処理はまとめにくい

プログラミングの歴史は抽象化の歴史です。

「同じ処理を何度も書くのツラい」
→「関数にまとめよう」

「関数化したけど毎回同じデータを引数に渡すのツラい」
→「オブジェクトにしてデータと手続きをまとめてみよう」

すごく雑ですが、こんな具合に手続きやデータに関しての抽象化は進んできたわけです。

ところで、プログラムの構成要素として重要なものに「繰り返し」があります。

ある程度大きなプログラムを作ると、似たような繰り返し処理をあちらこちらで書いていることに気づくことがあります。
(以下サンプルソースコードを胡散臭い日本語プログラム風に書きます)
ここからループ
  複雑な終了判定
  もしも終了ならループを抜ける

  いろんな処理A

ここまで
 
ここからループ
  複雑な終了判定
  もしも終了ならループを抜ける

  いろんな処理B

ここまで

上の例はループ方法は同じだけど実際の処理だけ違ってる場合ですね。

ループ処理の「いろんな処理」の部分の関数化は簡単ですが、ループ制御の部分自体はなかなか再利用しにくい箇所ではないでしょうか。

 
内部イテレータと外部イテレータ

こんな時、クロージャのある言語なら以下のようにできます。
定義 おのおのについて(クロージャ)

  ここからループ
    複雑な終了判定
    もしも終了ならループを抜ける

    クロージャを実行

  ここまで

定義終わり

おのおのについて(いろんな処理A)
おのおのについて(いろんな処理B)

これは内部イテレータと呼ばれるものです。

「いろんな処理」に該当する部分をクロージャとして関数に渡すことで、ループ処理のロジックを再利用できる形にしています。

ループ処理に関係するロジックが一箇所にまとまるのは便利ですね。

内部とくるからには外部イテレータと呼ばれるものもあります。
とりあえず定義は省略して使い方だけを見てみます。
イテレータを取得
ここからループ
  もしも イテレータ.終了? ならループを抜ける
  要素=イテレータ.次の要素()

  いろんな処理A

ここまで

およそこのような使い方になります。

外部イテレータはループの終了条件や要素の取り出し方を知っているオブジェクトです。
(イテレータオブジェクトがどのようなメソッドで次の要素を取得するかや終了を判定するかは言語やライブラリによって異なります。後述しますが、Javascriptの場合は終了フラグと次の要素を同時に返す仕組みになっています。ここでは終了判定メソッドと次の要素を返すメソッドを持っているものとして記します。)

ループ処理の中に元々書いていたループ終了条件の判定(複雑な終了判定と書いていたもの)や要素の取り出しのロジックは、イテレータオブジェクトのクラス定義の方へ移っているため、ループ側では終了判定はイテレータのメソッドを呼ぶだけ、次の要素の取得もイテレータの要素取得メソッドを呼ぶだけ、とシンプルになります。

外部イテレータはクラス定義のできる言語であれば実装可能ですが、言語機能がイテレータをサポートしてくれているとより使いやすくなります。(PHPのforeachやJavascriptのfor ofなど)
 
それぞれのメリット

内部イテレータと外部イテレータを比較すると、

内部イテレータ:
直感的に定義できる(たぶん)

外部イテレータ:
定義はやや煩雑(たぶん)

※個人の感想です

通常のループとクロージャで書ける内部イテレータは書きやすいのですが、外部イテレータはループの1ステップ毎に呼ばれる都合上、状態管理が時として複雑になることがあります。

内部イテレータのほうが使いやすいように思えますが、外部イテレータにも良い点があります。

内部イテレータではループ処理がイテレータ内に書かれているため、イテレータが実行されるとループを最後まで回すことになります。

なにをアタリマエのことを、と思うかもしれませんが、外部イテレータの場合はループが終了したか?次の要素は何か?を返すのみですから、自分自身ではループ処理を持っていません。

このため、たとえば複数の外部イテレータオブジェクトを用意して、それぞれを並列に取り出すことや、処理を中断して後に再開することなど、柔軟な繰り返し処理に向いています。

少し特殊な例ですが、無限個の1を返すイテレータというものを考えてみます。
定義 無限の1
  無限にループ
    結果配列に1を追加
  ここまで
  結果配列を返す
定義終わり

残念ながら、内部イテレータ的な定義だと結果配列を取り出す前に無限ループでハングアップします。(そりゃそうだ)

外部イテレータ的な定義だと
クラス定義 無限の1

  メソッド定義 終了?
    偽を返す
  定義終わり

  メソッド定義 次の要素
    1を返す
  定義終わり

定義終わり

ループの責任はイテレータの外部にあるのでイテレータ自身は問題なく取得できます。(そりゃそうだ)

このループについての情報を持っているイテレータというオブジェクトに対して、あれこれ操作することもできるのが外部イテレータの面白いところです。

たとえば、イテレータをコンストラクタの引数に取り、新しいイテレータを作るイテレータクラスを考えることができます。

ここでは、他のイテレータが最初のn個の要素を返したら終了する、という新しい「n個で終了」イテレータを考えます。

使い方は以下のような感じ
新しいイテレータ = new n個で終了(イテレータ, n)

このイテレータはどんなにたくさんの要素を返すイテレータでも最初のn個しか返さないイテレータに変えてしまいます。

ということは…
無限の1イテレータ = new 無限の1
5個の1イテレータ = new n個で終了(無限の1イテレータ, 5)

そうです。なんの役にも立たないと思われた「無限の1」イテレータを加工して1を5つ返すイテレータに変化させることができました。

もちろん最初から5回で停止するイテレータを書くのは難しいことではありません。
しかし、様々な既存のイテレータの定義そのままで新しいイテレータに変化させるという方法は、プログラムを組む上での強力な部品となると思います。
(ちなみにPHPにはこのようなイテレータを加工するイテレータがいくつか用意されています。)

また要素を無限に持つ数列のようなものを操作可能な対象として扱えるのも面白い点だと思います。

 
ジェネレータ

外部イテレータは柔軟で面白い。でもイテレータクラスの実装は煩雑でわかりにくい。
内部イテレータのように手軽に書けないだろうか…

「それできるよ、ジェネレータで」

そうなんです。

JavascriptはECMAScript 2015(ES6と呼ばれていました。)から、PHPは5.5から、ジェネレータという言語機能を利用できるようになっています。

非常に雑な言い方になりますが、ジェネレータとは、内部イテレータを定義するような記述の仕方で、外部イテレータで作るようなイテレータオブジェクトを実装できる代物、です。

ジェネレータ的に「無限の1」を定義してみます。
定義 無限の1
  無限にループ
    1を渡す
  ここまで
定義終わり

「渡す」ってなんなのさ、と思うことでしょう。実際の言語では yield という構文を利用します。

以下はJavascriptの場合ですが、
function* infinite_one() {
  while(true) {
    yield 1;
  }
}
Javascriptでは function* で始まる定義はジェネレータ関数の定義であり、一般の関数とは異なった振る舞いをします。
いや、これ無限ループしてるだろ、と思うのも無理もない(というかしてる)書き方ですが、このジェネレータ関数を呼び出した時に返ってくるのはイテレータオブジェクトです。ループは実行されません。
var iter = infinite_one();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 1, done: false }

イテレータオブジェクトのnext()を呼ぶたびに、valueとdoneというプロパティをもつオブジェクトが返ってきます。
(valueの値が次の要素、doneの値がループの終了フラグとなっています。これはJavascriptのiteratorプロトコルに従った仕様です。)

イテレータオブジェクトのnext()を呼ぶとジェネレータに定義されたコードが実行されます。
処理がyieldまで到達すると、valueとdoneを持つオブジェクトを返却し、処理を停止します。

プログラム全体が停止するわけではなく、next()を呼んだ側から見れば、普通にメソッドから戻り値が返ってきて処理は継続しています。

yieldはある面でreturnに似ています。
returnで終了した関数を再度呼ぶと、関数の頭から実行されますが、yieldで停止した場合は再度next()が呼ばれた時、処理を先ほどのyieldの次から継続するのです。

もしもyieldに到達しないまま処理がジェネレータの定義の最後まで到達すると、doneにtrueが入ったオブジェクトを返します。

奇妙な動作に見えるかもしれませんが、yieldがあるおかげで、わかりやすい通常のループ構造のほぼそのままで、イテレータオブジェクトを作ることができるわけです。
 
おわりに

ループという制御構造はプログラムの基礎的な要素ですが、抽象化があまり行われない(行われにくい)要素ではないかと個人的に思っています。
イテレータやジェネレータは繰り返し処理を見通し良く抽象化しまた再利用しやすくするのに役立ちます。
何かの折に使ってみていただけると良いかと思います。

 
参考サイト

http://php.net/manual/ja/class.iterator.php
http://php.net/manual/ja/language.generators.overview.php
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_Generators