C#/Scala/Python/Ruby/F#でデータ処理はどう違うのか?特集:人気言語でのデータ処理の比較(2/3 ページ)

» 2011年07月22日 00時00分 公開
[岩永信之,]

■イテレータ・パターン

 「データ処理の直交化と汎用化」で書いたように、イテレータ・パターンは、「利用側が非常に便利になる半面、実装側が面倒」という問題がある。そこで、通常の制御フローからイテレータを生成するような機能が求められる。

 通常の制御フローからイテレータを生成するというのは、「状態の保存と復帰」を行うことになる。「いまどこまでフローを進めたか」「その時点でのローカル変数の値はどうか」などの状態をどこかに保存しておいて、処理をいったん中断する。そして、次に実行されるときに状態を復元して、続きからフローを再開するのである。

C#

 C#のイテレータ・ブロックの場合、上述のような「状態の保存と復帰」を行うようなコードをコンパイラが生成する。例えば、List 9のようなコードを見てみよう。

public static IEnumerable<int> SimpleIterate()
{
  yield return 1;
  yield return 2;
}

List 9 単純なイテレータ・ブロックの例

 コンパイラは、「yield return」の部分に、状態の保存コードや、復帰用のラベルを埋め込み、全体をswitchステートメントで覆うようなコードを生成する。List 9のコードからは、List 10に示すようなイテレータが生成される。

class SimpleIterateEnumerator : IEnumerator<int>
{
  public int Current { get; private set; }
 
  private int state = 0;

  public bool MoveNext()
  {
    switch(state)
    {
      case 0:

      // yield return 1;
      Current = 1; // 戻り値の設定
      state = 1;   // 状態の保存
      return true; // いったん処理終了
      case 1:    // 次回の復帰用のラベル

      // yield return 2;
      Current = 2;
      state = 2;
      return true;
      case 2:

      // 終了状態
      default:
      return false;
    }
  }
  ……後略……
}

List 10 List 9から生成されるイテレータ

 基本的な考え方は、ループを含む場合でも同じである。ただ、C#では、ループ内へのジャンプ(gotoswitch)を禁止しているため、一度ループを展開して考える必要がある。例えば、List 11のようなコードを考えてみる。

public static IEnumerable<int> Repeat(int value, int n)
{
  for (; n > 0; --n )
  {
    yield return value;
  }
}

List 11 ループを含むイテレータ・ブロックの例

 これは、ifステートメントとgotoステートメントだけを使って、List 12のように書き直せる。

public static IEnumerable<int> Repeat_(int value, int n)
{
BEGIN_LOOP:
  if (!(n > 0)) goto END_LOOP;
  yield return value;
  --n;
  goto BEGIN_LOOP;
END_LOOP: ;
}

List 12 List 11をifステートメントとgotoステートメントを使って書き直した結果

 あとは先ほどと同じ要領で「yield return」を置き換えることで、List 13に示すようなイテレータを得る。

class RepeatEnumerator : IEnumerator<int>
{
  public int Current { get; private set; }
 
  private int state = 0;
 
  // ローカル変数やパラメータに相当するものをフィールドに
  internal int value;
  internal int n;
 
  public bool MoveNext()
  {
    switch (state)
    {
      case 0:
      BEGIN_LOOP:
        if (!(n > 0)) goto END_LOOP;
 
        // yield return value;
        Current = value;
        state = 1;
        return true;
        case 1:
 
        --n;
        goto BEGIN_LOOP;
 
      END_LOOP:
        state = 2;
        goto default;
 
      default:
        return false;
    }
  }
  ……後略……
}

List 13 List 12から生成されるイテレータ

Python

 Pythonは、C#のイテレータ・ブロックと非常によく似た、「ジェネレータ(generator: (データの)生成機)」という機能を持っている。例えば、List 11と同じものをPythonで実装すると、List 14のようになる。

def repeat(value, n):
  while n > 0:
    yield value
    n -= 1

List 14 Pythonのジェネレータの例

 「状態の保存と復帰」は、Pythonの実行環境が内部的に行っている(C言語でいうsetjmplongjmp関数のような挙動)。

Ruby

 Rubyにも列挙用のyieldキーワードがあるが、ほかの言語とは挙動が異なり、残念ながら、ストリーム的なデータ処理に直接使えない。Rubyの「yield」は、いわゆる「内部イテレータ」となる(これと区別するために、これまで説明してきたようなイテレータを「外部イテレータ」と呼ぶ)。

 例えば、List 11やList 14と同じつもりで、List 15のようなコードを書いたとしよう。これは、ほかの言語と同じ挙動にはならない。

def repeat(value, n)
  for i in 1..n
    yield value
  end
end

List 15 Rubyにおけるyield(内部イテレータ)の例

 ほかの言語と同じつもりで、List 16のようなコードを書こうとするとエラーになる。

for x in repeat(10, 5) # エラー
  puts x
end

List 16 Rubyの内部イテレータの間違った使い方の例
「repeatメソッドに対してブロックを渡さなければならない」という旨のエラーが出る。

 Rubyの場合、yieldキーワードを使ったメソッドは、暗黙に「ブロック」(ほかの言語でいう「匿名関数」)を引数に取り、「yield」はそのブロックの呼び出しに変換される。C#でいうと、List 17のようなコードに相当する。

static void Repeat(int value, int n, Action<int> yielder)
{
  foreach (var i in Enumerable.Range(0, n))
  {
    yielder(value);
  }
}

List 17 Rubyの内部イテレータをC#で表現

 従って、正しくは、List 18のような使い方をする。

repeat(10, 5) { |x| puts x }

List 18 Rubyの内部イテレータの使い方の例
後半の「{ }」の部分はブロック(匿名関数)となり、repeatメソッドの引数として暗黙的に渡される。

 この仕様のためか、RubyのEnumerableモジュールのmapselectメソッドは、ストリーム的になっていない(一時的な配列を作って返す)。データ処理をストリーム的、かつ、パイプライン的に行うためには、外部イテレータが必要となる。

Rubyにおける外部イテレータ

 Rubyでも、外部イテレータがないわけではなく(バージョン1.8から追加)、「Enumeratorクラス」というものがある。

 Enumeratorクラスは、内部的にFiberクラス(setjmplongjmpを使って内部状態の保存/復帰を行うクラス)を使い、内部イテレータから外部イテレータを生成するものである。

 例えば、ストリーム的な処理が可能なようにmapselectメソッドを再実装すると、List 19のようになる。

class Enumerator
  def lazy_map(&blk)
    Enumerator.new do |y|
      each do |e|
        y << blk[e]
      end
    end
  end

  def lazy_select(&blk)
    Enumerator.new do |y|
      each do |e|
        y << e if blk[e]
      end
    end
  end
end

List 19 一時配列を作らないmap/selectメソッドの例

 C#やScalaなど、ほかの言語とは、「〜able」と「〜ator」の関係性が異なる点にも注意が必要である。

Scala

 残念ながら、ScalaにはC#のイテレータ・ブロックや、Pythonのジェネレータに当たる機能はない。

 ただし、匿名クラスがあるため、イテレータの実装は、多少楽である。List 3のコードで「後述」とした部分の中身を見てみよう。List 20に示すとおりである。

def map[B](f: A => B): Iterator[B] = new Iterator[B] {
  def hasNext = self.hasNext
  def next() = f(self.next())
}

def filter(p: A => Boolean): Iterator[A] = new Iterator[A] {
  private var hd: A = _
  private var hdDefined: Boolean = false

  def hasNext: Boolean = hdDefined || {
    do {
      if (!self.hasNext) return false
      hd = self.next()
    } while (!p(hd))
    hdDefined = true
    true
  }

  def next() = if (hasNext) { hdDefined = false; hd } else empty.next()
}

List 20 ScalaのIteratorモジュールのmapメソッドとfilterメソッドの内部

 mapメソッドは比較的単純なものの、filterメソッドはまだ少々煩雑である。

F#

 F#には、C#のイテレータ・ブロックや、Pythonのジェネレータに直接相当する機能はないが、後述するシーケンス式(sequence expression)を使うことで同様のことが可能である。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。