最近Haskellの勉強を始めたくらたです.
会社にすごいH本が届くという字面のインパクトが強すぎてビビっていたところです.
はじめに
概要
StreamAPIのsumやallMatchなどに代表されるリダクション操作の仕組みについて解説にします.
経緯
今回この記事を書こうと思った経緯は, StreamAPIなど関数型プログラミングっぽく書けるインターフェースをよく利用していながらも, 全然わたしってわかっていないじゃんと思ったのがきっかけ.
空ストリームに対するallMatch/noneMatchはtrueというブログの記事の内容とかわかっていませんでした.
このブログの内容をもとに, いろいろ調べていたらすっごく理解が深まった(ような気がする)ので, その内容をシェアします.
簡単なクイズ
Qiita java.util.Stream#allMatch() を使うときの注意より改変
練習
以下のコードを見て, どんな結果が出力されると思いますか.
ヒント(JavaDocより)だけ載せておきますね.
allMatch(Predicate<? super T> predicate)
Returns whether all elements of this stream match the provided predicate.noneMatch(Predicate<? super T> predicate)
Returns whether no elements of this stream match the provided predicate.
public class Sample {
public static void main(String args[]) {
List<String> fruits1 = Arrays.asList("apple", "apple");
System.out.println("1. " + fruits1.stream().allMatch(fruit -> fruit.equals("apple")));
System.out.println("2. " + fruits1.stream().noneMatch(fruit -> fruit.equals("apple")));
}
}
結果
1. true
2. false
本番
では本番. 空のStreamに対して同様の処理を行った場合, どんな結果が得られるでしょうか.
このクイズがわからなかった人に読んでほしいです.
public class Sample {
public static void main(String args[]) {
List<String> fruits2 = Collections.emptyList();
System.out.println("3. " + fruits2.stream().allMatch(fruit -> fruit.equals("apple")));
System.out.println("4. " + fruits2.stream().noneMatch(fruit -> fruit.equals("apple")));
}
}
結果
3. true
4. true
わからなかった人もいらっしゃるかしら.
わたしもわかりませんでした. わからなかったので調べた, それがこの記事です.
なんでこうなるのかをこの記事で理解してもらえたら嬉しいなと思っています.
本題
リダクション操作
JavaのAPIドキュメントによると, collectやallMatchなどのメソッドは終端操作の中でも特にリダクション操作と呼ばれているようです.
リダクション操作(折りたたみとも呼ばれる)は、一連の入力要素を受け取り、結合操作を繰り返し適用することでそれらを結合し、単一のサマリー結果を出力します(一連の数値の合計または最大値の検索や、リストへの要素の蓄積など)。ストリーム・クラスには、reduce()、collect()と呼ばれる複数の形式の汎用リダクション操作と、sum()、max()、count()など、複数の特殊化されたリダクション形式が含まれています。
リダクション操作の仕組み
平易な例として, 1〜10までの整数を足し込むプログラムをfor文で実装します.
public class Sample {
public static void main(String args[]) {
int[] numbers = IntStream.rangeClosed(1, 10).toArray();
int sum = 0;
for (int number : numbers) {
sum += number;
}
System.out.println("合計: " + sum);
}
}
上のプログラムでは, 初期値0に対して, 配列の要素を加算し続けています.
この初期値と演算がリダクション操作を理解するのに大切です.
初期値と演算について
そもそもなんで初期値を0にしたんでしょうか。
それはねどんな実数に0を足してもその実数自身が結果として返ってくるからですよね。
0 + n = n
が成り立つということ.
べき乗のプログラムを作るなら
初期値は1にしますよね。これも理屈は同じで
1 * n = n
が成り立つから1を初期値にしているわけです.
この性質を持つ集合の要素を単位元と呼びます.
リダクション操作は, 単位元を初期値として, 指定した演算を要素ごとに繰り返し行うことだったのです.
クイズの種明かし
前のセクションでも説明したとおり, 足し算なのか, かけ算なのかによって, 初期値が違いました.
Wikipedia 単位元によると, 集合と演算の種類によって単位元が異なるということが一覧表でわかります.
一覧表を確認してみると, 真偽値の論理積(AND)の単位元は真とあるので, 空Streamに対するallMatchメソッドやnoneMatchメソッドの戻り値はtrue
だということがわかります.
以上, 簡単でしょ?
補足
SreamAPIのreduceメソッドを使うと, リダクション操作における単位元と演算の重要性が理解できます.
ここでは, IntStream#sumメソッドをIntStream#reduceメソッドで表現しようと思います.
reduceメソッドのJavaDocは以下の通りでした.
reduce(int identity, IntBinaryOperator op)
Performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value.
identityとは単位元のことです. つまりreduceメソッドは単位元と演算を定義してリダクション操作を行うメソッドです.
このreduceメソッドを利用してIntStream#sumメソッドを表現すると以下のようになります.
public class Sample {
public static void main(String args[]) {
int sum = IntStream.rangeClosed(1, 10).reduce(0, (x, y) -> x + y);
System.out.println("合計: " + sum);
}
}
これで足し算を例にした簡単なリダクション操作について理解できたと思います.
クイズで紹介したallMatchやnoneMatchもreduceメソッドで表現することができるので, 練習がてら書いてみるとより一層理解が深まると思います.
参考文献
Qiita 空ストリームに対するallMatch/noneMatchはtrue
Wikipedia 単位元
Qiita java.util.Stream#allMatch() を使うときの注意