blog

VAVR:Javaエクスペリエンスを破壊する

ご存知のように、Java8は関数型プログラミングをある程度サポートしていますが、標準ライブラリによって提供される関数型APIはあまり完全でフレンドリーではありません。 より良い関数型プログラミングを行...

Oct 18, 2020 · 16 min. read
シェア

それは誰ですか?

ご存知のように、Java8は関数型プログラミングをある程度サポートしていますが、標準ライブラリが提供する関数型APIはあまり完全でフレンドリーではありません。

より良い関数型プログラミングを行うためには、サードパーティのライブラリを使用する必要がありますが、VAVRはこの点で最も優れたライブラリの1つであり、コード量を効果的に削減し、コードの質を向上させることができます。

VAVR 無名ではなく、その前身は2014年にリリースされたJavaslangで、現在githubで約4kのスターを持っています。

これを読むと、多くの人が「JavaライブラリがJavaを破壊しようとしている のか?

VAVRの公式サイト 開くと、トップページには太字で"vavr - turnsjava upside down "と書かれています。

それはJavaが破壊されたということにはなりませんか?

サービングガイド

この記事を読むには、Java8のラムダ構文と一般的なAPIをある程度理解している必要があります。

これはフレームワークの入門記事なので、公式ドキュメントの翻訳として書くのを避けるために、この記事にはいくつかの制約があります。

  • これは機能やAPIを網羅したものではなく、あくまで一例です。
  • ソースコードの詳細には触れません

サンプルコードについては、基本的にユニットテストの形で提供されます。

注意: この記事はVAVRバージョン0.10.3、JDKバージョン11を使用しています。

まずは概要から。

集まれ、再出発

Java8のコレクション・ライブラリは、Streamが導入されてから本当に良くなったと言わざるを得ませんが、Streamを使うためにサンプルコードをたくさん書かなければならず、経験値がかなり減ります。

// of メソッドはJava9が提供するスタティック・ファクトリだ。
java.util.List.of(1, 2, 3, 4, 5)
 .stream()
 .filter(i -> i > 3)
 .map(i -> i * 2)
 .collect(Collectors.toList());

さらに、Javaのコレクションライブラリは本質的に変更可能であり、関数型プログラミングの基本的な特性である不変性に明らかに違反しているため、VAVRはScalaの経験に限りなく近い新しいコレクションライブラリを設計しました。

よりシンプルなAPI

io.vavr.collection.List.of(1, 2, 3, 4, 5)
 .filter(i -> i > 3)
 .map(i -> i * 2);

コレクションにデータを追加すると、新しいコレクションが作成され、そのコレクションは不変であることが保証されます。

var list = io.vavr.collection.List.of(1, 2)
var list2 = list
 .append(List.of(3, 4))
 .append(List.of(5, 6))
 .append(7);
// list = [1, 2]
// list2 = [1, 2, 3, 4, 5, 6]

強力な互換性、ライブラリのJava標準コレクションと非常に簡単に変換することができます。

var javaList = java.util.List.of(1, 2, 3);
java.util.List<Integer> javaList2 = io.vavr.collection.List.ofAll(javaList)
 .filter(i -> i > 1)
 .map(i -> i * 2)
 .toJavaList();

もう少し複雑な例を見てみましょう。成人に達したユーザーを一括でフィルタリングし、年齢でグループ分けし、各グループのユーザー名だけを表示します。

/**
* ユーザ情報
*/
@Data
class User {
 private Long id;
 private String name;
 private Integer age;
}

Java標準のコレクション・ライブラリを使用してこの要件を実装するには、collect(...) この長い入れ子のリストは本当に見づらいです。

public Map<Integer, List<String>> userStatistic(List<User> users) {
 return users.stream()
 .filter(u -> u.getAge() >= 18)
 .collect(Collectors.groupingBy(User::getAge, Collectors.mapping(User::getName, Collectors.toList())));
}

VAVRの実装を見てみましょう、より簡潔で直感的ではありませんか?

public Map<Integer, List<String>> userStatistic(List<User> users) {
 return users.filter(u -> u.getAge() >= 18)
 .groupBy(User::getAge)
 .mapValues(usersGroup -> usersGroup.map(User::getName));
}

VAVR のコレクションライブラリは以下のような、より機能的な API を提供します。

  • take(Integer) 最初の n 個の値を取ります。
  • tail() は、先頭ノード以外のセットを取得します。
  • zipWithIndex() は、(fori なしで) インデックスを取得するのに便利です。
  • .....

コード例ではListを使用していますが、上記の機能はQueue、Set、Mapでも使用でき、Java標準ライブラリで変換をサポートしています。

タプル、Javaに欠けていた構造

HaskellやScalaに慣れている人なら、「タプル」というデータ構造を知らない人はいないでしょう。

タプルは、異なる型のオブジェクトを保持する配列のようなもので、値を取得するときにキャストする必要がないように、それらの型情報を保持します。

// scala タプルは括弧で囲まれている。
val tup = (1, "ok", true)
// インデックスで値を取り、対応する型の操作を実行する。
val sum = tup._1 + 2 // int  
val world = "hello "+tup._2 // 文字列のスプライシング
val res = !tup._3 // ブール反転

もちろん、Javaにはタプルを作成するためのネイティブ構文はありませんし、標準ライブラリにもタプル関連のクラスはありません。

しかし、VAVRはジェネリックでタプルを実装しており、タプルの静的ファクトリ

import io.vavr.Tuple;
public TupleTest {
 
 @Test
 public void testTuple() {
 //  
 var oneTuple = Tuple.of("string");
 String oneTuple_1 = oneTuple._1;
 //  
 var twoTuple = Tuple.of("string", 1);
 String twoTuple_1 = twoTuple._1;
 Integer twoTuple_2 = twoTuple._2;
 	//  
 var threeTuple = Tuple.of("string", 2, 1.2F, 2.4D, 'c');
 String threeTuple_1 = threeTuple._1;
 Integer threeTuple_2 = threeTuple._2;
 Float threeTuple_3 = threeTuple._3;
 Double threeTuple_4 = threeTuple._4;
 Character threeTuple_5 = threeTuple._5;
 }
}

varがなければ、次のような長い変数定義を書かなければなりません。

Tuple5<String, Integer, Float, Double, Character> tuple5 = Tuple.of("string", 2, 1.2F, 2.4D, 'c');

現在、VAVRは最大8オクテット、つまり、最大8つの値ではなく、最大8つの型の構築をサポートしています。

タプルを「パターン・マッチ」と併用すると、さらに強力な混乱が生じます!

追記:パターン・マッチについて言及するのは少し早いですが、先行して感覚をつかむことはできます

var tup = Tuple.of("hello", 1);
// パターンマッチング
Match(tup).of(
 Case($Tuple2($(is("hello")), $(is(1))), (t1, t2) -> run(() -> {})),
 Case($Tuple2($(), $()),(t1, t2) ->run(() -> {}))
);

上記のコードは、実際には if.....else

// もし...else
if (tup._1.equalas("hello") && tup._2 == 1) {
 // ... do something
} else {
 // ... do something
}

Optionの他に、Try、Either、Future......があります。

Java8は悪名高いNullPointerExceptionを解決するために Optionalを導入しました。

Optionに加えて、VAVRはTry、Either、Futureなどの関数構造も実装しており、これらはJava標準ライブラリにはない強力なツールです。

Option

Optionは、オプショナル値を表すという点で、Java標準ライブラリのOptionalに似ていますが、両者の設計はまったく異なります。

VAVRでは、Optionはインターフェイスであり、SomeとNoneとして実装されています。

  • Some: 値を表します。
  • None: 値がないことを表します。

次のユニットテストで確認できます。

@Test
public void testOption() {
 	// ファクトリーメソッドで構築されている。
 Assert.assertTrue(Option.of(null) instanceof Option.None);
 Assert.assertTrue(Option.of(1) instanceof Option.Some);
 
 	// noneまたはいくつかの
 Assert.assertTrue(Option.none() instanceof Option.Some);
 Assert.assertTrue(Option.some(1) instanceof Option.Some);
}

また、java.util.Optionalの場合は、どのように構築されても同じ型です。

@Test
public void testOptional() {
 Assert.assertTrue(Optional.ofNullable(null) instanceof Optional);
 Assert.assertTrue(Optional.of(1) instanceof Optional);
 Assert.assertTrue(Optional.empty() instanceof Optional);
 Assert.assertTrue(Optional.ofNullable(1) instanceof Optional);
}

なぜ、このようなデザインの違いがあるのでしょうか?

これは本質的に、「Optionの目的はnullの計算を安全にすることですか」という質問に対する異なる答えです。Optionの目的はnullの計算を安全にすることですか?

以下の2つのテスト方法は、同じロジックですが、OptionとOptionalで異なる結果が得られます。

@Test
public void testWithJavaOptional() {
 // Java Optional
 var result = Optional.of("hello")
 .map(str -> (String) null)
 .orElseGet(() -> "world");
 	// result = "world"
 Assert.assertEquals("word", result);
}
@Test
public void testWithVavrOption() {
 	// Vavr Option
 var result = Option.of("hello")
 .map(str -> (String) null)
 .getOrElse(() -> "world");
 
 	// result = null
 Assert.assertNull(result);
}

Optional.of("hello") VAVRのテストコードでは、実際に.NET経由でSome("hello")オブジェクトを取得しています。

map(str -> (String)null)getOrElse(() -> "world") よって返される世界文字列ではなく、最終結果 = null を返します。

orElseGet(() -> "world") Javaのテストコードでは、map(str -> null)が呼び出されると、OptionalはOptional.emptyに切り替わるので、結局は.

この点は、関数型開発者がjava.util.MySQLの設計を批判するポイントの1つです。


@Test
public void testVavrOption() {
 	// option リストへ直接アクセスする
 List<String> result = Option.of("vavr hello world")
 .map(String::toUpperCase)
 .toJavaList();
 Assert.assertNotNull(result);
 Assert.assertEquals(1, result.size());
 Assert.assertEquals("vavr hello world", result.iterator().next());
 
 	// exists(Function)
 boolean exists = Option.of("ok").exists(str -> str.equals("ok"));
 Assert.assertTrue(exists);
 	// contains
 boolean contains = Option.of("ok").contains("ok");
 Assert.assertTrue(contains);
}

Optionは、標準ライブラリとの互換性を保つために、Optionalと簡単に交換することができます。

Option.of("toJava").toJavaOptional();
Option.ofOptional(Optional.empty());

Try

tryは、うまくいかない可能性のある振る舞いの「入れ物」であるという点で、Optionと似ています。

try {
	//..
} catch (Throwable t) {
	//...
} finally {
 //....
}

VAVR の Try を使えば、より機能的な別の try を実装することも可能です。.catch

/**
*  
*	 failure: / by zero
* finally
*/
Try.of(() -> 1 / 0)
 .andThen(r -> System.out.println("and then " + r))
 .onFailure(error -> System.out.println("failure" + error.getMessage()))
 .andFinally(() -> {
 System.out.println("finally");
 });

Try もインターフェイスで、Success または Failure として実装されています。

  • 成功:例外なく実行されたことを表します。
  • 失敗:実行中の例外を表します。

Optoinのように、of factoryメソッドで構築することもできます。

@Test
public void testTryInstance() {
 	// 失敗をビルドするには0で割る
 var error = Try.of(() -> 0 / 0);
 Assert.assertTrue(error instanceof Try.Failure);
 	// 合法的な追加、成功を築く
 var normal = Try.of(() -> 1 + 1);
 Assert.assertTrue(normal instanceof Try.Success);
}

降格戦略は、Try の recoverWith メソッドを使用してエレガントに実装できます。

@Test
public void testTryWithRecover() {
 Assert.assertEquals("NPE", testTryWithRecover(new NullPointerException()));
 Assert.assertEquals("IllegalState", testTryWithRecover(new IllegalStateException()));
 Assert.assertEquals("Unknown", testTryWithRecover(new RuntimeException()));
}
private String testTryWithRecover(Exception e) {
 return (String) Try.of(() -> {
 throw e;
 })
 .recoverWith(NullPointerException.class, Try.of(() -> "NPE"))
 .recoverWith(IllegalStateException.class, Try.of(() -> "IllegalState"))
 .recoverWith(RuntimeException.class, Try.of(() -> "Unknown"))
 .get();
}

Try の結果はマップに変換することも、Option に簡単に変換することもできます。

また、結果を変換し、Optionと対話するためにマップを使用することができます。

@Test
public void testTryMap() {
 String res = Try.of(() -> "hello world")
 .map(String::toUpperCase)
 .toOption()
 .getOrElse(() -> "default");
 Assert.assertEquals("HELLO WORLD", res);
}

Future

java.util.concurrent.Futureこのフューチャーは、非同期計算の結果を抽象化したものです。

java.util.concurrent.Future vavrのFutureは

  • onFailure 失敗コールバック
  • onSuccess 成功コールバック
@Test
public void testFutureFailure() {
 final var word = "hello world";
 io.vavr.concurrent.Future
 .of(Executors.newFixedThreadPool(1), () -> word)
 .onFailure(throwable -> Assert.fail("失敗のためにブランチするべきではない"))
 .onSuccess(result -> Assert.assertEquals(word, result));
}
@Test
public void testFutureSuccess() {
 io.vavr.concurrent.Future
 .of(Executors.newFixedThreadPool(1), () -> {
 throw new RuntimeException();
 })
 .onFailure(throwable -> Assert.assertTrue(throwable instanceof RuntimeException))
 .onSuccess(result -> Assert.fail("成功するためにブランチする必要はない。"));
}

また、JavaのCompleableFutureと交換することもできます。

Future.of(Executors.newFixedThreadPool(1), () -> "toJava").toCompletableFuture();
Future.fromCompletableFuture(CompletableFuture.runAsync(() -> {}));

その他

最後に、EitherとLazyを簡単に見てみましょう。

  • 例えば、以下のcompute()関数のEither戻り値は、ExceptionかStringのどちらかの構造体を表しています。

    正しい値は通常、右の

    public Either<Exception, String> compute() {
     //...
    }
    public void test() {
    	Either<Exception, String> either = compute();
     
     //  
     if (either.isLeft()) {
     Exception exception = compute().getLeft();
     throw new RuntimeException(exception);
     }
     //  
     if (either.isRight()) {
     String result = compute().get();
     // ...
     }
    }
    
  • Lazyもまた、初めて呼び出されるまで計算を遅らせるコンテナです。

    Lazy<Double> lazy = Lazy.of(Math::random);
     lazy.isEvaluated(); // = false
     lazy.get(); // = 0.123 (random generated)
     lazy.isEvaluated(); // = true
     lazy.get(); // = 0.123 (memoized)
    

io.vavr.APIには、ScalaのOptionとTry構造を構築する構文を模倣した静的メソッドがいくつかありますが、Javaの静的インポートと組み合わせて使用する必要があります。

import static io.vavr.API.*;
@Test
public void testAPI() {
 // 構築オプション
 var some = Some(1);
 var none = None();
 // 未来を構築する
 var future = Future(() -> "ok");
 // 試してみる
 var tryInit = Try(() -> "ok");
}

もちろん、大文字で始まる関数名はJavaのメソッド命名規則から少し外れているので、ハック戦術のようなものです。

詳しくは、公式ウェブサイトをご覧ください。

パターンマッチング:if...elseの宿敵

ここでいうパターンとは、データ構造の構成パターンのことで、Scalaではmatchキーワードで直接パターン・マッチを使うことができます

def testPatternMatch(nameOpt: Option[String], nums: List[Int]) = {
	/**
	* Optionの構造に合わせる
	*/
 nameOpt match {
 case Some(name) => println(s" $name")
 case None => println("名前なし")
 }
 /**
 * リストの構造をマッチさせる
 */
 nums match {
 case Nil => println(" ")
 case List(v) => println(s"size=1 $v")
 case List(v, v2) => println(s"size=2 $v $v2")
 case _ => println("size > 2")
 }
}

Javaにはパターン・マッチの概念がないので、当然そのための構文もありません。

しかし、VAVR は OOP を使ってパターンマッチを実装しており、Scala ネイティブほどではありませんが、かなり近いです。

Javaは JEP 375: Pattern Matching for instanceof proposalでinstanceofのパターン・マッチング機能を実装しましたが、Scalaのパターン・マッチングにはまだ程遠いと思います。

BMI値をテキスト記述にフォーマットする要件を実装するために、まずJavaの命令スタイルで

public String bmiFormat(double height, double weight) {
 double bmi = weight / (height * height);
 String desc;
 if (bmi < 18.5) {
 desc = "ちょっと不安定だ!";
 } else if (bmi < 25) {
 desc = "良い仕事を続けよう!";
 } else if (bmi < 30) {
 desc = "あなたは本当にしっかりしている!";
 } else {
 desc = " ";
 }
 return desc;
}

次に、VAVRのパターンマッチを使ってリファクタリングし、if...elseをなくしてみましょう。

構文をより使いやすくするためには、まずstatic importでAPIをインポートするのが一番です。

import static io.vavr.API.*;

以下はリファクタリングされたコードスニペットです。

public String bmiFormat(double height, double weight) {
 double bmi = weight / (height * height);
 return Match(bmi).of(
 // else if (bmi < 18.5)
 Case($(v -> v < 18.5), () -> "ちょっと不安定だ!"),
 // else if (bmi < 25)
 Case($(v -> v < 25), () -> "良い仕事を続けよう!"),
 // else if (bmi < 30)
 Case($(v -> v < 30), () -> "あなたは本当にしっかりしている!"),
 // else
 Case($(), () -> " ")
 );
}
  • Match(...)Match(...), Case(...), $, および $ はすべて io.vavr.API の静的メソッドで、 "パターンマッチング" の構文を模擬したものです。は、io.vavr.API の静的メソッドで、 "パターンマッチング "の構文をエミュレートします。

  • 最後の$()は、上記以外のすべてにマッチします。

読者が理解しやすいように、各メソッドのシグネチャーを簡単に列挙します。

public static <T> Match<T> Match(T value) {...}
public static <T, R> Case<T, R> Case(Pattern0<T> pattern, Function<? super T, ? extends R> f) {...}
public static <T> Pattern0<T> $(Predicate<? super T> predicate) {...}

of は Match オブジェクトのメソッドです。

public final <R> R of(Case<? extends T, ? extends R>... cases) {...}

ここで、もう一つの自作の文法記憶を示します。

構造を一致させる、次のいずれかに該当しない。
// Match(XXX).Of(
 - 構造はAと同じだ!
 //Case( $(A), () -> doSomethingA() ),
 
 - 構造はBと同じだ!
 //Case( $(B), () -> doSomethingB() ),
 - .....
 
 - 上記のような構造ではないが、ちょっとした作業もできる!
 //Case( $(), () -> doSomethingOthers())
 //);

パターン・マッチを前述のOption、Try、Either、Tupleと組み合わせると、1 + 1 > 3の組み合わせになります。

次のコードは、"パターン・マッチ "がOptionにどのような影響を与えるかを示しています。

import static io.vavr.API.*;
import static io.vavr.Patterns.$None;
import static io.vavr.Patterns.$Some;
public class PatternMatchTest {
 
 @Test
 public void testMatchNone() {
 // 該当なし
 var noneOpt = Option.none();
 Match(noneOpt).of(
 Case($None(), r -> {
 Assert.assertEquals(Option.none(), r);
 return true;
 }),
 Case($(), this::failed)
 );
 }
 @Test
 public void testMatchValue() {
 // SomeをNiceの値と一致させる
 var opt2 = Option.of("Nice");
 Match(opt2).of(
 Case($Some($("Nice")), r -> {
 Assert.assertEquals("Nice", r);
 return true;
 }),
 Case($(), this::failed)
 );
 }
 @Test
 public void testMatchAnySome() {
 // Some を任意の値にマッチさせる
 var opt = Option.of("hello world");
 Match(opt).of(
 Case($None(), this::failed),
 Case($Some($()), r -> {
 Assert.assertEquals("hello world", r);
 return true;
 })
 );
 }
 private boolean failed() {
 Assert.fail("このブランチは実行すべきではない。");
 return false;
 }
} 

ちなみにTryは、Caseが戻り値を持たない場合、2番目のパラメータをAPI.run()で置き換えることができます。

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.instanceOf;
public class PatternMatchTest {
 @Test
 public void testMatchFailure() {
 var res = Try.of(() -> {
 throw new RuntimeException();
 });
 Match(res).of(
 // マッチング成功
 Case($Success($()), r -> run(Assert::fail)),
 // 例外をRuntimeExceptionとしてマッチさせる
 Case($Failure($(instanceOf(RuntimeException.class))), r -> true),
 // IllegalStateExceptionのマッチング例外
 Case($Failure($(instanceOf(IllegalStateException.class))), r -> run(Assert::fail)),
 // NullPointerExceptionのマッチ例外
 Case($Failure($(instanceOf(NullPointerException.class))), r -> run(Assert::fail)),
 // 残りの不具合に対応する
 Case($Failure($()), r -> run(Assert::fail))
 );
 }
 @Test
 public void testMatchSuccess() {
 var res = Try.of(() -> "Nice");
 Match(res).of(
 // どんな成功にもマッチする
 Case($Success($()), r -> run(() -> Assert.assertEquals("Nice", r))),
 // 任意の失敗にマッチする
 Case($Failure($()), r -> run(Assert::fail))
 );
 }
}

もう一度タプルのコードに戻って見てください。タナリーのパターン・マッチを自分で書いてみることができます。

最後に

この記事では一般的な機能を紹介しただけですが、VAVRはさらに、Curring、Memoization、Partial applicationなどの高度な機能もサポートしています。

ついにこのレンガが投げられました。

広告

新しい機能を学ぶためにJava9+ベースのプロジェクトを探しているなら、 お勧めします。

これは Java11 ベースの zookeeper デスクトップクライアントで、モジュール性、var、その他多くの新機能を使用しています。

Read next

MySQLデータベースの分離レベル - 張三就活日記の基礎知識

:!@#$%......&*.前のはじけた会話はここでは省略。: 履歴書のデータベースを見てください、MySQLとOracleを使用しています、通常SQLはたくさん書いていますか?もちろん、データベースの使用はまだ非常に頻繁に、古いプロジェクトMySQLとOracleは、新しいプロジェクトはMです。

Oct 17, 2020 · 6 min read