成功するまで次を試すような処理へ Either モナドを適用 - FunctionalJava

成功するまで次の処理を試していくような処理に対して Either モナドを適用してみました。

使用した環境は下記の通りです。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140825/

はじめに

Either モナドは 2つの異なる値 (Left と Right) を扱う場合に使用し、一般的には失敗(エラー)を伴う処理に対して下記のような使い方をします。

Leftの値 Rightの値
失敗時のエラー内容(例外) 成功時の値

ただし今回は、下記のように処理が成功するまで元の値が Left へ保持されるような使い方をしてみました。

Leftの値 Rightの値
元の値 成功時の値

具体的には、下記のような処理を試します。

  • 実行時引数で指定した日付文字列に対して、Date オブジェクトへのパースが成功するまで次のパース処理を試していく

Either モナドを使わなかった場合

まずは Either モナドを使わず、普通に Java で実装してみました。

下記 (1) ~ (5) のパース処理を順に試して、例外が発生せず null では無い値を返した時点でパース処理を終了します。

  • (1) ISO-8601 タイムゾーン無しの日付文字列(例 2014-08-25T13:20:00)をパース
  • (2) ISO-8601 の日付文字列(例 2014-08-25T13:20:00+09:00)をパース
  • (3) ISO-8601 タイムゾーン付きの日付文字列(例 2014-08-25T13:20:00+09:00[Asia/Tokyo])をパース
  • (4) "yyyy-MM-dd HH:mm:ss" をパース
  • (5) "now" の場合に Date オブジェクトを返す

java.time のクラスを使う必要も無かったのですが、ついでに試してみました。

DateParse.java
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.function.Function;

public class DateParse {
    public static void main(String... args) {
        // SimpleDateFormat を使ったパース
        Function<String, Function<String, Date>> simpleDate = df -> s -> {
            try {
                return new SimpleDateFormat(df).parse(s);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };

        Date res = parseDate(
            args[0],
            s -> Date.from(LocalDateTime.parse(s).toInstant(ZoneOffset.UTC)), //(1)
            s -> Date.from(OffsetDateTime.parse(s).toInstant()), // (2)
            s -> Date.from(ZonedDateTime.parse(s).toInstant()), // (3)
            simpleDate.apply("yyyy-MM-dd HH:mm:ss"), // (4)
            s -> "now".equals(s)? new Date(): null // (5)
        );

        System.out.println("------");

        System.out.println(res);
    }

    // 日付文字列のパース
    @SafeVarargs
    public static Date parseDate(String date, Function<String, Date>... funcList) {
        for (Function<String, Date> func : funcList) {
            try {
                Date res = func.apply(date);

                if (res != null) {
                    return res;
                }
            } catch (Exception ex) {
                System.out.println("* " + ex.getMessage());
            }
        }
        return null;
    }
}
実行結果1 - (1) で成功(オフセット指定無しのため JST では +9時間される)
> java DateParse "2014-08-25T13:20:00"
------
Mon Aug 25 22:20:00 JST 2014
実行結果2 - (2) で成功
> java DateParse "2014-08-25T13:20:00+09:00"
* Text '2014-08-25T13:20:00+09:00' could not be parsed, unparsed text found at index 19
------
Mon Aug 25 13:20:00 JST 2014
実行結果3 - (3) で成功
> java DateParse "2014-08-25T13:20:00+09:00[Asia/Tokyo]"
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 19
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 25
------
Mon Aug 25 13:20:00 JST 2014
実行結果4 - (4) で成功
> java DateParse "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Mon Aug 25 13:20:00 JST 2014
実行結果5 - (5) で成功
> java DateParse "now"
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* java.text.ParseException: Unparseable date: "now"
------
Mon Aug 25 11:10:42 JST 2014
実行結果6 - 全失敗
> java DateParse "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
------
null

Either モナドを使った場合

同様の処理を FunctionalJava の Either を使って実装します。

Left にパース前の値(String)、Right にパース後の値(Date)を格納できるように Either の型を Either<String, Date> とします。

今回は Left の値 (String) に対して順次パース処理を試すようにしたいので、Either.left() で取得した Either.LeftProjection オブジェクトへパース処理を bind しています。

bind の引数には 「普通の値を取って Either を返す」 処理 (下記では F<String, Either<String, Date>>) を与える必要があるので、下記では eitherK というメソッドを定義し、通常のパース処理 (文字列を取って Date を返す F<String, Date>) を変換してから bind するようにしています。

なお、java.util.function.Function は使わず、全面的に FunctionalJava の fj.F を使うようにしました。

EitherDateParse.java
import fj.F;
import fj.data.Either;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.util.Date;

public class EitherDateParse {
    public static void main(String... args) {
        F<String, F<String, Date>> simpleDate = df -> s -> {
            try {
                return new SimpleDateFormat(df).parse(s);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };

        Either<String, Date> res = parseDate(
            Either.left(args[0]),
            s -> Date.from(LocalDateTime.parse(s).toInstant(ZoneOffset.UTC)), // (1)
            s -> Date.from(OffsetDateTime.parse(s).toInstant()), // (2)
            s -> Date.from(ZonedDateTime.parse(s).toInstant()), // (3)
            simpleDate.f("yyyy-MM-dd HH:mm:ss"), // (4)
            s -> "now".equals(s)? new Date(): null // (5)
        );

        System.out.println("------");

        System.out.println(res);
    }


    @SafeVarargs
    public static Either<String, Date> parseDate(Either<String, Date> date, F<String, Date>... funcList) {
        for (F<String, Date> func : funcList) {
            date = date.left().bind( eitherK(func) );
        }
        return date;
    }

    // F<S, T> の処理が例外発生か null であれば Left、そうでなければ Right を返す処理へ変換
    private static <S, T> F<S, Either<S, T>> eitherK(final F<S, T> func) {
        return s -> {
            try {
                T res = func.f(s);
                return (res == null)? Either.left(s): Either.right(res);
            } catch (Exception ex) {
                System.out.println("* " + ex.getMessage());
                return Either.left(s);
            }
        };
    }
}

実行結果は、Left や Right に包まれている部分を除けば Either を使わなかった場合と基本的に同じです。 (全失敗の場合は異なります)

実行結果1 - (1) で成功(オフセット指定無しのため JST では +9時間される)
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00"
------
Right(Mon Aug 25 22:20:00 JST 2014)
実行結果2 - (2) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00+09:00"
* Text '2014-08-25T13:20:00+09:00' could not be parsed, unparsed text found at index 19
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果3 - (3) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00+09:00[Asia/Tokyo]"
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 19
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 25
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果4 - (4) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果5 - (5) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "now"
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* java.text.ParseException: Unparseable date: "now"
------
Right(Mon Aug 25 11:59:01 JST 2014)
実行結果6 - 全失敗
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
------
Left(2014-08-25)

Either モナドを使った場合2 - 機能追加

次に下記のような機能を追加してみました。

  • (a) パース全失敗の場合は RuntimeException を throw する
  • (b) パース成功の場合は、その次の日の 0時 0分 0秒 の Date オブジェクトと共に 2要素の vectorV2) へ格納する

Either モナドを使えば if 文などでいちいち条件判定しなくても変換処理等を合成できるのが利点だと思います。

今回は、Either の結果出力に System.out.println() を直接使わず fj.Show.println() を使うようにしてみました。

EitherDateParse2.java
・・・
import fj.data.vector.V;
import fj.data.vector.V2;
import fj.Show;

import org.apache.commons.lang3.time.DateUtils;
・・・
public class EitherDateParse2 {
    public static void main(String... args) {
        ・・・
        Either<String, Date> res = parseDate(
            ・・・
        );

        // (a) パースが全失敗の場合は RuntimeException を throw
        res.left().bind( s -> {
            throw new RuntimeException("failed parse");
        });

        // (b) パース成功の場合は、次の日の 0時0分0秒 の結果と共に V2 へ格納して返す
        Either<String, V2<Date>> res2 = res.right().bind( d ->
            Either.right(
                V.v(d, DateUtils.truncate(DateUtils.addDays(d, 1), Calendar.DATE))
            )
        );

        System.out.println("------");

        Show.<String, V2<Date>>eitherShow(Show.anyShow(), Show.v2Show(Show.anyShow())).println(res2);
    }
    ・・・
}
実行結果 - 成功した場合
> java -cp .;functionaljava-4.2-beta-1.jar;commons-lang3-3.3.2.jar EitherDateParse2 "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Right(<Mon Aug 25 13:20:00 JST 2014,Tue Aug 26 00:00:00 JST 2014>)
実行結果 - 全失敗した場合
> java -cp .;functionaljava-4.2-beta-1.jar;commons-lang3-3.3.2.jar EitherDateParse2 "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
Exception in thread "main" java.lang.RuntimeException: failed parse
        at EitherDateParse2.lambda$main$19(EitherDateParse2.java:37)
        ・・・