覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Javaslang を試してみた

Java Advent Calendar 2016の16日目の記事です。


※追記: Javaslangは、名称がVavrに変更になりました


Javaで関数型プログラミングを行うためのライブラリとしてJavaslangが存在しています。
Javaslangは、Scalaの長所をJava言語にできる限り導入することを目的としています。

関数型プログラミング用のデータ構造や制御構造を提供してくれているようです。
関数型言語もモナドも詳しく理解していない私がとりあえず触ってみました。


利用準備

pom.xmlに以下を追加します。(今回のエントリを書くにあたってはバージョン2.1.0-alphaを使用しました)

<dependency>
    <groupId>io.javaslang</groupId>
    <artifactId>javaslang</artifactId>
    <version>{javaslang-version}</version>
</dependency>


Javaslangを使うにあたってjavaslang.APIをstatic importをしておくと何かと便利なメソッドが利用できるので、
基本的にどのサンプル試す場合も以下のimportは行っているものとします。

import static javaslang.API.*;


関数型コレクション

関数型の各種操作を提供するためのコレクションのクラス群が実装されています。

Javaslangで提供されるコレクションは、Traversableインターフェースを継承しており多数の操作を提供しています。
(基本的にどのコレクションでも共通的な操作ができるようになっています)

List / Set / Map

リテラル表現で生成

リテラル表現でのコレクションの生成を行うことができます。

■サンプルコード

import javaslang.collection.*;
import static javaslang.API.*;

// List
List<String> list1 = List.of("taro", "jiro", "hanako", "tamako");

println(list1.getClass() + " : " + list1);


// 以下のように書くこともできる
List<String> list2 = List("aa", "bb", "cc");

println(list2.getClass() + " : " + list2);


// Set
Set<Integer> set = Set(1, 2, 3, 4, 5);

println(set.getClass() + " : " + set);


// Map
Map<String, Integer> map = Map("key1", 1,"key2", 2,"key3", 3);

println(map.getClass() + " : " + map);

■実行結果

class javaslang.collection.List$Cons : List(taro, jiro, hanako, tamako)
class javaslang.collection.List$Cons : List(aa, bb, cc)
class javaslang.collection.HashSet : HashSet(1, 2, 3, 4, 5)
class javaslang.collection.HashMap : HashMap((key1, 1), (key2, 2), (key3, 3))


副作用なし

コレクションへの変更操作を行っても元のインスタンスには変化はなく、新しいコレクションが返されます。(いわゆる副作用がない状態になっています)

■サンプルコード

import javaslang.collection.*;


List<String> srcList = List.of("taro", "jiro", "hanako", "tamako");
List<String> newList = srcList.append("makoto").append("minako");

println("srcList: " + srcList);
println("newList: " + newList);

println("----------");

Set<String> srcSet = Set("taro", "jiro", "hanako", "tamako");
Set<String> newSet = srcSet.replace("hanako", "<unknown>");

println("srcSet: " + srcSet);
println("newSet: " + newSet);

println("----------");

Map<String, Integer> srcMap = Map("key1", 1,"key2", 2,"key3", 3);
Map<String, Integer> newMap = srcMap.remove("key2");

println("srcMap: " + srcMap);
println("newMap: " + newMap);

■実行結果

新しいコレクションは要素が変化しているが、元のコレクションは変化していない

srcList: List(taro, jiro, hanako, tamako)
newList: List(taro, jiro, hanako, tamako, makoto, minako)
----------
srcSet: HashSet(tamako, taro, hanako, jiro)
newSet: HashSet(tamako, taro, <unknown>, jiro)
----------
srcMap: HashMap((key1, 1), (key2, 2), (key3, 3))
newMap: HashMap((key1, 1), (key3, 3))


Stream APIを介さずに色んな操作ができる

Stream APIを介さずにListに対して豊富に提供された各種操作を実行することができます

■サンプルコード

import javaslang.collection.*;
import static javaslang.API.*;


List<String> srcList = List.of("ayaka", "taro", "jiro", "hanako", "tamako", "makoto", "ayaka");
srcList.filter(s -> s.length() >= 5)   // 長さが5以上ものにフィルタリング
        .distinct()                    // 重複除外
        .map(s -> s.toUpperCase())     // 大文字化
        .stdout();                     // 各要素を標準出力に出力する

println("-----------------");

Set<Integer> set1 = Set(1, 2, 3, 4, 5);
Set<Integer> set2 = Set(2, 3, 5, 6);

println("set1: " + set1);
println("set2: " + set2);

// コレクション同士の差分をとる
println("set1.diff(set2): " + set1.diff(set2));


println("-----------------");

Map<String, Integer> map = Map("key1", 1,"key2", 2,"key3", -1,"key4", 4);

List<String> list = map.takeUntil(tuple2 -> tuple2._2() == -1)
        .map(tuple2 -> tuple2._1() + "::" + tuple2._2())
        .toList();
println("map -> takeUntil -> toList: " + list);

■実行結果

AYAKA
HANAKO
TAMAKO
MAKOTO
-----------------
set1: HashSet(1, 2, 3, 4, 5)
set2: HashSet(2, 3, 5, 6)
set1.diff(set2): HashSet(1, 4)
-----------------
map -> takeUntil -> toList: List(key1::1, key2::2)


コレクション同士の比較

eqメソッドを使うと異なるコレクション同士でも比較ができます

■サンプルコード

import javaslang.collection.*;
import static javaslang.API.*;


List<String> srcList = List.of("taro", "jiro", "hanako", "tamako");
List<String> newList = srcList.append("makoto").append("minako");

println("srcList: " + srcList);
println("newList: " + newList);

println("----------");

Set<String> srcSet = Set("taro", "jiro", "hanako", "tamako");
Set<String> newSet = srcSet.replace("hanako", "<unknown>");

println("srcSet: " + srcSet);
println("newSet: " + newSet);

println("----------");

Map<String, Integer> srcMap = Map("key1", 1,"key2", 2,"key3", 3);
Map<String, Integer> newMap = srcMap.remove("key2");

println("srcMap: " + srcMap);
println("newMap: " + newMap);


println("-- List equals ---------");

List list1 = List(1, 2, 3, 4, 5);
List list2 = List(1, 2, 3, 4, 5, 6);
List list3 = List(1, 2, 3, 4);

println(list1.equals(List(1, 2, 3, 4, 5)));
println(list1.equals(list2));
println(list1.equals(list3));

println("-- Set equals ----------");

Set<Integer> set1 = Set(1, 2, 3, 4, 5);
Set<Integer> set2 = Set(1, 2, 3, 4, 5, 6);
Set<Integer> set3 = Set(1, 2, 3, 4);

println(set1.equals(Set(1, 2, 3, 4, 5)));
println(set1.equals(set2));
println(set1.equals(set3));

println("-- Map equals ----------");

Map<String, Integer> map1 = Map("key1", 1,"key2", 2,"key3", 3);
Map<String, Integer> map2 = Map("key1", 1,"key2", 2,"key3", 3,"key4", 4);
Map<String, Integer> map3 = Map("key1", 1,"key2", 2);

println(map1.equals(Map("key1", 1,"key2", 2,"key3", 3)));
println(map1.equals(map2));
println(map1.equals(map3));

println("-------------------------");

println("list1.equals(set1) -> " + list1.equals(set1));
println("list1.equals(set1) -> " + list1.equals(set2));
println("list1.eq(set1) -> " + list1.eq(set1));
println("list1.eq(set1) -> " + list1.eq(set2));

■実行結果

-- List equals ---------
true
false
false
-- Set equals ----------
true
false
false
-- Map equals ----------
true
false
false
-------------------------
list1.equals(set1) -> false
list1.equals(set1) -> false
list1.eq(set1) -> true
list1.eq(set1) -> false


Tuple

Tupleは一定数の要素を組み合わせて、ひとまとめにして扱うための型です。

■サンプルコード

import javaslang.Tuple;
import javaslang.Tuple3;
import static javaslang.API.*;


// ID, 名前, 生年月日を組みにして保持するTuple
Tuple3<Integer, String, LocalDate> tuple = Tuple.of(1, "Taro", LocalDate.of(1995, 12, 19));

println("src : " + tuple);

Tuple3<Integer, String, LocalDate> dest = tuple.map(
        i -> i + 10,
        s -> "Yamada " + s,
        d -> d.plusYears(20)
);

println("dest: " + dest);


// 3番目の要素だけ変換したtupleを返す
println(dest.map3(d -> d.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))));

■実行結果

src : (1, Taro, 1995-12-19)
dest: (11, Yamada Taro, 2015-12-19)
(11, Yamada Taro, 2015/12/19)


Option

Optionは値があるかもしれないし無いかもしれないものを表現するための型です。SomeまたはNoneいずれかのインスタンスになります。


Optionの生成

Option生成のためのファクトリメソッドを用いて何型の値が返されるのかを確認してみました

■サンプルコード

import javaslang.control.Option;
import static javaslang.API.*;

Option<String> opt1 = Option.of("a");
println("Option.of -> " + opt1.getClass());

Option<String> opt2 = Option.some("a");
println("Option.some -> " + opt2.getClass());

Option<String> opt3 = Option.of(null);
println("Option.of(null) -> " + opt3.getClass());

Option<String> opt4 = Option.none();
println("Option.none -> " + opt4.getClass());

Option opt5 = Option.nothing();
println("Option.nothing -> " + opt5.getClass());

■実行結果

各メソッドで生成した結果どういう型で返ってきているかは以下の通りです。
Option#nothingで生成するとSomeになるんですね。

Option.of -> class javaslang.control.Option$Some
Option.some -> class javaslang.control.Option$Some
Option.of(null) -> class javaslang.control.Option$None
Option.none -> class javaslang.control.Option$None
Option.nothing -> class javaslang.control.Option$Some


Option#nothing

Option#nothing で生成したら値としては何が取得できるのか?

■サンプルコード

import javaslang.control.Option;


Option opt = Option.nothing();

System.out.println(opt.getOrElse("unknown"));

■実行結果

null

nullが返ってきました。


Option#getOrElse と Option#getOrElseThrow

Option#getOrElse, Option#getOrElseThrow の動きの確認

■サンプルコード

import javaslang.control.Option;
import static javaslang.API.*;


Option<Integer> someOpt = Option.of(123);
Option<Integer> emptyOpt = Option.of(null);

int i1a = someOpt.getOrElse(-1);
println("someOpt.getOrElse : " + i1a);
int i1b = emptyOpt.getOrElse(-1);
println("emptyOpt.getOrElse: " + i1b);


int i2a = someOpt.getOrElse(() -> Integer.valueOf(100));
println("someOpt.getOrElse : " + i2a);
int i2b = emptyOpt.getOrElse(() -> Integer.valueOf(100));
println("emptyOpt.getOrElse: " + i2b);


try {
    int i3 = someOpt.getOrElseThrow(() -> new RuntimeException("someOpt not found"));
    println("someOpt.getOrElse : " + i3);
} catch (RuntimeException e) {
    e.printStackTrace();
}
try {
    int i3 = emptyOpt.getOrElseThrow(() -> new RuntimeException("emptyOpt not found"));
    println("emptyOpt.getOrElse: " + i3);
} catch (RuntimeException e) {
    e.printStackTrace();
}

■実行結果

値が存在する(Someの)場合保持している値が取得できる、値が存在しない(Noneの)場合は各々それ用の処理(デフォルト値を返したり、例外をスローしたり)が実行されている

someOpt.getOrElse : 123
emptyOpt.getOrElse: -1
someOpt.getOrElse : 123
emptyOpt.getOrElse: 100
someOpt.getOrElse : 123
java.lang.RuntimeException: emptyOpt not found
    at trial.app.javaslang.Trial2.lambda$main$3(Trial2.java:33)
    at javaslang.control.Option.getOrElseThrow(Option.java:266)
    at trial.app.javaslang.Trial2.main(Trial2.java:33)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)


java.util.Optionalを生成する

java.util.OptionalからOption を生成することもできる

■サンプルコード

import javaslang.control.Option;
import java.util.Optional;
import static javaslang.API.*;


// Option -> java.util.Optional
Option<String> opt = Option.ofOptional(Optional.ofNullable("abcd"));
println("Option value: " + opt.getOrElse("default1"));

// java.util.Optional -> Option
Optional<String> optional = opt.toJavaOptional();
println("Optional value: " + optional.orElse("default2"));

■実行結果

Option value: abcd
Optional value: abcd


Either

Eitherは、どちらか(Left or Right)を表現するための型です。一般的には正常値をRightに、エラーをLeftに割り当てるようです。

■サンプルコード

import javaslang.control.Either;

public class EitherTrial {

    public static void main(String[] args) {
        logic("Abcdefgh").right()
                .map(s -> s.toUpperCase())
                .forEach(System.out::println);

        logic("001").right()
                .map(s -> s.toUpperCase())
                .forEach(System.out::println);
    }

    static Either<Throwable, String> logic(String s) {
        if (s.length() < 5) {
            return Either.left(new RuntimeException("invalid string."));
        }
        return Either.right(s);
    }
}

■実行結果

一回目の処理の呼び出しは正常値が戻ってきているので、そのまま後続の処理も行われていますが、
2回目の呼び出しでは内部的にエラーが返されているので、後続の処理は行われていません。

ABCDEFGH


Lazy

Lazyは遅延評価される値を表現するための型です。

■サンプルコード

import javaslang.Lazy;
import java.time.LocalDate;
import static javaslang.API.*;

public class LazyTrial {

    public static void main(String[] args) {
        Lazy<LocalDate> lazy = Lazy.of(LazyTrial::currentDate);

        println(">>1 -----------");
        println(lazy.get());

        println(">>2 -----------");
        println(lazy.get());

        println(">>3 -----------");
        println(lazy.get());
    }

    static LocalDate currentDate() {
        println("currentDate start.");

        LocalDate now = LocalDate.now();

        println("currentDate end.");

        return now;
    }
}

■実行結果

Lazy#getが初めて呼ばれたタイミングで、中の処理が実行されて値を返します。
2回目以降の呼び出しではキャッシュされた値が返り、内部処理は実行されていません。

>>1 -----------
currentDate start.
currentDate end.
2016-12-13
>>2 -----------
2016-12-13
>>3 -----------
2016-12-13


Future

Futureは非同期の計算処理を表現するための型です。Futureに対する操作はノンブロッキングで実行されます。

■サンプルコード

import javaslang.concurrent.Future;
import java.time.LocalDateTime;
import static javaslang.API.*;

public class FutureTrial {

    public static void main(String[] args) {
        println(">> start");

        Future<String> future1 = Future.of(FutureTrial::longProcessingTimeLogic)
                .onSuccess(s -> println("onSuccess -> longProcessingTimeLogic : " + s))
                .onFailure(t -> println("onFailure -> longProcessingTimeLogic : " + t.getMessage()));

        Future<String> future2 = Future.of(FutureTrial::longProcessingTimeFailLogic)
                .onSuccess(s -> println("onSuccess -> longProcessingTimeFailLogic : " + s))
                .onFailure(t -> println("onFailure -> longProcessingTimeFailLogic : " + t.getMessage()));

        println(">> executing...");

        println("longProcessingTimeLogic result: [" + future1.getOrElse("empty") + "]");
        println("longProcessingTimeFailLogic result: [" + future2.getOrElse("empty") + "]");

        println(">> end");
    }


    static String longProcessingTimeLogic() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return "success: " + LocalDateTime.now().toString();
    }

    static String longProcessingTimeFailLogic() {
        try {
            Thread.sleep(3500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        throw new RuntimeException("error logic");
    }
}

■実行結果

>> start
>> executing...
onFailure -> longProcessingTimeFailLogic : error logic
onSuccess -> longProcessingTimeLogic : success: 2016-12-15T19:50:30.447
longProcessingTimeLogic result: [success: 2016-12-15T19:50:30.447]
longProcessingTimeFailLogic result: [empty]
>> end


Promise

Promiseは成功あるいは失敗を表す値を一度だけ設定して、その値をFutureに渡すことができます。
PromiseFutureは以下のような役割分担になるようです。

  • Future ・・・ いつか誰かが値を渡してくれることを待っている型
  • Promise ・・・ いつか値を設定する型

■サンプルコード

import javaslang.concurrent.Future;
import javaslang.concurrent.Promise;
import static javaslang.API.*;


Promise<String> promise1 = Promise.make();

// いつか値が来るのを待っている
Future<String> future1 = promise1.future()
        .onSuccess(s -> println("success: " + s))
        .onFailure(t -> println("failed: " + t.getMessage()));

// 成功を意味する値をセット
promise1.success("1st try OK.");

Promise<String> promise2 = Promise.make();

// いつか値が来るのを待っている
Future<String> future2 = promise2.future()
        .onSuccess(s -> println("success: " + s))
        .onFailure(t -> println("failed: " + t.getMessage()));

// 失敗を意味する値をセット
promise2.failure(new RuntimeException("2nd try NG."));

■実行結果

Promiseがセットし値を、Promiseによって生み出されたFutureが処理していることが分かります

success: 1st try OK.
failed: 2nd try NG.


Try

Tryは正常な値を返すかもしれないし、エラーになるかもしれない処理を表現するための型です。
TryのインスタンスはSuccessまたはFailureのいずれかのインスタンスになります。

■サンプルコード

import javaslang.control.Option;
import javaslang.control.Try;
import lombok.extern.slf4j.Slf4j;

import static javaslang.API.*;


@Slf4j
public class TrySample {

    public static void main(String[] args) {
        int result1 = Try.of(() -> logic("123"))
                .getOrElse(-1);

        println("success result: " + result1);

        int result2 = Try.of(() -> logic(null))
                .getOrElse(-1);

        println("failure result: " + result2);

        println("--------------------------");;

        String param1 = "normal-value";

        Try.of(() -> logic(param1))
                .onSuccess(i -> log.info("length: {}", i))
                .onFailure(e -> log.error("error {}", e.getMessage()));


        String param2 = null;

        Try.of(() -> logic(param2))
                .onSuccess(i -> log.info("length: {}", i))
                .onFailure(e -> log.error("error {}", e.getMessage()));
    }

    // nullを引数に渡すと例外をスローするメソッド
    static int logic(String value) {
        return Option.of(value)
                .getOrElseThrow(() -> new NumberFormatException("value is null"))
                .length();
    }
}

■実行結果

success result: 3
failure result: -1
--------------------------
2016/12/13 20:18:49.627 INFO  [main] t.a.j.TrySample length: 12
2016/12/13 20:18:49.629 ERROR [main] t.a.j.TrySample error value is null


パターンマッチ

Javaslangでは、パターンマッチの機構をMatchCaseというstaticメソッドで提供しています。


整数値をパターンマッチで振り分ける

以下は整数値を受け取ってパターンマッチ用いて処理を振り分ける例です

■サンプルコード

import static javaslang.API.*;

int i1 = 1;

String s1 = Match(i1).of(
        Case($(1), "one"),
        Case($(2), "two"),
        Case($(), n -> n.toString())
);

println("s1: " + s1);

int i2 = 0;

String s2 = Match(i2).of(
        Case($(1), "one"),
        Case($(2), "two"),
        Case($(), n -> n.toString())
);

println("s2: " + s2);

■実行結果

s1: one
s2: 0


Optionをパターンマッチで振り分ける

Optionをパターンマッチを用いて振り分ける例です

■サンプルコード

import javaslang.control.Option;

import static javaslang.API.Match;
import static javaslang.API.Case;
import static javaslang.API.$;
import static javaslang.API.println;
import static javaslang.Patterns.*;


Option<String> option1 = Option.of("test");

String result1 = Match(option1).of(
        Case(Some($()), s -> s.toUpperCase()),
        Case(None(), "empty")
);

println("try1: " + result1);


Option<String> option2 = Option.none();

String result2 = Match(option2).of(
        Case(Some($()), s -> s.toUpperCase()),
        Case(None(), "empty")
);

println("try2: " + result2);

■実行結果

値がある場合は大文字化され、値が無ければ"empty"が返されています

try1: TEST
try2: empty


Tryをパターンマッチで振り分ける

Tryの結果をパターンマッチを用いて振り分ける例です

■サンプルコード

import javaslang.control.Option;
import javaslang.control.Try;

import static javaslang.API.Match;
import static javaslang.API.Case;
import static javaslang.API.$;
import static javaslang.API.println;
import static javaslang.Patterns.*;


public class MatchTrial {

    public static void main(String[] args) {
        Try<Integer> tryLogic1 = Try.of(() -> logic("data1"));

        Option<Integer> logicResult1 = Match(tryLogic1).of(
                Case(Success($()), v -> Option.of(v)),
                Case(Failure($()), e -> Option.none())
        );

        println("logicResult1: " + logicResult1.getOrElse(-1));

        Try<Integer> tryLogic2 = Try.of(() -> logic(null));

        Option<Integer> logicResult2 = Match(tryLogic2).of(
                Case(Success($()), v -> Option.of(v)),
                Case(Failure($()), e -> Option.none())
        );

        println("logicResult2: " + logicResult2.getOrElse(-1));
    }

    // nullを渡すと例外をスローする
    static int logic(String value) {
        return Option.of(value)
                .getOrElseThrow(() -> new NumberFormatException("value is null"))
                .length();
    }
}

■実行結果

処理が失敗した場合は値が-1になっています

logicResult1: 5
logicResult2: -1


関数の合成(Composition)

複数の関数を合成して1つの関数を作ることができます。

サンプルでは引数String, 戻り値Stringの以下の関数を合成してみます。

  • 空白を除去する関数
  • 大文字化する関数
  • 現在日を先頭に付与する関数

■サンプルコード

import javaslang.Function1;
import java.time.LocalDate;
import static javaslang.API.*;


// 空白除去の関数
Function1<String, String> trimFunc = s -> s.trim();
// 大文字化の関数
Function1<String, String> upper = s ->  s.toUpperCase();
// 先頭に現在日を付与する関数
Function1<String, String> headToCurrentDate = s -> LocalDate.now() + " : " + s;

// andThenで合成
Function1<String, String> compFunc1 = trimFunc.andThen(upper).andThen(headToCurrentDate);
println(compFunc1.apply("sample_string  "));

// compose
Function1<String, String> compFunc2 = trimFunc.compose(upper).compose(headToCurrentDate);
println(compFunc2.apply("Hello, World!      "));

■実行結果

2016-12-14 : SAMPLE_STRING
2016-12-14 : HELLO, WORLD!


Lifting

対象の関数の戻り値をOptionに入れて返す関数にするのがLiftingです。

引数の文字列がnullまたは空の場合にIllegalArgumentExceptionをスローする関数をLiftingしてみます

■サンプルコード

Function1#liftで対象の処理(trimAndToUpper)をLiftingしています。

import javaslang.Function1;
import javaslang.control.Option;

import static javaslang.API.*;


public class LiftTrial {

    public static void main(String[] args) {
        Function1<String, Option<String>> liftedFunc = Function1.lift(LiftTrial::trimAndToUpper);

        // 正常なデータパターン
        String result1 = liftedFunc.apply("normal data    ")
                .getOrElse("unknown");
        println("result1: " + result1);

        // エラー扱いのデータパターン
        String result2 = liftedFunc.apply("")
                .getOrElse("unknown");
        println("result2: " + result2);
    }

    // 引数がnullまたは空ならIllegalArgumentExceptionをスローする
    static String trimAndToUpper(String src) {
        if (src == null || src.isEmpty()) {
            throw  new IllegalArgumentException("param is empty");
        }

        return "[" + src.trim().toUpperCase() + "]";
    }
}

■実行結果

正常系の場合はSomeが返り、エラーの場合はNoneが返っていることが分かります

result1: [NORMAL DATA]
result2: unknown


カリー化

引数のいくつかを固定して関数を部分適用するというのがカリー化です。

f = ax + yという関数を定義して、このaの部分に定数値をセットしてカリー化してみます。

■サンプルコード

import javaslang.Function1;
import javaslang.Function3;
import static javaslang.API.*;


// y = a * x + y;
Function3<Integer, Integer, Integer, Integer> sum3 = (a, x, y) -> a * x + y;

// y = 10x + y (aの部分を10で固定してカリー化)
Function1<Integer, Function1<Integer, Integer>> f1 = sum3.curried().apply(10);

// y = 10 * 5 + 20
int result1 = f1.apply(5).apply(20);

println("f1 result = " + result1);


// y = 99x + y (aの部分を99で固定してカリー化)
Function1<Integer, Function1<Integer, Integer>> f2 = sum3.curried().apply(99);

// y = 99 * 5 + 20
int result2 = f2.apply(5).apply(20);

println("f2 result = " + result2);

■実行結果

f1 result = 70
f2 result = 515


メモ化

メモ化された関数は一度だけ評価され、その後は同一のパラメータに対する関数呼び出しに関してはキャッシュから値が返されます。

1秒かけて足し算する関数をメモ化してみます。

■サンプルコード

slowFunctionが1秒かかって足し算を行うメソッドです。このメソッドをメモ化して呼び出しています。
実行時の処理経路が分かるようにログを各所で出力するようにしています。

import javaslang.Function2;
import lombok.extern.slf4j.Slf4j;

import static javaslang.API.*;

@Slf4j
public class MemoTrial {

    public static void main(String[] args) {
        Function2<Integer, Integer, Integer> sum = MemoTrial::slowFunction;

        // メモ化
        Function2<Integer, Integer, Integer> memoizedSum = sum.memoized();

        log.info("{} ", ">>1st 1 + 2  -----------");
        // 1 + 2
        int result1 = memoizedSum.apply(1).apply(2);
        log.info("1st: {} + {} = {}", 1, 2, result1);

        log.info("{} ", ">>2nd 1 + 2  -----------");

        // 1 + 2
        int result2 = memoizedSum.apply(1).apply(2);
        log.info("2nd: {} + {} = {}", 1, 2, result2);

        log.info("{} ", ">>1st 100 + 21  -----------");
        // 100 + 21
        int result3 = memoizedSum.apply(100).apply(21);
        log.info("1st: {} + {} = {}", 100, 21, result3);
    }

    static int slowFunction(int a, int b) {
        log.info("{} param: {} and {}", "logic start.", a, b);

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.info("{} ", "logic end.");

        return a + b;
    }
}

■実行結果

パラメータに1と2を指定した呼び出しは初回は遅いですが、2回目は即時に終わっていることが分かります。
新しいパラメータの100と21で呼び出すと、また時間がかかって結果が返っていてることが分かります。

2016/12/14 19:07:50.324 INFO  [main] t.a.j.MemoTrial >>1st 1 + 2  ----------- 
2016/12/14 19:07:50.331 INFO  [main] t.a.j.MemoTrial logic start. param: 1 and 2
2016/12/14 19:07:51.331 INFO  [main] t.a.j.MemoTrial logic end. 
2016/12/14 19:07:51.331 INFO  [main] t.a.j.MemoTrial 1st: 1 + 2 = 3
2016/12/14 19:07:51.331 INFO  [main] t.a.j.MemoTrial >>2nd 1 + 2  ----------- 
2016/12/14 19:07:51.331 INFO  [main] t.a.j.MemoTrial 2nd: 1 + 2 = 3
2016/12/14 19:07:51.332 INFO  [main] t.a.j.MemoTrial >>1st 100 + 21  ----------- 
2016/12/14 19:07:51.332 INFO  [main] t.a.j.MemoTrial logic start. param: 100 and 21
2016/12/14 19:07:52.332 INFO  [main] t.a.j.MemoTrial logic end. 
2016/12/14 19:07:52.332 INFO  [main] t.a.j.MemoTrial 1st: 100 + 21 = 121


まとめ

  • 色々理解できていない私にもJavaslang(Vavr)がかなり高機能な感じであることは分かりました。
  • JavaslangのGitHub等にも書いてありますが、Java自体を拡張しようとしている感じがヒシヒシと伝わってきます。
  • 今回試していないですが、Seq, Queue, Vector, Tree, Array, BitSet, Validation, Stream...等も存在しています
  • 2017年にバージョン3.0が出るようです。かなり楽しみです。


私自身は、関数型プログラミングを理解するためにも以下の辺りの本読もうかと思います

[増補改訂]関数プログラミング実践入門 ──簡潔で、正しいコードを書くために (WEB+DB PRESS plus)

[増補改訂]関数プログラミング実践入門 ──簡潔で、正しいコードを書くために (WEB+DB PRESS plus)

Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)

Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)



関連リンク