Dart における高階関数を使ったデータ処理

最近ではコンテナークラスなどのデータを高階関数を使って処理する手法が広がってきています。
今回はその高階関数を使ったデータ処理を Dart で行う方法について紹介します。

高階関数を使ったデータ処理

まず、高階関数を使ったデータ処理について説明します。

プログラミング言語の傾向

高階関数を使ったデータ処理は Lisp や関数型言語でよく使われてきた方法で、 関数型プログラミングには欠かせないものです。

しかし、関数型言語でなければ使えないというものではありません。
高階関数を使ったデータ処理はコードを短くかつわかりやすく記述することができます。 そのため、 Ruby, C#(LINQ) を始めとした多くの言語で取り入れられてきました。 最近では、 Java や C++ にも取り入れられてきており、 これからのプログラミングでは一般的な手法になって来ています。

処理の対象

これまで処理の対象をデータと表記していますが、 リスト(配列)のようなコンテナーだけでなく、 様々なものに対しても処理を行うことができるためです。

Dart では具体的には Iterable クラスを継承したクラスで使うことができます。 Dart のコンテナークラスは Map(連想配列) を除いて、この Iterable を継承しています。

dart_trans_iterable.png


さらに、 Dart には Mix-in の機能があるため、 IterableMixinを使えば、 自作したコンテナークラスでもこれから紹介する多くのデータ処理用のメソッドが使えるようになります。


また、ファイル等の並列処理のための Stream クラスも Iterable を継承してはいませんが、 同じようなメソッドを持っています。

逐次処理

データ処理の中でもっとも基本的な逐次処理を例に高階関数を使う方法を説明します。


C, C++ 風にリストの要素に順に処理していく場合にはループカウンターを使うと思います。
  var lis = ["foo", "bar", "baz"];
  for (var cnt = 0 ; cnt < lis.length ; cnt++) {
    print(lis[cnt]);
  }
  // foo
  // bar
  // baz  
これを Dart では for-in を使って書くことできます。
  for (var elem in lis) {
    print(elem);
  }
ループカウンターを使うよりも短く書けますし、リストの要素を処理しているということが分かりやすくなっていると思います。


これを高階関数を使って書きなおしてみます。
高階関数のデータ処理では forEach を使います。 渡す関数は、無名関数を使っていますが、通常の関数でも構いません。
  lis.forEach( (elem) => print(elem) );
  // 無名関数の別の書き方
  lis.forEach( (elem) { print(elem); } );
for-in 版とそんなには変わりません。 ですが、これは逐次処理に限って言語に制御文が用意されているからです。
高階関数のデータ処理では逐次処理にかぎらず、処理が短くかつ分かりやすく書けるようになります。
次の章からその高階関数を使ったできるデータ処理の基本的なものについて見てきます。


なお forEach も後で説明する"連結"で使えるため、いらないというわけでもありません。

基本的なメソッド

それでは、ここから高階関数を用いたデータ処理でまず最初に覚えておいた方がよいと思われる機能について説明していきます。主要な機能は次の 5 つです。
  1. 逐次処理(forEach)
  2. 写像(map)
  3. フィルター(where)
  4. 並び替え(sort)
  5. 畳み込み(fold, reduce)
このうち逐次処理は先ほど説明しました。残りの 4 つを説明していきます。

map : 写像

写像というのは map, mapping と呼ばれる処理で、 配列などのコレクションのメンバーに一つずつ関数を適用して 戻り値で新しいコレクションを作ります。
cs_linq_map.png

Dart ではそのまま map という関数名です。
  var src = [3, 2, 9, 6];
  var mapped = src.map((elem) => elem * 2); // [6, 4, 18, 12]
map に渡す関数は要素の型を引数にとる関数で、その戻り値が新しいコレクションの要素となります。 map は渡す関数の戻り値の型を変えれば、 値を変えるだけでなく、型を変えた新しいコレクションも作ることができ、 用途の広いメソッドです。

where : フィルター

フィルターはコレクションの中から条件にあう要素を取り出す処理です。
cs_linq_filter.png

Datt では where() という名前になっています。
  src = [3, 2, 9, 6];
  var filtered = src.where((elem) => elem % 2 == 1); // [3, 9]
データ処理で重要なメソッドをさらに絞るとすると、前節のマップとこのフィルターが、 特に重要度が高いです。

sort : 並び替え

sort は要素の並び替えを行います。
基本的な処理に入れましたが、並び替えは逐次的な処理では無いため少し特殊です。 そのせいか Dart では Iterable には含まれておらず、 List のメソッドとなっています。


sort では何も指定しなければ、要素の compareTo を使って比較します。 比較用のメソッドを渡すと比較の基準を変えることができます。
  lis = [-3, 2, -9, 0];
  lis.sort();                                     // [-9, -3, 0,  2]
  lis.sort((x, y) => x.abs().compareTo(y.abs())); // [0, 2, -3, -9]

fold, reduce : 畳み込み(縮退)


畳み込みは要素を順に取得し、それらを計算に使った結果を返す処理です。
cs_linq_fold.png
畳み込みは他言語では fold, reduce, inject などの関数名が使われます。 Dart では fold, reduce の 2 つが用意されています。

先に reduce を見ていきます。
  src = [3, 2, 9, 6];
  var sumval = src.reduce((sum, elem) => sum + elem);                // 20
  var maxval = src.reduce((max, elem) => (max < elem) ? elem : max); // 9

reduce に渡す関数は 2 つの引数を取り、 2 つ目が各要素で、 関数の戻り値が次に呼ばれた時の 1 つ目の引数です。 これにより、各関数の結果を重ねていったものが reduce の戻り値として得られます。
1 番最初に呼ばれる場合、 1 つ目の要素は最初の要素です。



1 番最初に呼ばれる場合の初期値を指定する場合に fold を使います。
  src = [3, 2, 9, 6];
  var leng = src.fold(0, (count, elem) => count+1);  // 4
  var cons = src.fold("", (str, elem) => str += elem.toString() + ' '); // "3 2 9 6 "

その他のメソット(プロパティー)

データ処理の定番のメソッド以外で、抑えておいた方がいいかなと思うものも挙げて置きます。
length、 isEmpty
要素数の取得と空かどうかのチェック (プロパティー)
var src = [3, 2, 9, 6];
src.length;   // 4
src.isEmpty;  // false
take
指定した要素数の取り出し。
var src = [3, 2, 9, 6];
src.take(2); // (3, 2)
firstWhere, lastWhere
指定した条件に最初(最後)にマッチする要素の検索。
検索もデータ処理ではよく行う処理ですが、 条件にあうすべての要素を取得するのがフィルター(where)で、 最初の要素を取得するのが firstWhere です。
var src = [3, 2, 9, 6];
src.firstWhere((elem) => elem % 2 == 1); // 3
src.lastWhere( (elem) => elem % 2 == 1); // 9
contains
要素を含んでいるかの判定。
var src = [3, 2, 9, 6];
src.contains(9);  // true
src.contains(5);  // false
any, every
要素どれか一つ(すべて)が条件を満たすかどうかの判定。
var src = [3, 2, 9, 6];
src.any(  (elem) => elem % 3 == 0); // true
src.every((elem) => elem % 3 == 0); // false
ここで紹介した以外にもまだメソッドはあります。 詳しくは Dart のリファレンスを見て下さい。

メソッドの連結

map や where の返すものをちゃんと見てみると、iterable を返しています。
var src = [3, 2, 9, 6];
var result = src.map((elem) => elem * 2);
print(result);              // (6, 4, 18, 12)
print(result.runtimeType);  // MappedListIterable
このため、これらのデータ処理は連結して書いていくことができます。
var src = [3, 2, 9, 6];
src.where((elem) => elem % 2 == 1)
  .map((elem) => elem * 2)
  .forEach((elem) => print(elem));
// 6
// 18

遅延評価

メソッドを連結して書くとメモリーがもったいないと思われる人もいるかもしれません。
しかし、 map 等の返す iterable は特殊なもので、 必要になるまで実行されないという遅延評価の機能があります。 前節の例では forEach で一つずつ取り出す時が実行のタイミングです。

連結の処理は一見、次のように処理してるように見えます。
              where           map
{3, 2, 9, 6}    →   {3, 9}    →   {6, 18}
しかし、実際には遅延評価により、メソッドごとに結果をためるのではなく、 1 要素ずつ流すように次のメソッドに渡していきます。
    where       map
3    →    3     →    6
2    →    ☓
9    →    9     →    18
6    →    ☓

なお、ソート(sort)は全要素が揃わないと完了しない処理なので、 言語によって扱いが変わってきます。

Dart ではソートでは遅延処理はしないという方針です。
そのため、 toList で一旦リストに変えてから List のメソッドでソートする必要があります。 このリストにする時が "必要なとき" となり、その時点で評価が行われます。
var src = [3, 2, 9, 6];
var resultsort = src.where((elem) => elem % 2 == 1)
  .map((elem) => elem * 2)
  .toList();
resultsort.sort((x, y) => y.compareTo(x));
print(resultsort); // [18, 6]

サンプルコード

説明で使用したサンプルのコードは以下のリンクからダウンロード(リンク先を保存)できます。 コンパイルする場合は以下のコマンドを実行します。
 > dart transform.dart
Dart をコマンドラインから使う方法については以前の記事を見て下さい。
関連記事
Prev.    Category    Next 

Facebook コメント


コメント

コメントの投稿

Font & Icon
非公開コメント

このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
40位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
8位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

02月 | 2025年03月 | 04月
- - - - - - 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 - - - - -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。