Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz
Last active July 2, 2021 08:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gakuzzzz/0c779d5335f4b2bff596 to your computer and use it in GitHub Desktop.
Save gakuzzzz/0c779d5335f4b2bff596 to your computer and use it in GitHub Desktop.
Java8 と fugue で Validation (エムスリー Advent Calendar 2015 18th)

Java8 と fugue で Validation

この記事は エムスリー Advent Calendar 2015 の18日目の記事です。

まえがき

Java8 で色々便利になりました。でもエラー処理でやっぱり困る事は多々あって、そういう時に Either が欲しくなるものです。

Either といえば Java7 以前から Atlassian の fugue というライブラリがサポートしていました。

以前は Google の Guava をサポートする形で提供されていたのですが、今は Java8 の標準 API をベースに Guava 対応版はオプションとして提供されている形になっています。

で、fugueEither を使えば良いのですが、実際の業務アプリケーションの場合、単純な Either よりも、エラーを集約できる、Scalaz で言う所の Validation のような形で使う事の方が多いように感じます。

そこで fugue の Either を利用して、 Validation のようなエラーの集約ができる仕組みを作ってみようという話です。

Validation とは

Validation を端的に言うと、型引数 L が連結できるという制約をもっている Either<L, R> のことです。

ちょっと意味不明ですね。コードで説明しましょう。

仮に「連結できる」という性質を Java で表現しようとすると java.lang.Comparable に似たような interface を定義するのが一般的かと思います。

public interface Appendable<A> {
    A append(A other);
}

この Appendable があったとした時に、Either の Left の型が A extends Appendable<A> になっている、つまり Either<A extends Appendable<A>, B> の事を指します。

なのですが、実際の所エラーとして集約したいのは List<String> とかだったりするわけです。

標準API の java.util.List に後から自作の interface を implements させる事できません。

そこで思い出して下さい、java.lang.Comparable を implements していない既存クラスを比較したい時どうするでしょうか?一般的には java.util.Comparator を利用すると思います。

同じようにこの Appendable に対応する Comparator のような interface があれば良いと思いませんか?

そして都合のいい事に、fugue の 3.1 からそういう interface が提供されています

定義を切り出すと以下の様になります。

public interface Semigroup<A> {
  A append(A a1, A a2);

  ... default method 省略

この Semigroup のインスタンスを持つ型に対して Either を操作するクラスを提供できれば、やりたい事が実現できそうです。

実装

まず、Semigroup<A> を field に持つ Validation<A> クラスを定義します。

package com.m3.advent.validation;

import io.atlassian.fugue.Semigroup;

public class Validation<A> {

    private final Semigroup<A> MA;

    public Validation(final Semigroup<A> MA) {
        this.MA = MA;
    }

}

このクラスに Either<A, ???> に対する操作を加えて行く形になります。

まず apply2 を定義しましょう。

package com.m3.advent.validation;

import java.util.function.BiFunction;
import java.util.function.Function;
import io.atlassian.fugue.Either;
import io.atlassian.fugue.Semigroup;

public class Validation<A> {

    private final Semigroup<A> MA;

    public Validation(final Semigroup<A> MA) {
        this.MA = MA;
    }

    public <B1, B2, C> Either<A, C> apply2(
            final Either<? extends A, ? extends B1> e1,
            final Either<? extends A, ? extends B2> e2,
            final BiFunction<? super B1, ? super B2, ? extends C> f) {
        return e1.fold(
            l1 -> Either.left(e2.fold(l2 -> MA.append(l1, l2), ignore -> l1)),
            r1 -> e2.bimap(Function.identity(), r2 -> f.apply(r1, r2))
        );
    }

いきなりコードを見ると何が何やらですが、やりたいことは、Either<A, B1>Either<A, B2> を受け取って、両方が Right だった場合に BiFunction<B1, B2, C> を実行し、Either<A, C> を得る、という事です。どちらかが Left だった場合は結果が Left になります。

そしてポイントは、両方が Left だった場合、Semigroup<A> を使って両方の値を連結し、その結果を Left とする所です。

で、二つだけの合成だと不便なので、apply3 とかも定義したい所です。ただ残念ながら、BiFunction より引数の多い interface は標準APIでは提供されていないので、自前で用意していまいましょう。

package com.m3.advent.validation;

public class FunctionN {

    private FunctionN() {}

    @FunctionalInterface
    public static interface Function3<A1, A2, A3, R> {
        R apply(final A1 a1, final A2 a2, final A3 a3);
    }

    @FunctionalInterface
    public static interface Function4<A1, A2, A3, A4, R> {
        R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4);
    }

    @FunctionalInterface
    public static interface Function5<A1, A2, A3, A4, A5, R> {
        R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4, final A5 a5);
    }

    ... 以下好きなだけ

}

Function22 ぐらいまであれば十分じゃないですかね?

これらの interface と Pair というクラスを使えば、 apply3 以降は全て apply2 を使って実装することができます。

package com.m3.advent.validation;

import com.m3.advent.validation.FunctionN.*;

import java.util.function.BiFunction;
import java.util.function.Function;
import io.atlassian.fugue.Either;
import io.atlassian.fugue.Pair;
import io.atlassian.fugue.Semigroup;

public class Validation<A> {

    private final Semigroup<A> MA;

    public Validation(final Semigroup<A> MA) {
        this.MA = MA;
    }

    public <B1, B2, C> Either<A, C> apply2(
            final Either<? extends A, ? extends B1> e1,
            final Either<? extends A, ? extends B2> e2,
            final BiFunction<? super B1, ? super B2, ? extends C> f) {
        return e1.<Either<A, C>>fold(
            l1 -> Either.<A, C>left(e2.fold(l2 -> MA.append(l1, l2), ignore -> l1)),
            r1 -> e2.<A, C>bimap(l2 -> l2, r2 -> f.apply(r1, r2))
        );
    }

    public <B1, B2, B3, C> Either<A, C> apply3(
            final Either<? extends A, ? extends B1> e1,
            final Either<? extends A, ? extends B2> e2,
            final Either<? extends A, ? extends B3> e3,
            final Function3<? super B1, ? super B2, ? super B3, ? extends C> f) {
        return apply2(
            this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair), 
            e3, 
            (t, r3) -> f.apply(t.left(), t.right(), r3)
        );
    }

    public <B1, B2, B3, B4, C> Either<A, C> apply4(
            final Either<? extends A, ? extends B1> e1,
            final Either<? extends A, ? extends B2> e2,
            final Either<? extends A, ? extends B3> e3,
            final Either<? extends A, ? extends B4> e4,
            final Function4<? super B1, ? super B2, ? super B3, ? super B4, ? extends C> f) {
        return apply2(
            this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
            this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
            (t1, t2) -> f.apply(t1.left(), t1.right(), t2.left(), t2.right())
        );
    }

    public <B1, B2, B3, B4, B5, C> Either<A, C> apply5(
            final Either<? extends A, ? extends B1> e1,
            final Either<? extends A, ? extends B2> e2,
            final Either<? extends A, ? extends B3> e3,
            final Either<? extends A, ? extends B4> e4,
            final Either<? extends A, ? extends B5> e5,
            final Function5<? super B1, ? super B2, ? super B3, ? super B4, ? super B5, ? extends C> f) {
        return apply3(
            this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
            this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
            e5,
            (t1, t2, r3) -> f.apply(t1.left(), t1.right(), t2.left(), t2.right(), r3)
        );
    }

    ... 以下好きなだけ

Validationインスタンスの生成

ここまで作った所で、この Validation のインスタンスを作るヘルパーを用意しましょう。

package com.m3.advent.validation;

public class Validations {

    public static <A> Validation<A> validation(final Semigroup<A> MA) {
        return new Validation<A>(MA);
    }
}

で、実際の所エラーを集約するの大抵 java.util.List だったりしますよね。

なのでそのショートカットも用意しておきましょう。その為には Semigroup<List<A>> が必要になります。

ついでなので Validation<List<String>> まで定義しちゃいましょう。

package com.m3.advent.validation;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.atlassian.fugue.Semigroup;

public class Validations {

    public static <A> Validation<A> validation(final Semigroup<A> MA) {
        return new Validation<A>(MA);
    }

    private static <A> Semigroup<List<A>> listSemigroup() {
        return (a, b) -> Stream.concat(a.stream(), b.stream()).collect(Collectors.toList());
    }

    public static <A> Validation<List<A>> validationList() {
        return validation(listSemigroup());
    }

    private static final Validation<List<String>> LIST_STRING = validationList();

    public static Validation<List<String>> validationListString() {
        return LIST_STRING;
    }

}

使ってみる

具体的なユースケースを例に使用例を見ていきましょう。

CSVの読み込んでEntityに変換するよくあるケースです。

以下の様な Entity と value object があるとします。

class ID<A> {
    private final String value;
    public static <AA> ID<AA> of(final String value) {
        return new ID<AA>(value);
    }
    // コンストラクタ、toString/equals/hasCode 省略
}

class Name<A> {
    private final String value;
    public static <AA> Name<AA> of(final String value) {
        return new Name<AA>(value);
    }
    // コンストラクタ、toString/equals/hasCode 省略
}

class Workplace {
   全部省略
}

class Doctor {
    private final ID<Doctor> id;
    private final Name<Doctor> name;
    private final Name<Workplace> workplaceName;
    // コンストラクタ、toString/equals/hasCode 省略
}

これを1行分の String をうけとって精査と変換を行います。変換中に見つかったエラーは全て出力する必要があります。

つまり以下の様なメソッドを作りたいわけです。

package com.m3.advent.validation;

import java.util.List;
import io.atlassian.fugue.Either;

public class Example {

    public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
        // TODO
    }
}

まず、いきなり全体の validation を行うのではなく、個々の field に注目します。

最もチェックが単純な name と workplaceName について考えます。この二つは必須チェックだけです。

従って、空文字でなければOKという事ですね。つまり以下の様になります。

package com.m3.advent.validation;

import java.util.Collections;
import java.util.List;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;

public class Example {

    private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
        return StringUtils.isNotBlank)
            ? Either.right(Name.of(input))
            : Either.left(Collections.singletonList(String.format("%d行目: 氏名は必ず入力してください", lineNo)));
    }

    private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
        return StringUtils.isNotBlank)
            ? Either.right(Name.of(input))
            : Either.left(Collections.singletonList(String.format("%d行目: 勤務先は必ず入力してください", lineNo)));
    }

    public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
        // TODO
    }
}

ほぼ一緒ですね。そして必須チェックはIDでも使いそうです。ここは共通化してみましょう。

package com.m3.advent.validation;

import java.util.Collections;
import java.util.List;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;

public class Example {

    private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
        return StringUtils.isNotBlank(input)
            ? Either.right(input)
            : Either.left(Collections.singletonList(String.format("%d行目: %sは必ず入力してください", lineNo, fieldName)));
    }

    private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "氏名", input).map(Name::of);
    }

    private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "勤務先", input).map(Name::of);
    }

    public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
        // TODO
    }
}

次に ID ですが、 IDは必須チェックをパスしたら数字だけで構成された文字かどうか精査する必要があります。

package com.m3.advent.validation;

import java.util.Collections;
import java.util.List;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;

public class Example {

    private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
        return StringUtils.isNotBlank(input)
            ? Either.right(input)
            : Either.left(Collections.singletonList(String.format("%d行目: %sは必ず入力してください", lineNo, fieldName)));
    }

    private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
        return nonEmpty(lineNo, "ID", input)
            .flatMap(i -> StringUtils.isNumeric(input)
                ? Either.right(input)
                : Either.left(Collections.singletonList(String.format("%d行目: IDは数字を入力してください", lineNo))))
            .map(ID::of);
    }

    private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "氏名", input).map(Name::of);
    }

    private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "勤務先", input).map(Name::of);
    }

    public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
        // TODO
    }
}

よさそうですが、数時チェックのネストが読みにくいですね。汎用化できそうですし、括りだしてみましょう。

package com.m3.advent.validation;

import java.util.Collections;
import java.util.List;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;

public class Example {

    private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
        return StringUtils.isNotBlank(input)
            ? Either.right(input)
            : Either.left(Collections.singletonList(String.format("%d行目: %sは必ず入力してください", lineNo, fieldName)));
    }

    private Either<List<String>, String> numeric(final int lineNo, final String fieldName, final String input) {
        return StringUtils.isNumeric(input)
            ? Either.right(input)
            : Either.left(Collections.singletonList(String.format("%d行目: %sは数字を入力してください", lineNo, fieldName)));
    }

    private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
        return nonEmpty(lineNo, "ID", input)
            .flatMap(i -> numeric(lineNo, "ID", i))
            .map(ID::of);
    }

    private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "氏名", input).map(Name::of);
    }

    private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "勤務先", input).map(Name::of);
    }

    public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
        // TODO
    }
}

良く見ると nonEmptynumeric もそっくりですね。これも共通化しちゃいましょう。

package com.m3.advent.validation;

import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;

public class Example {

    private <A, B> Either<List<A>, B> validate(B b, Predicate<? super B> f, Supplier<? extends A> error) {
        return f.test(b) 
          ? Either.right(b) 
          : Either.left(Collections.singletonList(error.get()));
    }

    private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
        return validate(input, StringUtils::isNotBlank, 
            () -> String.format("%d行目: %sは必ず入力してください", lineNo, fieldName));
    }

    private Either<List<String>, String> numeric(final int lineNo, final String fieldName, final String input) {
        return validate(input, StringUtils::isNumeric, 
            () -> String.format("%d行目: %sは数字を入力してください", lineNo, fieldName));
    }

    private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
        return nonEmpty(lineNo, "ID", input)
            .flatMap(i -> numeric(lineNo, "ID", i))
            .map(ID::of);
    }

    private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "氏名", input).map(Name::of);
    }

    private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "勤務先", input).map(Name::of);
    }

    public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
        // TODO
    }

}

ここまでくれば後は validateDoctor は簡単ですね。

package com.m3.advent.validation;

import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;

public class Example {

    private <A, B> Either<List<A>, B> validate(B b, Predicate<? super B> f, Supplier<? extends A> error) {
        return f.test(b) 
          ? Either.right(b) 
          : Either.left(Collections.singletonList(error.get()));
    }

    private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
        return validate(input, StringUtils::isNotBlank, 
            () -> String.format("%d行目: %sは必ず入力してください", lineNo, fieldName));
    }

    private Either<List<String>, String> numeric(final int lineNo, final String fieldName, final String input) {
        return validate(input, StringUtils::isNumeric, 
            () -> String.format("%d行目: %sは数字を入力してください", lineNo, fieldName));
    }

    private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
        return nonEmpty(lineNo, "ID", input)
            .flatMap(i -> numeric(lineNo, "ID", i))
            .map(ID::of);
    }

    private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "氏名", input).map(Name::of);
    }

    private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
        return nonEmpty(lineNo, "勤務先", input).map(Name::of);
    }

    private Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
        return validate(line.split(",", -1), csv -> csv.length >= 3, () -> String.format("%d行目: 項目の数が足りません", lineNo))
            .flatMap(csv -> Validations.validationListString().apply3(
                validateId(lineNo, csv[0]),
                validateName(lineNo, csv[1]),
                validateWorkplaceName(lineNo, csv[2]),
                Doctor::new)
            );
    }
}

traverse と sequence

さて、上記までのコードで、1件のレコードを変換する処理はできました。

あとは List<String> から Either<List<String>, List<Doctor>> を作る処理ができれば完璧ですね。

こういう時に便利なのが traverse もしくは sequence と呼ばれる操作です。

具体的にコードを見て見ましょう。

    public <B, R> Either<A, List<R>> traverse(final List<B> list, final Function<? super B, Either<A, R>> f) {
         // TODO
    }
    public <R> Either<A, List<R>> sequence(final List<Either<A, R>> list) {
        return traverse(list, Function.identity());
    }

traverseList<B>B から Either<A, R> に変換する関数を受け取って、最終的に Either<A, List<R>> を返す、という処理です。

sequenceList<Either<A, R>>Either<A, List<R>> にする処理です。List と Either が入れ替わってるのがわかるでしょうか。

難しそうな操作に見えますが、実際の所 sequence は単に traverse の関数に恒等関数を渡してるだけになります。

この traversesequenceListEither だけでなく、ListOptionalOptionalEither など、ある特定の性質を満たすクラスについて抽象化して定義する事が可能なんですが、Javaでそれを実現するには色々と工夫する必要があり、それはそれで大変なので、抽象化してDRYにすることは諦めて愚直に個別に定義していきます。

愚直に定義するため、型毎に名前も分ける事にしましょう。

    public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
         // TODO
    }
    public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
        return traverseList(list, Function.identity());
    }
    public <B, R> Either<A, Optional<R>> traverseOptional(final Optional<B> opt, final Function<? super B, Either<A, R>> f) {
         // TODO
    }
    public <R> Either<A, Optional<R>> sequenceOptional(final Optional<Either<A, R>> opt) {
        return traverseOptional(opt, Function.identity());
    }

ところで、単純に traverseList を実装してもいいんですが、せっかくなので java.util.stream.Stream に対応する traverseStream も定義しておきたいですよね。それがあれば traverseList の実装も簡単そうですし。

    public <B, R> Either<A, Stream<R>> traverseStream(final Stream<B> stream, final Function<? super B, Either<A, R>> f) {
         // TODO
    }
    public <R> Either<A, Stream<R>> sequenceStream(final Stream<Either<A, R>> stream) {
        return traverseStream(stream, Function.identity());
    }
    public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
        return traverseStream(list.stream(), f).map(s -> s.collect(Collectors.toList()));
    }
    public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
        return traverseList(list, Function.identity());
    }
    // Optional は省略

しかし、Stream を引数で受け取って処理するメソッドを定義するのは推奨できません。なぜかというと、中間操作にしろ終端操作にしろ、Stream に何か操作を加えると、その Stream の状態が変更され利用できなくなります。つまり、引数で受け取ったオブジェクトの状態を破壊的に変更する困ったメソッドになってしまうのです。

そこで、traverse と同じ処理を行う Collector を作ることにしましょう。

    public <B, R> Collector<B, ?, Either<A, Stream<R>>> traverseCollector(final Function<? super B, Either<A, R>> f) {
        // TODO
    }
    public <R> Collector<Either<A, R>, ?, Either<A, Stream<R>>> sequenceCollector() {
        return traverseCollector(Function.identity());
    }
    public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
        return list.stream().collect(traverseCollector(f)).map(s -> s.collect(Collectors.toList()));
    }
    public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
        return traverseList(list, Function.identity());
    }

シグネチャは上記のような感じになります。

そして出来上がった実装がこちら。import も含めて全部載せます。

package com.m3.advent.validation;

import static java.util.stream.Collector.Characteristics.CONCURRENT;
import io.atlassian.fugue.Either;
import com.m3.advent.validation.FunctionN.*;
import io.atlassian.fugue.Pair;
import io.atlassian.fugue.Semigroup;
import io.atlassian.fugue.Semigroups;

import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Validation<A> {

    private final Semigroup<A> MA;

    public Validation(final Semigroup<A> MA) {
        this.MA = MA;
    }

    public <B1, B2, C> Either<A, C> apply2(
            final Either<? extends A, ? extends B1> e1,
            final Either<? extends A, ? extends B2> e2,
            final BiFunction<? super B1, ? super B2, ? extends C> f) {
        return e1.<Either<A, C>>fold(
            l1 -> Either.<A, C>left(e2.fold(l2 -> MA.append(l1, l2), ignore -> l1)),
            r1 -> e2.<A, C>bimap(l2 -> l2, r2 -> f.apply(r1, r2))
        );
    }

    // apply3 以降は省略

    public <B, R> Collector<B, AtomicReference<Either<A, Stream<R>>>, Either<A, Stream<R>>> traverseCollector(final Function<? super B, Either<A, R>> f) {
        final Semigroup<Stream<R>> rSemi = Stream::concat;
        final Semigroup<Either<A, Stream<R>>> vSemi = Semigroups.either(MA, rSemi);
        return Collector.<B, AtomicReference<Either<A, Stream<R>>>, Either<A, Stream<R>>>of(
            ()       -> new AtomicReference<>(Either.right(Stream.empty())),
            (acc, e) -> acc.accumulateAndGet(f.apply(e).map(Stream::of), (a, b) -> apply2(a, b, rSemi::append)),
            (a, b)   -> {a.accumulateAndGet(b.get(), vSemi::append); return a;},
            AtomicReference::get,
            CONCURRENT
        );
    }
    public <R> Collector<Either<A, R>, AtomicReference<Either<A, Stream<R>>>, Either<A, Stream<R>>> sequenceCollector() {
        return traverseCollector(Function.identity());
    }

    public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
        return list.stream().collect(traverseCollector(f)).map(s -> s.collect(Collectors.toList()));
    }
    public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
        return traverseList(list, Function.identity());
    }

    // Optional系は省略
}

Collector は常に副作用前提かつ非同期の為の仕組みが入ってくるので、本来その辺を意識しなくてよい筈の操作でも気にして実装する必要があり面倒ですね。accumulator の処理に関しては単一スレッドで動作する前提なので AtomicReference でなくても問題ないのですが、破壊的変更前提で参照を持たないといけないため手軽に使えそうなものがコレだったので使用しました。

これでめでたく道具が揃いました。目標であった List<String> から Either<List<String>, List<Doctor>> を作る処理を書いてみましょう。

    private Either<List<String>, List<Doctor>> validateAllLines(final List<String> lines) {
        return IntStream.range(0, lines.size())
            .mapToObj(index -> validateDoctor(index+1, lines.get(index)))
            .collect(Validations.validationListString().sequenceCollector())
            .map(s -> s.collect(Collectors.toList()));
    }

ランダムアクセス前提にしている所は手抜きですが、やりたい事は実現できました。

これで以下のテストコードがパスします。

    @Test
    public void testInvalidSample() {
        final List<String> lines = Arrays.asList(
            "1111111111,医師太郎,エムスリー病院",
            "2222222222,医師二郎",
            ",,エムスリー病院",
            "4444444444,医師四郎,",
            "abcdefghjk,,エムスリー病院",
            "6666666666,医師六郎,エムスリー病院"
        );

        assertEquals(
            Either.left(Arrays.asList(
                "2行目: 項目の数が足りません",
                "3行目: IDは必ず入力してください",
                "3行目: 氏名は必ず入力してください",
                "4行目: 勤務先は必ず入力してください",
                "5行目: IDは数字を入力してください",
                "5行目: 氏名は必ず入力してください"
            )), validateAllLines(lines));
    }

    @Test
    public void testValidSample() {
        final List<String> lines = Arrays.asList(
            "1111111111,医師太郎,エムスリー病院",
            "2222222222,医師二郎,エムスリー病院",
            "3333333333,医師三郎,エムスリー病院",
            "4444444444,医師四郎,エムスリー病院",
            "5555555555,医師五郎,エムスリー病院",
            "6666666666,医師六郎,エムスリー病院"
        );

        assertEquals(
            Either.right(Arrays.asList(
                new Doctor(ID.of("1111111111"), Name.of("医師太郎"), Name.of("エムスリー病院")),
                new Doctor(ID.of("2222222222"), Name.of("医師二郎"), Name.of("エムスリー病院")),
                new Doctor(ID.of("3333333333"), Name.of("医師三郎"), Name.of("エムスリー病院")),
                new Doctor(ID.of("4444444444"), Name.of("医師四郎"), Name.of("エムスリー病院")),
                new Doctor(ID.of("5555555555"), Name.of("医師五郎"), Name.of("エムスリー病院")),
                new Doctor(ID.of("6666666666"), Name.of("医師六郎"), Name.of("エムスリー病院"))
            )), validateAllLines(lines));
    }

まとめ

こんな感じで EitherSemigroup を使うとエラーハンドリングを細かい単位で実装してテストしやすくしたり、エラーを集約する事ができるようになります。

それでは皆様、良い Java8 ライフを。

注意

素朴な実装なのでパフォーマンス悪かったりStackOverflowすると思うので実務で使う場合はご注意下さい。

補足

そういえば書き忘れてましたが Functional Java には Validation が存在してるのでそっち使うのもよいでしょう。 Streamtraverse は無さそうですが。

package com.m3.advent.validation;
import static java.util.stream.Collector.Characteristics.CONCURRENT;
import io.atlassian.fugue.Either;
import com.m3.advent.validation.FunctionN.*;
import io.atlassian.fugue.Pair;
import io.atlassian.fugue.Semigroup;
import io.atlassian.fugue.Semigroups;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Validation<A> {
private final Semigroup<A> MA;
public Validation(final Semigroup<A> MA) {
this.MA = MA;
}
public <B1, B2, C> Either<A, C> apply2(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final BiFunction<? super B1, ? super B2, ? extends C> f) {
return e1.<Either<A, C>>fold(
l1 -> Either.<A, C>left(e2.fold(l2 -> MA.append(l1, l2), ignore -> l1)),
r1 -> e2.<A, C>bimap(l2 -> l2, r2 -> f.apply(r1, r2))
);
}
public <B1, B2, B3, C> Either<A, C> apply3(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Function3<? super B1, ? super B2, ? super B3, ? extends C> f) {
return apply2(this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair), e3, (t, r3) -> f.apply(t.left(), t.right(), r3));
}
public <B1, B2, B3, B4, C> Either<A, C> apply4(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Either<? extends A, ? extends B4> e4,
final Function4<? super B1, ? super B2, ? super B3, ? super B4, ? extends C> f) {
return apply2(
this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
(t1, t2) -> f.apply(t1.left(), t1.right(), t2.left(), t2.right())
);
}
public <B1, B2, B3, B4, B5, C> Either<A, C> apply5(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Either<? extends A, ? extends B4> e4,
final Either<? extends A, ? extends B5> e5,
final Function5<? super B1, ? super B2, ? super B3, ? super B4, ? super B5, ? extends C> f) {
return apply3(
this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
e5,
(t1, t2, r3) -> f.apply(t1.left(), t1.right(), t2.left(), t2.right(), r3)
);
}
public <B1, B2, B3, B4, B5, B6, C> Either<A, C> apply6(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Either<? extends A, ? extends B4> e4,
final Either<? extends A, ? extends B5> e5,
final Either<? extends A, ? extends B6> e6,
final Function6<? super B1, ? super B2, ? super B3, ? super B4, ? super B5, ? super B6, ? extends C> f) {
return apply3(
this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
this.<B5, B6, Pair<B5, B6>>apply2(e5, e6, Pair::pair),
(t1, t2, t3) -> f.apply(t1.left(), t1.right(), t2.left(), t2.right(), t3.left(), t3.right())
);
}
public <B1, B2, B3, B4, B5, B6, B7, C> Either<A, C> apply7(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Either<? extends A, ? extends B4> e4,
final Either<? extends A, ? extends B5> e5,
final Either<? extends A, ? extends B6> e6,
final Either<? extends A, ? extends B7> e7,
final Function7<? super B1, ? super B2, ? super B3, ? super B4, ? super B5, ? super B6, ? super B7, ? extends C> f) {
return apply2(
this.<Pair<B1, B2>, Pair<B3, B4>, Pair<Pair<B1, B2>, Pair<B3, B4>>>apply2(
this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
Pair::pair),
this.<Pair<B5, B6>, B7, Pair<Pair<B5, B6>, B7>>apply2(
this.<B5, B6, Pair<B5, B6>>apply2(e5, e6, Pair::pair),
e7,
Pair::pair),
(t1, Pair) -> f.apply(t1.left().left(), t1.left().right(), t1.right().left(), t1.right().right(), Pair.left().left(), Pair.left().right(), Pair.right())
);
}
public <B1, B2, B3, B4, B5, B6, B7, B8, C> Either<A, C> apply8(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Either<? extends A, ? extends B4> e4,
final Either<? extends A, ? extends B5> e5,
final Either<? extends A, ? extends B6> e6,
final Either<? extends A, ? extends B7> e7,
final Either<? extends A, ? extends B8> e8,
final Function8<? super B1, ? super B2, ? super B3, ? super B4, ? super B5, ? super B6, ? super B7, ? super B8, ? extends C> f) {
return apply2(
this.<Pair<B1, B2>, Pair<B3, B4>, Pair<Pair<B1, B2>, Pair<B3, B4>>>apply2(
this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
Pair::pair),
this.<Pair<B5, B6>, Pair<B7, B8>, Pair<Pair<B5, B6>, Pair<B7, B8>>>apply2(
this.<B5, B6, Pair<B5, B6>>apply2(e5, e6, Pair::pair),
this.<B7, B8, Pair<B7, B8>>apply2(e7, e8, Pair::pair),
Pair::pair),
(t1, Pair) -> f.apply(t1.left().left(), t1.left().right(), t1.right().left(), t1.right().right(), Pair.left().left(), Pair.left().right(), Pair.right().left(), Pair.right().right())
);
}
public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
return list.stream().collect(traverseCollector(f)).map(s -> s.collect(Collectors.toList()));
}
public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
return traverseList(list, Function.identity());
}
public <B, R> Collector<B, ?, Either<A, Stream<R>>> traverseCollector(final Function<? super B, Either<A, R>> f) {
final Semigroup<Stream<R>> rSemi = Stream::concat;
final Semigroup<Either<A, Stream<R>>> vSemi = Semigroups.either(MA, rSemi);
return Collector.<B, AtomicReference<Either<A, Stream<R>>>, Either<A, Stream<R>>>of(
() -> new AtomicReference<>(Either.right(Stream.empty())),
(acc, e) -> acc.accumulateAndGet(f.apply(e).map(Stream::of), (a, b) -> apply2(a, b, rSemi::append)),
(a, b) -> {a.accumulateAndGet(b.get(), vSemi::append); return a;},
AtomicReference::get,
CONCURRENT
);
}
public <R> Collector<Either<A, R>, ?, Either<A, Stream<R>>> sequenceCollector() {
return traverseCollector(Function.identity());
}
}
package com.m3.advent.validation;
public class FunctionN {
private FunctionN() {}
@FunctionalInterface
public static interface Function3<A1, A2, A3, R> {
R apply(final A1 a1, final A2 a2, final A3 a3);
}
@FunctionalInterface
public static interface Function4<A1, A2, A3, A4, R> {
R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4);
}
@FunctionalInterface
public static interface Function5<A1, A2, A3, A4, A5, R> {
R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4, final A5 a5);
}
@FunctionalInterface
public static interface Function6<A1, A2, A3, A4, A5, A6, R> {
R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4, final A5 a5, final A6 a6);
}
@FunctionalInterface
public static interface Function7<A1, A2, A3, A4, A5, A6, A7, R> {
R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4, final A5 a5, final A6 a6, final A7 a7);
}
@FunctionalInterface
public static interface Function8<A1, A2, A3, A4, A5, A6, A7, A8, R> {
R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4, final A5 a5, final A6 a6, final A7 a7, final A8 a8);
}
}
package com.m3.advent.validation;
import io.atlassian.fugue.Either;
import io.atlassian.fugue.Semigroup;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Validations {
private static <A> Semigroup<List<A>> listSemigroup() {
return (a, b) -> Stream.concat(a.stream(), b.stream()).collect(Collectors.toList());
}
public static <A> Validation<A> validation(final Semigroup<A> MA) {
return new Validation<A>(MA);
}
public static <A> Validation<List<A>> validationList() {
return validation(listSemigroup());
}
private static final Validation<List<String>> LIST_STRING = validationList();
public static Validation<List<String>> validationListString() {
return LIST_STRING;
}
public static <A, B> Either<List<A>, B> invalidList(final A left) {
return Either.left(Collections.singletonList(left));
}
public static <A, B> Either<List<A>, B> validList(final B right) {
return Either.right(right);
}
public static <A, B> Either<List<A>, B> validate(B b, Predicate<? super B> f, Supplier<? extends A> error) {
return f.test(b) ? validList(b) : invalidList(error.get());
}
}
package com.m3.advent.validation;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.junit.Assert.assertEquals;
public class Example {
private <A, B> Either<List<A>, B> validate(B b, Predicate<? super B> f, Supplier<? extends A> error) {
return f.test(b)
? Either.right(b)
: Either.left(Collections.singletonList(error.get()));
}
private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
return validate(
input,
StringUtils::isNotBlank,
() -> String.format("%d行目: %sは必ず入力してください", lineNo, fieldName)
);
}
private Either<List<String>, String> numeric(final int lineNo, final String fieldName, final String input) {
return validate(
input,
StringUtils::isNumeric,
() -> String.format("%d行目: %sは数字を入力してください", lineNo, fieldName)
);
}
private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
return nonEmpty(lineNo, "ID", input)
.flatMap(i -> numeric(lineNo, "ID", i))
.map(ID::of);
}
private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
return nonEmpty(lineNo, "氏名", input).map(Name::of);
}
private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
return nonEmpty(lineNo, "勤務先", input).map(Name::of);
}
private Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
return Validations.validate(line.split(",", -1), csv -> csv.length >= 3, () -> String.format("%d行目: 項目の数が足りません", lineNo))
.flatMap(csv -> Validations.validationListString().apply3(
validateId(lineNo, csv[0]),
validateName(lineNo, csv[1]),
validateWorkplaceName(lineNo, csv[2]),
Doctor::new)
);
}
private Either<List<String>, List<Doctor>> validateAllLines(final List<String> lines) {
return IntStream.range(0, lines.size())
.mapToObj(index -> validateDoctor(index+1, lines.get(index)))
.collect(Validations.validationListString().sequenceCollector())
.map(s -> s.collect(Collectors.toList()));
}
@Test
public void testInvalidSample() {
final List<String> lines = Arrays.asList(
"1111111111,医師太郎,エムスリー病院",
"2222222222,医師二郎",
",,エムスリー病院",
"4444444444,医師四郎,",
"abcdefghjk,,エムスリー病院",
"6666666666,医師六郎,エムスリー病院"
);
assertEquals(
Either.left(Arrays.asList(
"2行目: 項目の数が足りません",
"3行目: IDは必ず入力してください",
"3行目: 氏名は必ず入力してください",
"4行目: 勤務先は必ず入力してください",
"5行目: IDは数字を入力してください",
"5行目: 氏名は必ず入力してください"
)), validateAllLines(lines));
}
@Test
public void testValidSample() {
final List<String> lines = Arrays.asList(
"1111111111,医師太郎,エムスリー病院",
"2222222222,医師二郎,エムスリー病院",
"3333333333,医師三郎,エムスリー病院",
"4444444444,医師四郎,エムスリー病院",
"5555555555,医師五郎,エムスリー病院",
"6666666666,医師六郎,エムスリー病院"
);
assertEquals(
Either.right(Arrays.asList(
new Doctor(ID.of("1111111111"), Name.of("医師太郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("2222222222"), Name.of("医師二郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("3333333333"), Name.of("医師三郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("4444444444"), Name.of("医師四郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("5555555555"), Name.of("医師五郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("6666666666"), Name.of("医師六郎"), Name.of("エムスリー病院"))
)), validateAllLines(lines));
}
private static class ID<A> {
private final String value;
private ID(final String value) {
Objects.requireNonNull(value);
this.value = value;
}
public static <AA> ID<AA> of(final String value) {
return new ID<AA>(value);
}
@Override
public String toString() {
return String.format("ID(%s)", value);
}
@Override
public int hashCode() {
final int prime = 29;
int result = 1;
result = prime * result + value.hashCode();
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final ID<?> other = (ID<?>) obj;
return value.equals(other.value);
}
}
private static class Name<A> {
private final String value;
private Name(final String value) {
Objects.requireNonNull(value);
this.value = value;
}
public static <AA> Name<AA> of(final String value) {
return new Name<AA>(value);
}
@Override
public String toString() {
return String.format("Name(%s)", value);
}
@Override
public int hashCode() {
final int prime = 29;
int result = 1;
result = prime * result + value.hashCode();
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final ID<?> other = (ID<?>) obj;
return value.equals(other.value);
}
}
private static class Workplace {}
private static class Doctor {
private final ID<Doctor> id;
private final Name<Doctor> name;
private final Name<Workplace> workplaceName;
Doctor(final ID<Doctor> id, final Name<Doctor> name, final Name<Workplace> workplaceName) {
Objects.requireNonNull(id);
Objects.requireNonNull(name);
Objects.requireNonNull(workplaceName);
this.id = id;
this.name = name;
this.workplaceName = workplaceName;
}
@Override
public String toString() {
return String.format("Doctor(%s, %s, %s)", id, name, workplaceName);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + name.hashCode();
result = prime * result + id.hashCode();
result = prime * result + workplaceName.hashCode();
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final Doctor other = (Doctor) obj;
return id.equals(other.id)
&& name.equals(other.name)
&& workplaceName.equals(other.workplaceName);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment