减少状态之一抛出异常的简洁方法

Concise way to reduce where one of the states throws an exception

我有一堆:

Validation<String, Foo> a;
Validation<String, Foo> b;
Validation<String, Foo> c;

以下是他们的一些方法:

boolean isValid();
boolean isInvalid(); // === !isValid()
String getError();

现在,我正在尝试这样做:

Stream.of(a, b, c).reduce(
    Validation.valid(foo),
    (a, b) -> a.isValid() && b.isValid()
              ? Validation.valid(foo)
              : String.join("; ", a.getError(), b.getError())
);

有一个明显的问题,如果ab中只有一个错误,那么就会有一个不必要的;。但是还有一个更严重的问题:getError() 如果验证有效则抛出异常。

有没有一种方法可以编写这个 lambda(或在 io.vavr.control.Validation library 中使用其他东西)而不需要全部 4 种情况(a && ba && !b!a && b , !a && !b) 显式?


编辑

说得更清楚一点,我最后想要一个 Validation<String, Foo> 的结果。我认为它的行为类似于 "monad,",但我不确定。

如我所见,您的 reduce 的输出是一个 字符串 ,错误列表由 ; 分隔。

您正在混合累加器参数:

  • a是当前减少的部分结果
  • b 是您正在迭代的对象本身

我会这样做:

Stream.of(a, b, c).reduce(
    "", //initial state,
    (prevState, validationObject) -> {
        if (validationObject.isInvalid())
            return prevState + ";" + validationObject.getError();
        else
            return prevState;
    }
)

Collectors.groupingBy()呢。我认为你仍然可以改进字符串的假部分集并加入它们,下面代码的输出是:

{false=[SomeException, ReallyBadProblem], true=[3, 5]}

代码示例:

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toSet;

import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Stream;

public class Main {


    public static void main(String[] args)  {

        Validation<String,Integer> a = new Validation<>(true, null, 3);
        Validation<String,Integer> b = new Validation<>(true, null, 5);
        Validation<String,Integer> c = new Validation<>(false, "SomeException", null);
        Validation<String,Integer> d = new Validation<>(false, "ReallyBadProblem", null);

        //Stream.of(a,b,c).collect(Collectors.groupingBy(v->v.isValid(), v->v.));

        TreeMap<Boolean, Set<Object>> map = Stream.of(a,b,c,d).collect((groupingBy(v->v.isValid(), TreeMap::new,
                                             mapping(v-> { return v.isValid() ? v.valid() : v.getError();}, toSet()))));

        System.out.println(map);
    }

    public static class Validation<E, T>{

        boolean valid;
        T validVal;
        String error;


        public Validation(boolean valid, String error, T validVal) {
            super();
            this.valid = valid;
            this.error = error;
            this.validVal = validVal;
        }
        /**
         * @return the valid
         */
        public boolean isValid() {
            return valid;
        }
        /**
         * @param valid the valid to set
         */
        public void setValid(boolean valid) {
            this.valid = valid;
        }
        /**
         * @return the error
         */
        public String getError() {
            return error;
        }
        /**
         * @param error the error to set
         */
        public void setError(String error) {
            this.error = error;
        }

        public T valid() {
            return validVal;
        }


    }
}

我认为您要实现的目标在 Either 域中更容易解决。

首先,将您的 Validation 流转换为 Either 流:

Stream<Either<String, Foo>> eithers = Stream.of(a, b, c)
    .map(Validation::toEither);

然后合并它们:

Either<String, Foo> result = Either.sequence(eithers)
    .mapLeft(seq -> seq.collect(Collectors.joining("; ")))
    .map(combinator); // fill in with combinator function that converts
                      // a Seq<Foo> into a single Foo

由于您没有指定如何将多个有效 Foo 对象合并为一个对象,因此我将其打开供您填写上面示例中的组合器函数。

Either.sequence(...) 将通过返回包含左值序列的 Either.Left 将许多 either 减少为一个,如果提供的 eithers 中的 any 是一个左,或者 Either.Right 包含所有右值的(可能为空)序列,如果 none 提供的任一个是左值。

更新:

有一种 Validation.sequence(...) 方法可以在不转换为 Either 域的情况下完成(我在创建原始答案时不知何故错过了 - 感谢您指出):

Validation<Seq<String>, Seq<Foo>> validations = Validation.sequence(
        Stream.of(a, b, c)
            .map(v -> v.mapError(List::of))
);

Validation<String, Foo> result = validations
    .mapError(errors -> errors.collect(Collectors.joining("; ")))
    .map(combinator); // fill in with combinator function that converts
                      // a Seq<Foo> into a single Foo

你说 Foo 个实例是相同的,这意味着你可以使用 Seq::head 代替组合函数。但是你需要注意不要使用空的验证序列作为输入,因为在这种情况下它会导致 Seq::head 抛出 NoSuchElementException

这就是我最终选择的方式,尽管我认为@Nandor(接受的答案)是正确的。 (他的解决方案基于 io.vavr.control.Validation 的最新版本,而不是我可用的版本(仍然是 javaslang.control.Validation)。我发现 mapLeft 已重命名为 mapErrors,但是是一些与 Seq 操作相关的缺失位。由于对 Java 不熟悉,我无法解决我走这条路的错误。)

Validation<String, AdRequest> validation =
    Stream.of(
        validateTargetingRequest(adRequest),
        validateFlightRequest(adRequest, ad),
        validateCreativeRequest(adRequest)
    ).reduce(
        Validation.valid(adRequest),
        (a, b) -> {
          if (a.isValid() && b.isValid()) {
            return Validation.valid(adRequest);
          }
          if (a.isInvalid() && b.isInvalid()) {
            return Validation.invalid(String.join("; ", a.getError(), b.getError()));
          }
          // This seemingly overcomplicated structure was necessitated by the fact that getError
          // throws an exception when called on an Valid form of Validation.
          return a.isInvalid() ? a : b;
        }
    );