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 をコマンドラインから使う方法については以前の記事を見て下さい。
スポンサーサイト



 

Haskell の Windows へのインストールと Emacs モードの設定

Haskell は純粋関数型のプログラミング言語です。 今回は Haskell の Windows へのインストール、使い方および Emacs での設定について説明します。

Haskell とは

Haskell は関数型プログラミングをやるために作られたような言語で、 大きな特徴は次の 2 つです。
  • 純粋関数型言語
  • 遅延評価
関数型プログラミングでは値を変更しないように作ります。 しかし、関数型言語はいろいろとありますが、多くは使いやすさ等の理由により、制限付きで値が変更できるようになっています。
そんな中 Haskell は純粋関数型なので、本当に値が変更できません。 これは融通が利かないとも思われるかもしれませんが、 純粋に関数型でプログラミングできるだけの機能がそろっているとも言えます。


最近では C# の LINQ といったように関数型にとどまらず、高階関数を使った Stream 系のデータ処理が広がってきました。 そこでは必要になったときに初めて計算するという遅延評価になっていることが多いです。
これはメモリーの使用量を減らすなどのメリットがあってそうなっているのですが、 Haskell では全てで遅延評価となります。 こちらも関数型言語といわれる言語でもそこまで対応している言語はほとんどありません。


また、 Haskell はスクリプトとして動作させることもできますが、ソースコードをビルドして、実行ファイルを作るコンパイル型の言語です。ただ、コンパイル型といってもそんなに速いわけではありません。

Scala は JVM, F# は .NET というように新しい関数型言語の多くは仮想マシンで動作する言語が多いです。 新しい言語ではライブラリー等の環境が成熟していないといったデメリットがついて回るものなのですが、 JVM や .NET の言語ではそれがありません。
そういった意味では、 Haskell は学術的な意義の高い言語ですが、実用性という面ではこれからの言語だと思います。

インストール

Haskell には Haskell Platform というコンパイラーや開発ツールがセットになったものが用意されていて、インストーラーもあるので、インストールは簡単です。

ダウンロード

以下のサイトからインストールする環境にあわせて 32 ビットまたは 64 ビット版のインストーラーをダウンロードします。 haskell_dl.png

インストール

ダウンロードしたインストーラー(HaskellPlatform-YYYY.X.X.X-zzzz-setup.exe)を実行するとインストールが始まります。

haskell_inst.png

インストール先を指定して、 ウィザードを続行すればインストールは完了です。

インストールすると "(インストール先)/bin" 以下にコンパイラー等の実行ファイルが格納されます。 bin フォルダーへの PATH の登録もインストーラーが行ってくれます。

使い方

bin フォルダー内の実行ファイルのうち、主に使うのは次の 3 つです。
  1. GHCi : REPL(対話型)
  2. runghc : スクリプトとして実行
  3. ghc : コンパイラー

REPL(対話型) : GHCi

REPL は入力行をすぐ評価することができ、動作確認など開発時に使用します。
~/lang/hs $ ghci
GHCi, version 7.8.3: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> print "Hello world!"
"Hello world!"
Prelude> :quit
Leaving GHCi.
ghci で起動し、 :? でヘルプ、 :quit で終了します。

Windows のスタートメニューから [Haskell Platform YYYY.X.X.X] → [WinGHCi] を選択すると WinGHCi が起動します。こちらは多少 GUI で操作できる GHCi です。

haskell_winghci.png

runghc : スクリプトとして実行

まず、 ソースファイルを用意します。 Haskell ではファイルの拡張子には hs が使われます。

hello.hs :
main =
    print "Hello world!"
スクリプトとして使う場合には、 runghc にソースコードを渡して実行します。
~/lang/hs $ runghc.exe hello.hs 
"Hello world!"
スクリプトとして実行する場合 runhaskell(.exe) も使えます。
微妙な違いはありますが、ほぼ同じものです。

GHC : コンパイラー

GHC(Glasgow Haskell Compiler)ではソースファイルをコンパイルして実行ファイルを作ることができます。
~/lang/hs $ ghc hello.hs
[1 of 1] Compiling Main             ( hello.hs, hello.o )
Linking hello.exe ...
~/lang/hs $ ./hello.exe 
"Hello world!"
実行ファイル名はデフォルトでは 「ソースファイルのベース名 + ".exe"」 となります。 指定したい場合は -o オプションを使います。

その他

bin フォルダー内のその他の実行ファイルについても簡単に紹介します。
コマンド 機能 参考
ghc-pkg Haskell のパッケージ管理ツール。
パッケージ管理には cabal も使います。 cabal の方は lib/extralibs/bin フォルダーにあり、こちらにも PATH は通っています。
Haskellのパッケージ管理について調べてみた - りんごがでている
haddock ソースコードからドキュメントを作成。
Haskell 版の Doxygen や JavaDoc のようなものです。
Haskell の Javadoc, Haddock を使う - 一歩前進
hp2ps ヒープ領域のプロファイラー。 本物のプログラマは Haskell を使う - 第 46 回 ヒープ・プロファイラで領域漏れを探る: ITpro
hpc Haskell Program Coverage: カバレッジ(テストでソースコードをどの程度実行できたか)の計測ツール。 Haskell program coverage - HaskellWiki
hsc2hs C のコードを Haskell から利用するための補助ツール 12.2. C コードへの Haskell インタフェースを書く: hsc2hs

Emacs モードの設定

Hakell 用のパッケージはいろいろありますが、最低限必要なのは haskell-mode です。 これはパッケージマネージャーから簡単にインストールすることができます。 インストールすると hs などのファイルを開いた時に haskell-mode になります。

ただし、そのままではインデントをしようとすると変なエラーが出ます。 これはインデントのモードを設定していないためなので、 モードを 3 つの中から選んで、設定しておく必要があります。
モード 説明
haskell-simple-indent 単純な TAB によるインデント
haskell-indent コードの構造を考慮したインデント
haskell-indentation haskell-indent に加え、 [RET][DEL] のキーを拡張

これらのモードにする場合は turn-on-xxxx の関数を haskell-mode になった時に呼び出すようにします。
(add-hook 'haskell-mode-hook 'turn-on-haskell-indent)
Emacs では「改行 + インデント」はC-jを使うのが普通だと思うので、個人的にはhaskell-indent でいいと思います。 なお、 インデントに関しては hi2 というパッケージもあり、インデントルールは同じで、もう少し複雑な動きをします。 使う場合は hi2 をインストールして、同じような設定を書きます。
(add-hook 'haskell-mode-hook 'turn-on-hi2)



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

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

10月 | 2014年11月 | 12月
- - - - - - 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 - - - - - -


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

yohshiy

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

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

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