Dart における高階関数を使ったデータ処理
最近ではコンテナークラスなどのデータを高階関数を使って処理する手法が広がってきています。
今回はその高階関数を使ったデータ処理を Dart で行う方法について紹介します。
高階関数を使ったデータ処理
まず、高階関数を使ったデータ処理について説明します。プログラミング言語の傾向
高階関数を使ったデータ処理は Lisp や関数型言語でよく使われてきた方法で、 関数型プログラミングには欠かせないものです。しかし、関数型言語でなければ使えないというものではありません。
高階関数を使ったデータ処理はコードを短くかつわかりやすく記述することができます。 そのため、 Ruby, C#(LINQ) を始めとした多くの言語で取り入れられてきました。
- C# やるなら LINQ を使おう | プログラマーズ雑記帳
- 特集:人気言語でのデータ処理の比較: C#/ Scala / Python / Ruby / F#でデータ処理はどう違うのか? (1/3) - @ IT
処理の対象
これまで処理の対象をデータと表記していますが、 リスト(配列)のようなコンテナーだけでなく、 様々なものに対しても処理を行うことができるためです。Dart では具体的には Iterable クラスを継承したクラスで使うことができます。 Dart のコンテナークラスは Map(連想配列) を除いて、この Iterable を継承しています。

さらに、 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 つです。- 逐次処理(forEach)
- 写像(map)
- フィルター(where)
- 並び替え(sort)
- 畳み込み(fold, reduce)
map : 写像
写像というのは map, mapping と呼ばれる処理で、 配列などのコレクションのメンバーに一つずつ関数を適用して 戻り値で新しいコレクションを作ります。
Dart ではそのまま map という関数名です。
var src = [3, 2, 9, 6]; var mapped = src.map((elem) => elem * 2); // [6, 4, 18, 12]map に渡す関数は要素の型を引数にとる関数で、その戻り値が新しいコレクションの要素となります。 map は渡す関数の戻り値の型を変えれば、 値を変えるだけでなく、型を変えた新しいコレクションも作ることができ、 用途の広いメソッドです。
where : フィルター
フィルターはコレクションの中から条件にあう要素を取り出す処理です。
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 : 畳み込み(縮退)
畳み込みは要素を順に取得し、それらを計算に使った結果を返す処理です。

畳み込みは他言語では 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); // 9reduce に渡す関数は 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
メソッドの連結
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.dartDart をコマンドラインから使う方法については以前の記事を見て下さい。
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 ビット版のインストーラーをダウンロードします。
インストール
ダウンロードしたインストーラー(HaskellPlatform-YYYY.X.X.X-zzzz-setup.exe)を実行するとインストールが始まります。
インストール先を指定して、 ウィザードを続行すればインストールは完了です。
インストールすると "(インストール先)/bin" 以下にコンパイラー等の実行ファイルが格納されます。 bin フォルダーへの PATH の登録もインストーラーが行ってくれます。
使い方
bin フォルダー内の実行ファイルのうち、主に使うのは次の 3 つです。- GHCi : REPL(対話型)
- runghc : スクリプトとして実行
- 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 です。

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)