C#/Scala/Python/Ruby/F#でデータ処理はどう違うのか?:特集:人気言語でのデータ処理の比較(2/3 ページ)
最近のプログラミング言語は、データ処理をどう設計・実装しているのか? 5つの言語を比較しながらデータ処理の特徴を説明する。
■イテレータ・パターン
「データ処理の直交化と汎用化」で書いたように、イテレータ・パターンは、「利用側が非常に便利になる半面、実装側が面倒」という問題がある。そこで、通常の制御フローからイテレータを生成するような機能が求められる。
通常の制御フローからイテレータを生成するというのは、「状態の保存と復帰」を行うことになる。「いまどこまでフローを進めたか」「その時点でのローカル変数の値はどうか」などの状態をどこかに保存しておいて、処理をいったん中断する。そして、次に実行されるときに状態を復元して、続きからフローを再開するのである。
●C#
C#のイテレータ・ブロックの場合、上述のような「状態の保存と復帰」を行うようなコードをコンパイラが生成する。例えば、List 9のようなコードを見てみよう。
public static IEnumerable<int> SimpleIterate()
{
yield return 1;
yield return 2;
}
コンパイラは、「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;
}
}
……後略……
}
基本的な考え方は、ループを含む場合でも同じである。ただ、C#では、ループ内へのジャンプ(gotoやswitch)を禁止しているため、一度ループを展開して考える必要がある。例えば、List 11のようなコードを考えてみる。
public static IEnumerable<int> Repeat(int value, int n)
{
for (; n > 0; --n )
{
yield return value;
}
}
これは、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: ;
}
あとは先ほどと同じ要領で「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;
}
}
……後略……
}
●Python
Pythonは、C#のイテレータ・ブロックと非常によく似た、「ジェネレータ(generator: (データの)生成機)」という機能を持っている。例えば、List 11と同じものをPythonで実装すると、List 14のようになる。
def repeat(value, n):
while n > 0:
yield value
n -= 1
「状態の保存と復帰」は、Pythonの実行環境が内部的に行っている(C言語でいうsetjmp/longjmp関数のような挙動)。
●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 16のようなコードを書こうとするとエラーになる。
for x in repeat(10, 5) # エラー
puts x
end
「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 18のような使い方をする。
repeat(10, 5) { |x| puts x }
後半の「{ }」の部分はブロック(匿名関数)となり、repeatメソッドの引数として暗黙的に渡される。
この仕様のためか、RubyのEnumerableモジュールのmap/selectメソッドは、ストリーム的になっていない(一時的な配列を作って返す)。データ処理をストリーム的、かつ、パイプライン的に行うためには、外部イテレータが必要となる。
○Rubyにおける外部イテレータ
Rubyでも、外部イテレータがないわけではなく(バージョン1.8から追加)、「Enumeratorクラス」というものがある。
Enumeratorクラスは、内部的にFiberクラス(setjmp/longjmpを使って内部状態の保存/復帰を行うクラス)を使い、内部イテレータから外部イテレータを生成するものである。
例えば、ストリーム的な処理が可能なようにmap/selectメソッドを再実装すると、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
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()
}
mapメソッドは比較的単純なものの、filterメソッドはまだ少々煩雑である。
●F#
F#には、C#のイテレータ・ブロックや、Pythonのジェネレータに直接相当する機能はないが、後述するシーケンス式(sequence expression)を使うことで同様のことが可能である。
Copyright© Digital Advantage Corp. All Rights Reserved.