成功するまで次を試すような処理へ Either モナドを適用 - FunctionalJava
成功するまで次の処理を試していくような処理に対して Either モナドを適用してみました。
使用した環境は下記の通りです。
- Java SE 8u20
- FunctionalJava 4.2 beta1
ソースは 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要素の vector (
V2
) へ格納する
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) ・・・