C#/Scala/Python/Ruby/F#でデータ処理はどう違うのか?:特集:人気言語でのデータ処理の比較(3/3 ページ)
最近のプログラミング言語は、データ処理をどう設計・実装しているのか? 5つの言語を比較しながらデータ処理の特徴を説明する。
■二重ループ相当の列挙
Figure 2のようなパイプライン型のデータ処理で困るのが、二重(以上の多重)ループ相当のデータ列挙が書きづらいことである。例えば、イテレータ・ブロックを使うなら、List 21のようなものである。
// 単純な直積集合を得る
static IEnumerable<Tuple<T, U>> Product<T, U>(
IEnumerable<T> a, IEnumerable<U> b)
{
foreach (var x in a)
foreach (var y in b)
yield return Tuple.Create(x, y);
}
class Parent<T>
{
public IEnumerable<T> Children { get; set; }
}
// 階層的な列挙(子要素の展開)
static IEnumerable<T> Flatten<T>(IEnumerable<Parent<T>> a)
{
foreach (var x in a)
foreach (var y in x.Children)
yield return y;
}
「これを、メソッドではなく、式として書くにはどうすればいいか」という話である。慣れの問題もあるものの、二重ループを書くのと同じ感覚で書ける式が欲しい。
* 残念ながら、Rubyは二重ループ的な列挙を簡素化するような式は持っていない。
●C#
C#では、クエリ式がその役割を担う。いわゆるO/Rマッパー(object/relational mapper)としても使うため構文がSQL風*2だが、内部的に行っていることはScalaのfor式などと非常に近い。
*2 ただし、SQLとは異なり、select句が末尾に来る。統合開発環境による補完機能との親和性を考えた結果である。
例として、二重ループ的なクエリ式(from句が二重)をList 22に示す。
var q =
from x in Enumerable.Range(1, 5)
from y in Enumerable.Range(1, x)
where x + y < 8
select x * y;
クエリ式は、コンパイラによって、WhereやSelectなどのメソッドに展開される。二重ループ的なもの、つまり、この例のように2つ以上のfrom句がある場合、2つ目以降のfrom句はSelectManyメソッドに展開される。List 22のクエリ式の場合、展開結果はList 23のようになる*3。
var q = Enumerable.Range(1, 5)
.SelectMany(x => Enumerable.Range(1, x), (x, y) => new { x, y })
.Where(_ => _.x + _.y < 8)
.Select(_ => _.x * _.y);
*3 この例のように、SelectManyメソッドを使う場合、元のクエリ式では不要だった一時変数(_)が必要になることがある。このような一時変数を避けられるのもクエリ式の利点の1つである。
C#のクエリ式のいいところは、1つの句がほぼ1つのメソッド呼び出しに翻訳され、パイプライン的な処理になることである。上半分はList 1の再掲になるが、List 24に示す2つの式を比べてみてほしい。
var クエリ式版 =
from c in 顧客一覧
where c.性別 == "女"
group c.年齢 by c.年齢 into g
orderby g.Key
select new { 年齢 = g.Key, 数 = g.Count() };
var メソッド版 = 顧客一覧
.Where(c => c.性別 == "女")
.GroupBy(c => c.年齢, c => c.年齢)
.OrderBy(g => g.Key)
.Select(g => new { 年齢 = g.Key, 数 = g.Count() });
●Scala
Scalaには「for式(for expression)」というものがある。forがステートメントではなく、式なのである。「for()」の直後に「yield」と書くことで、ストリーム的なデータ列を生成することもできる。
例えば、List 22と同様のものをScalaで書くと、List 25のようになる。
val q = for (
x <- 1 to 5;
y <- 1 to x
if x + y < 8)
yield x * y
Scalaのfor式は、C#のクエリ式同様、メソッド呼び出しへの展開となる。ただし、パイプライン型ではなく、List 26に示すような入れ子型のメソッド呼び出しとなる。
val q = (1 to 5).flatMap(x => {
(1 to x).filter(y => x + y < 8)
.map(y => x * y)
})
一重の場合、もしくは二重以上の最後のループは、mapメソッドに展開される。二重以上は、flatMapメソッドに展開される。また、「if」を書くとfilterメソッドとなる。
●Python
Pythonには「ジェネレータ式(generator expressions)」というものがある。Pythonには昔から、「リスト内包(list comprehensions)」というリスト生成のための構文があったが、Python 2.4からは、これと同じような記法で、イテレータ(ストリーム的な、遅延評価リスト)が生成できるようになった(これを「ジェネレータ式」という)。
例えば、List 22と同様のものをPythonで書くと、List 27のようになる。
q = (x * y
for x in range(1, 6)
for y in range(1, x + 1)
if x + y < 8)
最終的に取得したい値(この例の場合、「x * y」)が先頭に来る点に注意。
ジェネレータ式の内部挙動は実にシンプルで、前述のジェネレータ(=yieldステートメントを持つメソッド)を自動生成するだけである。
●F#
F#では、「シーケンス式(sequence expressions)」という機能を使ってイテレータを生成できる。
例えば、List 22と同様のものをF#で書くと、List 28のようになる。
let q = seq {
for x in 1..5 do
for y in 1..x do
if x + y < 8 then
yield x * y }
これもやはり、メソッド呼び出しに展開される。List 28の展開結果はList 29のようになる。
let p = Seq.collect (fun x ->
Seq.collect (fun y ->
if x + y < 8 then
Seq.singleton (x * y)
else
Seq.empty
) (seq{1..x})) (seq{1..5})
この展開結果は少々複雑ではあるが、その分、強力な点もある。F#のシーケンス式では、「{}」内に一通りの制御フローが書けるのだ。「for」や「if」に限らず、「let」(変数束縛)、「use」(リソース利用。C#でいうusingステートメント)、「match」(ほかの言語でいうswitchステートメント)、「try」(例外処理)なども書ける。
イテレータ・パターンの説明の際、「F#には、C#のイテレータ・ブロックに直接相当するものはない」と書いたが、そもそもF#には必要ないのである。シーケンス式を使えば、式として一通りの制御フローが書けるため、わざわざメソッドにする必要がないのである。
【コラム】F#のコンピューテーション式について
F#には、シーケンス式を一般化した「コンピューテーション式(computation expressions)」という機能もある。上記の展開結果に出て来るSeqモジュールのようなものを自作することで、F#の制御フローを自作のメソッドで置き換えられる。
その代表例は「非同期ワークフロー」で、同期処理とほとんど同じ書き方の制御フローで非同期処理が行えるというものである。
最後に、データ処理のポイントを再度挙げると、以下のとおりである。
- パイプライン型のデータ処理
- ストリーム処理
- 二重ループ相当のデータ列取得が少し厄介
今回見てきたように、実装方法はさまざまであるが、多くのプログラミング言語がこれらのポイントを押さえようとしている。逆に、同じ目標であっても、言語ごとの個性が見事に表れているといえる。
Copyright© Digital Advantage Corp. All Rights Reserved.