Java 8+で導入された関数型の機能をエンハンスするライブラリJavaslangに関する日本語の情報が少なかったので、Javaslangユーザガイドを日本語訳してみました。間違いや誤訳などあればご指摘ください。
Javaslangユーザガイド
Daniel Dietrich, Robert Winkler version 2.0.2, 2016-08-20
Translated by linzhixing, 2016-09-11
1. はじめに
JavaslangはJava 8+用の関数型ライブラリです。永続データ構造や、関数型プログラミングで使われる制御構造を提供しています。
1.1. Javaslangを用いたJava 8での関数型データ構造
Java 8のラムダ(λ)式のおかげで、このライブラリのAPIを洗練したものにすることができました。ラムダ式はJavaの表現力を飛躍的に高めてくれます。
Javaslangはラムダ式を利用して関数型言語のパターンに基づいた新しい機能を提供します。関数型コレクションのライブラリはそのうちの1つで、Java標準のコレクションを置き換えることを目的にしています。
(これはただの全体像です。後でちゃんと読める画像が出てきます)
1.2. 関数型プログラミング
データ構造の詳細について深入りする前にいくつか基本的な概念について触れることで、私がJavaslang、とくに新しいJavaコレクションを作ろうとした動機を説明しましょう。
1.2.1. 副作用
Javaアプリケーションはたいてい副作用で溢れているものです。何らかの状態を変更しなければならなかったり、変更がシステム外部に及ぶ場合もあります。典型的な副作用としてはオブジェクトや変数をその場で変更すること、コンソールへの出力、ログファイル/データベースへの書込みなどが挙げられます。プログラムの意味論に対して不適切な方法で影響を与える副作用は、有害だと考えられています。
たとえば関数が例外を投げ、その例外が解釈されるというのは、プログラムに影響を及ぼす副作用であると考えられます。さらに言うと、例外は非局所なgotoステートメントのようなもので、通常の制御フローを破壊してしまいます。しかしながら、現実のアプリケーションには副作用が伴うものです。
int divide(int dividend, int divisor) {
// throws if divisor is zero
return dividend / divisor;
}
関数型の世界では、副作用はTryに包んでしまうと便利です:
// = Success(result) or Failure(exception)
Try<Integer> divide(Integer dividend, Integer divisor) {
return Try.of(() -> dividend / divisor);
}
この割り算メソッドは、もう例外を投げません。処理が失敗する可能性はTry型として明示されています。
1.2.2. 参照透過性
関数、あるいはより一般的に言って式は、関数/式が呼び出されている箇所をその関数/式の値で置き換えてもプログラムの動作に何の影響もないとき、参照透過であると言われます。簡単に言えば、同じ入力に対して常に同じ出力が返ってくるということです。
// これは参照透過でない
Math.random();
// これは参照透過
Math.max(1, 2);
関数は、関数に含まれるすべての式が参照透過であるとき、純粋であると言われます。純粋関数から組み立てられたアプリケーションはおそらく、コンパイルさえすれば正常に動作し、我々はその動作を予測することができます。単体テストを書くのが簡単になり、デバッグなどというものは過去の遺物になることでしょう。
1.2.3. 値はすべての中心
Clojureの作者Rich Hickeyは、値の価値(The Value of Values)について素晴らしい洞察を残しています。とくに、イミュータブルな値は以下の性質を満たすので、最も興味深いものとして位置づけられています。
- 本質的にスレッドセーフであり、同期の必要がない
- equalsとhashCodeに関して安定して折り、信頼できるハッシュキーとなり得る
- 複製する必要がない
- 未検査の反変キャスト(Java特有の事情)で使用されても型安全に振る舞う
Javaをよりよく使うには、イミュータブルな値と参照透過性を組み合わせることが重要です。
Javaslangはこれらの目標をふつうのJavaプログラミングにおいて実現するために必要な制御構造とコレクションを提供するものです。
1.3. データ構造のクイックリファレンス
Javaslangのコレクションに関するライブラリは、ラムダ式をベースに設計された豊富な種類の関数型データ構造群から構成されます。Java標準のコレクションと共通しているインタフェースはIterableのみです。Javaのコレクションインタフェースの持つミュータブルなメソッドは、インタフェース背後に実際に存在するコレクション型のオブジェクトを返さない、というのがその理由です。さまざまな種類のデータ構造について説明する中で、この重要性が理解できるでしょう。
1.3.1. ミュータブルなデータ構造
Javaはオブジェクト志向のプログラミング言語です。データを隠蔽するために状態をオブジェクトの中にカプセル化し、その状態を変更できるように破壊的メソッドが用意されます。Javaコレクションフレームワーク(JCF)はこのアイディアに基づいて作られています。
interface Collection<E> {
// このコレクションからすべての要素を削除
void clear();
}
今日では、void型の返り値にはなにかまずいところ(code smell)がある、と認識されています。副作用が発生して状態が変更されていることを意味しているからです。ミュータブルな状態が共有されていることは、並行処理の場面でなくとも、エラーの主たる原因となります。
1.3.2. イミュータブルなデータ構造
イミュータブルなデータ構造は、一度作成された後は変更できません。イミュータブルなデータ構造はJavaの場合コレクションのラッパとして広く利用されています。
List<String> list = Collections.unmodifiableList(otherList);
// Boom!
list.add("why not?");
似たようなユーティリティメソッドを提供するライブラリには色々ありますが、いずれも与えられた特定のコレクションに対する変更不可能なビューを返します。破壊的メソッドを呼び出そうとすると一般的には実行時エラーが投げられます。
1.3.3. 永続データ構造
永続データ構造は更新時に、更新前の自分自身のバージョンを保存するので、結果として不変なデータ構造となります。完全な永続データ構造では、すべてのバージョンに対して更新とクエリをかけることができます。
たいていの操作では変更される部分はごくわずかであるため、以前のバージョンをそのままコピーするのは効率的ではありません。時間とメモリを節約するには、バージョン間の異同を識別してなるべく多くのデータを共有することが求められます。
このモデル自体は特定の実装に制限されていません。そこで関数型データ構造の出番となります。
1.4. 関数型データ構造
純粋関数型データ構造とも呼ばれますが、関数型データ構造はイミュータブルで永続的です。関数型データ構造のメソッドは参照透過性も持ちます。
Javaslangは、一般的に使用されている関数型データ構造を幅広くサポートしています。 以下具体的に詳しく説明します。
1.4.1. 連結リスト
一番よく使われており簡単な関数型データ構造から始めましょう。(単)連結リストはヘッド要素とテールリストを持っています。連結リストはスタックのように後入れ先出し(LIFO)構造に従います。
Javaslangでは次のようにListをインスタンス化します:
// = List(1, 2, 3)
List<Integer> list1 = List.of(1, 2, 3);
Listの各要素は、それぞれ別のListノードを形成します。最後の要素のテールは空白リストであるNilとなります。
この構造により、リストはバージョンを横断して要素を共有することができます。
// = List(0, 2, 3)
List<Integer> list2 = list1.tail().prepend(0);
新しい先頭要素0は元のListのテールに連結されます。元のリストは変更されないまま保たれます。
これらの操作にかかる時間は一定、つまりListのサイズには依存しません。その他の多くの処理には線型時間かかります。JavaslangではそのためにScalaでもおなじみのLinearSeqインタフェースが用意されています。
一定時間内に走査可能なデータ構造が必要なら、JavaslangのArrayやVectorが使えます。両者はいずれもランダムアクセスが可能です。
Array型はJavaの配列を背後に持っています。挿入や削除は線型時間で行われます。VectorはArray型とList型の中間で、ランダムアクセスと変更に適しています。
連結リストは、実は次のキューと呼ばれるデータ構造を実装するのにも使われています。
1.4.2. キュー
キューは2つの連結リストから、関数型で非常に効率的に実装することができます。*前の(front)Listはキューから取り出された(dequeue)*要素を格納し、*後ろの(rear)Listはキューに入れられた(enqueue)*要素を格納します。キューの出し入れはどちらもO(1)時間で実行されます。
Queue<Integer> queue = Queue.of(1, 2, 3)
.enqueue(4)
.enqueue(5);
まずQueueが3つの要素で初期化された後、2つの要素が後ろのListに追加されます。
前のリストから要素がすべて取り出されたら、後ろのリストが反転されてから新しい前のリストになります。
キューから取り出すとき、キューの先頭だった要素と残りのキューのペアを取得することができます。新しいバージョンのQueueを返すのは、イミュータブルで永続的な関数型データ構造としての性質を満たすためです。元のキューには何の影響も及びません。
Queue<Integer> queue = Queue.of(1, 2, 3);
// = (1, Queue(2, 3))
Tuple2<Integer, Queue<Integer>> dequeued =
queue.dequeue();
Queueが空だったらどうなるでしょうか? dequeue()はNoSuchElementExceptionを投げてしまいます。もっと関数型な方法で行うには、optional値の結果を返すようにします。
// = Some((1, Queue()))
Queue.of(1).dequeueOption();
// = None
Queue.empty().dequeueOption();
optionalの返り値は、その値がemptyであってもなくてもそのまま後続処理に流すことができます。
// = Queue(1)
Queue<Integer> queue = Queue.of(1);
// = Some((1, Queue()))
Option<Tuple2<Integer, Queue<Integer>>> dequeued =
queue.dequeueOption();
// = Some(1)
Option<Integer> element = dequeued.map(Tuple2::_1);
// = Some(Queue())
Option<Queue<Integer>> remaining =
dequeued.map(Tuple2::_2);
1.4.3. ソート済みセット
ソート済みセットはキューよりも頻繁に使われるデータ構造です。ソート済みセットを使って二分探査木を関数型の方法で実現することができます。二文探査木の各ノードには高々2個の子ノードがあり、各ノードには値が付与されています。
二分探査木を作成する前提として、要素のComparatorにより要素間になんらかの順序が導入されているとしましょう。任意の与えられたノードの左側の部分木上のどの値も、その与えられたノードの値よりも小さくなっています(等号を含みません)。同様に右側の部分木の方は大きくなっています。
// = TreeSet(1, 2, 3, 4, 6, 7, 8)
SortedSet<Integer> xs = TreeSet.of(6, 1, 3, 2, 4, 7, 8);
このような木の上での探索はO(log n)時間かかります。ルートから探索を開始し、目的のノードが見つかったか判定しまい。 値全体が順序づけられているおかげで、現在の木の左ブランチ/右ブランチどちらを次に探せばよいかは明らかです。
// = TreeSet(1, 2, 3);
SortedSet<Integer> set = TreeSet.of(2, 3, 1, 2);
// = TreeSet(3, 2, 1);
Comparator<Integer> c = (a, b) -> b - a;
SortedSet<Integer> reversed = TreeSet.of(c, 2, 3, 1, 2);
木に対する操作はたいてい本質的に再帰的なものです。挿入関数も探索関数と同じように振る舞います。探索パスが終端に達したら新しいノードが作成され、ルートにいたるまで遡ってパスが再構築されます。既存の子ノードたちを参照することもいつでもできます。したがって挿入の操作はO(log n)の時間と容量で実行されます。
// = TreeSet(1, 2, 3, 4, 5, 6, 7, 8)
SortedSet<Integer> ys = xs.add(5);
二分木の性能に関するこのような特徴は、バランスを取って維持していかなければなりません。ルートから末端へのパスはおおまかに同じ長さにしておく必要があります。
Javaslangでは二分探査木を赤黒木に基づいて実装しており、挿入と削除に対してツリーのバランスを取るために特別な色分け戦略を利用しています。詳細については書籍Purely Functional Data Structures(Chris Okasaki 著)を参照してください。
1.5. コレクションの現状
様々なプログラミング言語が収斂していく過程を我々は体験してきました。良い性質は残り、その他は淘汰されていきます。しかしJavaでは事情が異なります。後方互換性に永遠に縛りつけられているのです。それは強みであるともいえますが、進化を遅らせることにもなっています。
ラムダ式はJavaとScalaを近づけましたが、依然として大きな違いが存在しています。Scalaの作者Martin Oderskyは最近BDSBTB 2015 キーノートでJava 8のコレクションの現状についてて言及しています。
彼によると、JavaのStreamはIteratorを派手にしただけのものに過ぎません。Java8のSream APIは持ち上げられたコレクションの一例です。やっていることは、まず処理の内容を定義し、さらに別の明示的なステップにおいてその処理を特定のコレクションに結びつけているだけです。
// i + 1
i.prepareForAddition()
.add(1)
.mapBackToInteger(Mappers.toInteger())
Java 8 Stream APIがやっていることはこういうことです。よく知られたJavaコレクションの上に設けられた処理用のレイヤにすぎません。
// = ["1", "2", "3"] in Java 8
Arrays.asList(1, 2, 3)
.stream()
.map(Object::toString)
.collect(Collectors.toList())
JavaslangはScalaに非常に大きな影響を受けており、さきほどのJava 8のサンプルは次のように書き換えられます。
// = Stream("1", "2", "3") in Javaslang
Stream.of(1, 2, 3).map(Object::toString)
昨年我々は多大な努力をかけてJavaslangコレクションライブラリを実装しました。このライブラリには最も広く使用されているコレクション型が含まれています。
1.5.1. Seq
順序を持つ型から紹介しましょう。上のほうですでに連結リストについて説明しました。その後に遅延型の連結リストであるStreamにも触れました。これにより、潜在的に無限の長さの要素を持つシーケンスを処理することが可能になります。
すべてのコレクションはIterableであるため、拡張for文で使用することができます。
for (String s : List.of("Java", "Advent")) {
// 副作用や変更処理
}
同じことは、ループを内部化して所定の振るまいをラムダ式で注入することでも実現できます。
List.of("Java", "Advent").forEach(s -> {
// 副作用や変更処理
});
いずれにせよ前にも言った通り、何も返さないよりは何か値を返す式の方を我々は好みます。単純な例により、複数の文を連ねることがノイズをもたらし、ひとまとめにすべきものを分割してしまっているかを確認してみましょう。
String join(String... words) {
StringBuilder builder = new StringBuilder();
for(String s : words) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(s);
}
return builder.toString();
}
Javaslangのコレクションは背後にある要素に対して作用する関数を数多く提供しており、もっと簡潔にやりたいことを表現することができます。
String join(String... words) {
return List.of(words)
.intersperse(", ")
.fold("", String::concat);
}
たいていの目的はJavaslangを使って色々な方法で実現することができます。このメソッドの中身をListインスタンスに対する便利な関数の呼び出しに置き換えることもできますし、Listを直接使ってメソッド全体を置き換えることもできます。
List.of(words).mkString(", ");
こうして実際のアプリケーションでもコードの行数を圧倒的に削減しバグのリスクを低減することができるようになります。
1.5.2. SetとMap
シーケンスはとても優れたものです。しかし完全なコレクションライブラリとしてはSetとMapという異なるタイプもサポートする必要があります。
ソート済みセットを二分探査木でモデル化する方法は説明しました。ソート済みマップは、キー・バリューペアおよびキーの順序を持ったソート済みセットに外なりません。
HashMapの実装はHash Array Mapped Trie (HAMT)に基づいています。同様に、HashSetはキー・キーペアを含んだHAMTにより実現されています。
我々のMapはキー・バリューペアを表現するために特別なEntry型を持っていません。その代わりJavaslangの一部であるTuple2を使います。Tupleの各フィールドは1つずつ取り出すことができます。
// = (1, "A")
Tuple2<Integer, String> entry = Tuple.of(1, "A");
Integer key = entry._1;
String value = entry._2;
MapsとTuplesはJavaslangの至るところで使われています。複数の返り値を一般的なやり方で扱おうとするなら、Tupleは不可欠です。
// = HashMap((0, List(2, 4)), (1, List(1, 3)))
List.of(1, 2, 3, 4).groupBy(i -> i % 2);
// = List((a, 0), (b, 1), (c, 2))
List.of('a', 'b', 'c').zipWithIndex();
Javaslangでは実証実験として、99 Euler Problemsを実装することによりライブラリの改良とテストを行っています。お気兼ねなくプルリクエストをお寄せください。
2. Javaslangを使い始めるには
Javaslangを含んだプロジェクトは最低でもJava 1.8以上を対象にする必要があります。
jarファイルはMavent Centralで取得できます。
2.1. Gradle
dependencies {
compile "io.javaslang:javaslang:2.0.2"
}
2.2. Maven
<dependencies>
<dependency>
<groupId>io.javaslang</groupId>
<artifactId>javaslang</artifactId>
<version>2.0.2</version>
</dependency>
</dependencies>
2.3. スタンドアロン
Javaslangは(JVM以外に)どんなライブラリにも依存していないので、スタンドアロンの.jarとして簡単にクラスパスに追加することができます。
2.4. スナップショット
開発者バージョンはここから取得できます。
2.4.1. Gradle
追加のスナップショットリポジトリをbuild.gradleに加えます:
repositories {
(...)
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}
2.4.2. Maven
~/.m2/settings.xmlに以下の記述を含めます:
<profiles>
<profile>
<id>allow-snapshots</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
3. 使い方
Javaslangは、Javaに存在していなかったり非常に基本的である型のいくつかを、洗練された設計のもとに実装しています: タプル
、値
、ラムダ式
。
Javaslangはすべてがこの3つの基本的な構成要素から組み立てられています。
3.1. タプル
Javaには一般的なタプルの概念がありません。タプルは一定数の要素を組み合わせたもので、ひとまとめにして受け渡すことかできます。配列やリストとは異なり、タプルは異なる型のオブジェクトを保持することができることと、データがイミュータブルであることに注意してください。
タプルにはTuple1, Tuple2, Tuple3などがあります。現在のところ上限は8つの要素までです。タプルの要素にアクセスするには、1番目の要素にt._1
メソッド、2番目の要素にはt._2
メソッドというようにします。
3.1.1. タプルの作成
この例ではStringとIntegerを持つタプルを作成します:
// (Java, 8)
Tuple2<String, Integer> java8 = Tuple.of("Java", 8); //<1>
// "Java"
String s = java8._1; //<2>
// 8
Integer i = java8._2; //<3>
- タプルはstaticなファクトリメソッドTuple.of()で作成されます。
- タプルの1番目の要素を取得します。
- タプルの2番目の要素を取得します。
3.1.2. タプルの各要素のマップ
タプルの要素ごとに関数が評価され、別のタプルが返されます。
// (Javaslang, 2)
Tuple2<String, Integer> that = java8.map(
s -> s + "slang",
i -> i / 4
);
3.1.3. 単一関数によるタプルのマップ
単一のマッピング関数だけでタプルをマップすることもできます。
// (Javaslang, 2)
Tuple2<String, Integer> that = java8.map(
(s, i) -> Tuple.of(s + "slang", i / 4)
);
3.1.4. タプルの変換
変換により、タプルの内容に基づいて新しい型の返り値が作成されます。
// "Javaslang 2"
String that = java8.transform(
(s, i) -> s + "slang " + i / 4
);
3.2. 関数
関数型プログラミングとは、値と、値を関数を使用して変換すること以外のなにものでもありません。Java 8では、1つの引数を受け取るFunction
、2つの引数を受け取るBiFunction
が用意されています。Javaslangは最大8つの引数まで関数を用意しています。この関数インタフェースはFunction0, Function1, Function2, Function3
などと定義されています。検査例外を投げる関数が必要な場合は、CheckedFunction1, CheckedFunction2
などを使うこともできます。
次のラムダ式は2つの整数を足す関数を作成します:
// sum.apply(1, 2) = 3
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
これは以下の匿名クラスによる定義を省略したものです:
Function2<Integer, Integer, Integer> sum = new Function2<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer a, Integer b) {
return a + b;
}
};
またstaticなファクトリメソッドFunction3.of(…)を使用して任意のメソッド参照から関数を作成することもできます。
Function3<String, String, String, String> function3 =
Function3.of(this::methodWhichAccepts3Parameters);
さらに、Javaslangの関数インタフェースはJava 8の関数インタフェースよりも強化されています:
-
合成(Composition)
-
持ち上げ(Lifting)
-
カリー化(Currying)
-
メモ化(Memoization)
3.2.1. 合成(Composition)
関数は合成することができます。数学では関数の合成とは、ある関数の結果に別の関数を適用して、第三の関数を得ることをいいます。たとえば関数 f : X → Y および g : Y → Z を合成して、X → Zに移す関数h: g(f(x))
が得られます。
andThen
を使うこともできますし:
Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiplyByTwo = a -> a * 2;
Function1<Integer, Integer> add1AndMultiplyBy2 = plusOne.andThen(multiplyByTwo);
then(add1AndMultiplyBy2.apply(2)).isEqualTo(6);
compose
を使うこともできます:
Function1<Integer, Integer> add1AndMultiplyBy2 = multiplyByTwo.compose(plusOne);
then(add1AndMultiplyBy2.apply(2)).isEqualTo(6);
3.2.2. 持ち上げ(Lifting)
部分関数を、Option値を返す全域関数へと持ち上げることができます。部分関数という用語は数学から来ています。XからYへの部分関数は、Xのある部分集合X′に対してf: X′ → Yの形をしています。この考え方によれば、関数f: X → YにおいてXのどの要素もかならずYの要素に写されるわけではない場合にまで、関数の概念を一般化することができます。つまり部分関数は特定の入力値に対してのみ有効に動作します。もし許可されていない入力値で関数が呼ばれれば、普通は例外が投げられます。
以下のdivideメソッドはゼロ以外の分母のみを受け付ける部分関数です。
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
lift
を使用すれば、divide
をすべての入力値に対して定義された全域関数に変更することができます。
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);
// = None
Option<Integer> i1 = safeDivide.apply(1, 0); //<1>
// = Some(2)
Option<Integer> i2 = safeDivide.apply(4, 2); //<2>
- 持ち上げられた関数は、許可されていない入力値で呼び出された場合、例外を投げる代わりに
None
を返します。 - 持ち上げられた関数は、許可された入力値で呼び出された場合、
Some
を返します。
次のsum
メソッドは正の入力値しか受け取らない部分関数です。
int sum(int first, int second) {
if (first < 0 || second < 0) {
throw new IllegalArgumentException("Only positive integers are allowed"); //<1>
}
return first + second;
}
-
sum
関数は負の入力値に対してIllegalArgumentException
を投げます。
sum
メソッドはメソッド参照により持ち上げることができます。
Function2<Integer, Integer, Option<Integer>> sum = Function2.lift(this::sum);
// = None
Option<Integer> optionalResult = sum.apply(-1, 2); //<1>
- 持ち上げられた関数は
IllegalArgumentException
をキャッチするとNone
を返します。
3.2.3. カリー化(Currying)
カリー化は引数のいくつかを固定して関数を部分適用するというテクニックです。引数の総数にもよりますが、1つ以上の引数を固定することができます。引数は左から先に右へと束縛されていきます。
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
Function1<Integer, Integer> add2 = sum.curried().apply(2); //<1>
then(add2.apply(4)).isEqualTo(6);
- 最初の引数aは値2に固定されます。
3.2.4. メモ化
メモ化は一種のキャッシュです。メモ化された関数は一度だけ評価され、その後はキャッシュから値が返されます。
以下の例では最初の呼び出し時に乱数を計算し、二度目以降の呼び出しではキャッシュされた数値が返されています。
Function0<Double> hashCache =
Function0.of(Math::random).memoized();
double randomValue1 = hashCache.apply();
double randomValue2 = hashCache.apply();
then(randomValue1).isEqualTo(randomValue2);
3.3. 値
関数型の世界では、値というものは正規化された項であり、それ以上評価されません。Javaでこの性質を実現するには、オブジェクトの状態をfinalにし呼び出しに対してイミュータブルにする必要がありました。
Javaslangの関数型の値は不変なオブジェクトを抽象化したものです。インスタンス間で不変なメモリが共有されることで、効率的な書込み操作が可能になります。何もしなくてもスレッドセーフが実現されるのです!
3.3.1. Option
Option型はモナド的なコンテナで、optional値を返します。OptionのインスタンスはSome
またはNone
どちらかのインスタンスになります。
// optional *value*, no more nulls
Option<T> option = Option.of(...);
3.3.2. Try
Try型はモナド的なコンテナで、エラーになるかあるいは正常に値を返す可能性がある処理を表現します。Either
と似ていますが、セマンティクスが異なっています。TryのインスタンスはSuccess
かFailure
どちらかのインスタンスになります。
// 例外処理は不要
Try.of(() -> bunchOfWork()).getOrElse(other);
A result = Try.of(this::bunchOfWork)
.recover(x -> Match(x).of(
Case(instanceOf(Exception_1.class), ...),
Case(instanceOf(Exception_2.class), ...),
Case(instanceOf(Exception_n.class), ...)
))
.getOrElse(other);
3.3.3. Lazy
Lazyはモナド的なコンテナで、遅延評価される値を表現します。Supplierと違うのは、Lasyがメモ化されている点です。つまり評価は一度だけ行われるので、参照透過性を持ちます。
Lazy<Double> lazy = Lazy.of(Math::random);
lazy.isEvaluated(); // = false
lazy.get(); // = 0.123 (random generated)
lazy.isEvaluated(); // = true
lazy.get(); // = 0.123 (memoized)
バージョン2.0.0からは、その型のままで遅延評価される値を作ることもできます(インタフェースでしか上手くいきませんが):
CharSequence chars = Lazy.of(() -> "Yay!", CharSequence.class);
3.3.4. Either
Eitherは2つの型どちらかになり得る値を表現します。EitherはLeftかRightのどちらかになります。もし所与のEitherがRightでありながらLeftに射影された場合、Leftに関する操作はRightである値に何の影響も及ぼしません。もし所与のEitherがLeftでありながらRightに射影された場合、Rightに関する操作はLeftである値に何の影響も及ぼしません。もしLeftがLeftに、あるいはRightがRightに射影されれば、それらの操作は意味を持ちます。
例: compute()関数はInteger値(正常時)またはString型のエラーメッセージ(エラー時)を返すとします。慣習により正常値はRightに、エラーはLeftに割り当てられます。
Either<String,Integer> value = compute().right().map(i -> i * 2).toEither();
compute()の結果がRight(1)の場合、valueはRight(2)になります。
compute()の結果がLeft("error")の場合、valueはLeft("error")になります。
3.3.5. Future
Futureはある時点から利用可能になる処理結果を表します。Futureに対する操作はノンブロッキングで実行されます。背後ではExecutorServiceにより非同期にハンドル処理されています。例: onComplete(…)
Futureには実行中(pending)と完了(completed)の2つの状態があります:
実行中(Pending): 処理が実行中です。完了またはキャンセル可能なのは実行中のfutureだけです。
完了(Completed): 処理が正常に終了しなんらかの結果が返されたか、エラーのため例外が発生したか、キャンセルされた状態です。
Futureに対していつでもコールバックを登録することができます。登録されたアクションはFutureが完了次第すぐに実行されます。完了したFutureに対して登録されたアクションは、その場でただちに実行されます。アクションは背後にあるExecutorServiceの設定により、別のスレッドで実行することが可能です。キャンセルされたFutureに対して登録されたアクションは、エラーが発生したときの結果が渡されて実行されます。
// future *value*, result of an async calculation
Future<T> future = Future.of(...);
3.3.6. Validation
Validation制御構造はアプリカティブファンクターであり、複数のエラーを累積的に扱うのに向いています。モナドの合成では、合成された処理は最初にエラーが発生した時点で通常のルートから外れます。しかし'Validation'では関数の合成された処理を続行し、すべてのエラーを累積していきます。この動作はWebフォームなど複数のフィールドにバリデーションが適用されていて、1度に1つのエラーではなく発生した全てのエラーを知りたいという場合に、とくに役立ちます。
例: Webフォームから'name'と'age'フィールドを取得し、有効なPersonインスタンスか、バリデーションエラーのリストを返します。
PersonValidator personValidator = new PersonValidator();
// Valid(Person(John Doe, 30))
Validation<List<String>, Person> valid = personValidator.validatePerson("John Doe", 30);
// Invalid(List(Name contains invalid characters: '!4?', Age must be greater than 0))
Validation<List<String>, Person> invalid = personValidator.validatePerson("John? Doe!4", -1);
バリデーションで有効とされる値はValidation.Valid
インスタンスに、バリデーションエラーのリストはValidation.Invalid
インスタンスに格納されます。
以下のバリデータは、1つのValidationインスタンスに様々なバリデーション結果を組み合わせるために使われています。
class PersonValidator {
private static final String VALID_NAME_CHARS = "[a-zA-Z ]";
private static final int MIN_AGE = 0;
public Validation<List<String>, Person> validatePerson(String name, int age) {
return Validation.combine(validateName(name), validateAge(age)).ap(Person::new);
}
private Validation<String, String> validateName(String name) {
return CharSeq.of(name).replaceAll(VALID_NAME_CHARS, "").transform(seq -> seq.isEmpty()
? Validation.valid(name)
: Validation.invalid("Name contains invalid characters: '"
+ seq.distinct().sorted() + "'"));
}
private Validation<String, Integer> validateAge(int age) {
return age < MIN_AGE
? Validation.invalid("Age must be greater than " + MIN_AGE)
: Validation.valid(age);
}
}
バリデーションに成功すると(つまり入力値が有効な場合)、Personインスタンスが与えられたnameとageで作成されます。
class Person {
public final String name;
public final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person(" + name + ", " + age + ")";
}
}
3.4. コレクション
Javaslangは大変な努力を費やして、関数型プログラミングの、言い換えれば不変性の条件を満たす全く新しいコレクションライブラリをJavaのために設計しました。
JavaのStreamによる持ち上げは、処理を別のレイヤに持ち上げた上で、それを対象となるコレクションに
結びつけるためにさらに明示的なステップを必要とします。Javaslangならそのような余分なボイラープレートは必要ありません。
新しいコレクション群はjava.lang.Iterableをベースにしているので、おなじみの糖衣的な構文で繰り返し処理を行うことができます。
// 1000 random numbers
for (double random : Stream.gen(Math::random).take(1000)) {
...
}
TraversableOnce
1 はコレクションを操作するための便利な関数を数多く提供しています。java.util.stream.StreamのAPIはTraversableOnceに似ていますが、TraversableOnceの方が成熟したものになっています。
3.4.1. List
JavaslangのList
はイミュータブルな連結リストです。変更されるとは、新しいインスタンスを作成します。ほとんどの操作は線型時間で実行されます。後続の操作は1つずつ逐次的に実行されます。
Java 8
Arrays.asList(1, 2, 3).stream().reduce((i, j) -> i + j);
IntStream.of(1, 2, 3).sum();
Javaslang
// javaslang.collection.List
List.of(1, 2, 3).sum();
3.4.2. Stream
javaslang.collection.Stream
は遅延連結リストで実装されています。値は必要になったときに1度だけ評価されます。遅延評価のおかげでほとんどの処理は一定時間内に処理されます。処理は中間操作であり、まとめて一気通貫に実行されます。
Streamのすごいところは、(原理的には)無限長のシーケンスを表現するのに使えるということです。
// 2, 4, 6, ...
Stream.from(1).filter(i -> i % 2 == 0);
3.5. プロパティのチェック
プロパティのチェック(プロパティのテストとも呼ばれます)とは、関数型の方法でコードのプロパティをテストするための強力な方法です。テストはランダムに生成されたデータをユーザ定義のチェック関数に渡して実行されます。
Arbitrary<Integer> ints = Arbitrary.integer();
// square(int) >= 0: OK, passed 1000 tests.
Property.def("square(int) >= 0")
.forAll(ints)
.suchThat(i -> i * i >= 0)
.check()
.assertIsSatisfied();
単純なデータ型の生成を組み合わせることで、複雑なデータ型を生成することができます。
3.6. パターンマッチ
Scalaはネイティブでパターンマッチをサポートしており、普通のJavaよりも優れた点の1つとなっています。その基本的な構文はJavaのswitchに近いものです:
val s = i match {
case 1 => "one"
case 2 => "two"
case _ => "?"
}
しかしながら、matchが式であり値を返すことに注意してください。それだけでなくScalaのmatchは:
- 名前付きパラメータ
case i: Int ⇒ "Int " + i
- オブジェクトの抽出
case Some(i) ⇒ i
- ガード
case Some(i) if i > 0 ⇒ "positive " + i
- 複数条件
case "-h" | "--help" ⇒ displayHelp
- コンパイル時に網羅性をチェック
をサポートしています。
パターマッチは、if-then-elseのたくさんの分岐を積み上げることから我々を解放してくれます。コード量を削減するとともに、本質的な内容にフォーカスすることができるようになるのです。
3.6.1. Javaでのマッチの基本
Javaslang 2.0はScalaのmatchに似た新しいマッチAPIを導入しました。アプリケーションに以下のインポートをすれば使えるようになります。:
import static javaslang.API.*;
staticメソッドMatch、Case、および原子的な以下のパターン
-
$()
- ワイルドカードパターン -
$(value)
- 同値パターン -
$(predicate)
- 条件式パターン
をコードのスコープ内に含めた状態であれば、最初のScalaの例は次のように書けるようになります:
String s = Match(i).of(
Case($(1), "one"),
Case($(2), "two"),
Case($(), "?")
);
⚡ 大文字始まりのメソッドになっているのは、Javaでは'case'が予約語になっているためです。そのためこのAPIはちょっと特別扱いになっています。
網羅性
最後のワイルドカードパターン$()を使えば、マッチするケースがない場合にMatchErrorが投げられることを避けられます。
Scalaコンパイラのように網羅性チェックを行うことができないので、optional値を返すことができるようにしています
Option<String> s = Match(i).option(
Case($(0), "zero")
);
構文糖衣
Caseの最初の引数が条件式パターン$(predicate)である場合、簡単にこう書くこともできます。
Case(predicate, ...)
⚡ この簡略化された記法は、$(value)には曖昧性が生じるため使えないことに注意してください。
Javaslangは標準でいくつか条件式を用意しています。
import static javaslang.Predicates.*;
これを使えば最初のScalaの例は次のように書くことができます:
String s = Match(i).of(
Case(is(1), "one"),
Case(is(2), "two"),
Case($(), "?")
);
複数条件
isIn
述語を使えば複数の条件でチェックすることができます。:
Case(isIn("-h", "--help"), ...)
副作用のある処理
Matchは式のように振る舞うので、値を返さなければなりません。副作用を発生させるにはVoid
を返すヘルパー関数run
を使用する必要があります:
Match(arg).of(
Case(isIn("-h", "--help"), run(this::displayHelp)),
Case(isIn("-v", "--version"), run(this::displayVersion)),
Case($(), run(() -> {
throw new IllegalArgumentException(arg);
}))
);
⚡ 当然ですがvoidはJavaでは有効な返り値ではないため、曖昧さを回避するためにrunが使用されています。
注意: run
を返り値として直接(=ラムダ式の外で)使用しないでください:
// Wrong!
Case(isIn("-h", "--help"), run(this::displayHelp))
もしそうしなければ、Caseはパターンマッチの前に先行して評価されるため、Match式全体が機能しなくなります。代わりにラムダ式の中でならrun
を使うことができます:
// Ok
Case(isIn("-h", "--help"), o -> run(this::displayHelp))
すでに見たようrun
は使い方を誤ればエラーの原因となるので注意してください。将来のバージョンではrun
を非推奨にしてもっとよいAPIを副作用のために提供する予定です。
名前付きパラメータ
Javaslangはラムダ式を利用して名前付きパラメータでマッチした値を束縛します。
Number plusOne = Match(obj).of(
Case(instanceOf(Integer.class), i -> i + 1),
Case(instanceOf(Double.class), d -> d + 1),
Case($(), o -> { throw new NumberFormatException(); })
);
ここまで値を原子的なパターンでマッチしてきました。原子的パターンでマッチすると、マッチしたオブジェクトの型はパターンのコンテキストから推論されます。
それでは次に、オブジェクトのグラフに(理論的には)任意の深さでマッチできる再帰的パターンについて見てみましょう。
オブジェクトの分解(デコンストラクタ)
Javaではクラスを初期化するためにコンストラクタを使用します。オブジェクトの分解とは、オブジェクトをその部分に分解することを指します。
コンストラクタが引数に適用されて新しいインスタンスを作成する関数だとすれば、デコンストラクタはインスタンスを取りその部分を返す関数です。オブジェクトが抽出された(unapplied)とも言います。
オブジェクトの解体の方法は一通りではありません。たとえばLocalDateは次のように分解されます。
-
年、月、日の各部分
-
そのインスタンスに対応するエポックミリ秒のlong値
-
など
3.6.2. パターン
Javaslangではパターンを使って、特定の型のインスタンスがどのように分解されるかを定義します。パターンはMatch APIと連携して使用されます。
事前定義されたパターン
Javaslangの用意する多くの型にはすでにマッチパターンが定義されています。インポートするには
import static javaslang.Patterns.*;
たとえばTryの結果をマッチすることができます:
Match(_try).of(
Case(Success($()), value -> ...),
Case(Failure($()), x -> ...)
);
⚡ Javaslang Match APIの初期のプロトタイプでは、マッチしたパターンからユーザが自由にオブジェクトを抽出することができました。しかしこれでは生成されるメソッドが指数関数的に増大してしまうため、コンパイラによる適切なサポートがない限り現実的ではありません。現在のAPIでは妥協として、すべてのパターンでマッチさせることはできるものの分解の対象となるのはルートパターンのみとしています。
Match(_try).of(
Case(Success(Tuple2($("a"), $())), tuple2 -> ...),
Case(Failure($(instanceOf(Error.class))), error -> ...)
);
この例ではルートパターンはSuccessとFailureになります。これらは適切な総称型を伴うTuple2あるいはErrorに分解されます。
⚡ 深くネストされた型は、Matchの引数にしたがって推論されるのであって、マッチしたパターンにしたがって推論されるのではありません。
ユーザ定義のパターン
finalクラスのインスタンスも含め、任意のオブジェクトからの抽出は必要不可欠な機能です。Javaslanでは、コンパイル時に有効な@Patterns
および@Unapply
アノテーションにより、宣言的なスタイルでこの機能が実現されています。
アノテーションのプロセッサを有効化するにはjavaslang-matchアーティファクトをプロジェクトの依存性に加える必要があります。
⚡ 注意: もちろんパターンをコードジェネータを使用せずに実装することもできます。詳細については生成されたコードを参照してください。
import javaslang.match.annotation.*;
@Patterns
class My {
@Unapply
static <T> Tuple1<T> Optional(java.util.Optional<T> optional) {
return Tuple.of(optional.orElse(null));
}
}
アノテーションプロセッサはMyPatternsファイルを同じパッケージに置きます(標準ではtarget/generated-sourcesフォルダ)。インナークラスもサポートされています。特別なケースとして、クラス名が$の場合、生成されるクラス名は接頭辞なしの単なるPatternsになります。
ガード
Optionalをガードを使ってマッチさせることができるようになりました。
Match(optional).of(
Case(Optional($(v -> v != null)), "defined"),
Case(Optional($(v -> v == null)), "empty")
);
述語はisNull
およびisNotNull
が実装されればもっと単純化できます。
⚡ null値を抽出するのが恐ろしいことであることは言うまでもありません。JavaのOptionalを使用する代わりにJavaslangのOptionを試してください!
Match(option).of(
Case(Some($()), "defined"),
Case(None(), "empty")
);
3.6.3. 今後の展望
Javaslangの近いリリースでは、標準で定義された述語がもっと増える予定です。たとえば
isNull
-
isNotNull
やnonNull
- など
パターンには、分解された値すべてに対して条件チェックを行うガードメソッド'If'(モジュール名)が含まれる予定です。
Case(Pattern(...).If(predicate), function)
4. ライセンス
Copyright 2014-2016 Javaslang, http://javaslang.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.