是否可以在 Java 中构造一个允许 flatMap 到 return 不同 Left 值的 Either?

Is it possible to construct an Either in Java that allows flatMap to return a different Left value?

我想了解 Either 是如何实现的。我一直坚持以一种允许 return 在 flatMap 期间使用不同的 Left 值的方式将多个函数链接在一起。我不知道在类型系统中如何可能。

最小任一示例代码:

public class Either<A,B> {
    public final A left;
    public final B right;

    private Either(A a, B b) {
        left = a;
        right = b;
    }

    public static <A, B> Either<A, B> left(A a) {
        return new Either<>(a, null);
    }


    public static <A, B> Either<A, B> right(B b) {
        return new Either<>(null, b);
    }


    public <C> Either<A, C> flatMap(Function<B, Either<A,C>> f) {
        if (this.isRight()) return f.apply(this.right);
        else return Either.left(this.left);
    }

    // map and other useful functions....

我最初认为我可以映射到不同的 Left 值,这将允许 returning 每一点的相关错误。

因此,例如,给定这些函数:

public static Either<Foo, String> doThing() {
        return Either.right("foo");
}

public static Either<Bar, String> doThing2(String text) {
    return (text.equals("foo")) 
        ? Either.right("Yay!") 
        : Either.left(new Bar("Grr..."));
}

public static Either<Baz, String> doThing3() {
    return (text.equals("Yay!")) 
        ? Either.right("Hooray!") 
        : Either.left(new Baz("Oh no!!"));
}

我以为我能做到

doThing().flatMap(x -> doThing2()).flatMap(y -> doThing3())

但是,编译器将其标记为不可能。

在研究了代码之后,我意识到这是由于我的 <A,B> 通用参数造成的。

flatMap 有两种不同的情况:

  1. 我们映射右侧的情况
  2. 我们通过左值的情况

所以,如果我的目标是有时 returning 来自 flatMap 的不同 Left 值,那么我的两个通用变量 <A,B> 不起作用,因为如果案例 1 执行并且我的函数发生变化A,则情况2无效,因为A != A'。将函数应用于右侧的行为可能已将左侧更改为不同的类型。

所有这些让我想到了这些问题:

  1. 我对 Either 类型行为的预期是否不正确?
  2. 是否可以在 flatMap 操作期间 return 不同的 Left 类型?
  3. 如果是这样,您如何确定类型?

由于参数化,没有您想要的合理 flatMap() 函数。考虑:

Either<Foo, String> e1 = Either.left(new Foo());
Either<Bar, String> e2 = foo.flatMap(x -> doThing2());
Bar bar = e2.left; // Where did this come from???

flatMap() 本身必须以某种方式发明一个 Bar 实例。如果您开始编写可以更改两种类型的 flatMap(),您会更清楚地看到问题:

public <C, D> Either<C, D> flatMap(Function<B, Either<C, D>> f) {
    if (this.isRight()) {
        return f.apply(this.right);
    } else {
        // Error: can't convert A to C
        return Either.left(this.left);
    }
}

可以,但是旧的 Left 必须是新的 Left 的子类型或等于新的 Left,因此可以向上转换。我不是很熟悉 Java 的语法,但 Scala 实现看起来像:

def flatMap[A1 >: A, B1](f: B => Either[A1, B1]): Either[A1, B1] = this match {
  case Right(b) => f(b)
  case _        => this.asInstanceOf[Either[A1, B1]]
}

这里的 A1 >: A 指定 A 作为 A1 的子类型。我知道 Java 有一个 <A extends A1> 语法,但我不确定它是否可以用来描述 A1 上的约束,正如我们在这种情况下所需要的那样。

关于您对 Either (doThing(...)) 的使用,您的平面映射似乎还不够。我假设您希望像 Optional<T>.

这样的平面映射工作

Optional.flatMap 的映射器采用 T 种类 和 return 的 Optional<U> 值,其中 U 是该方法的泛型类型参数。但是 Optional 有一个泛型类型参数 TEither 有两个:AB。因此,如果您想平面映射一个 Either<A,B> either,使用一个映射是不够的。

一个映射应该映射什么? "The value which isn't null" 你会说 - 不是吗?好的,但是您首先在运行时就知道了。您的 flatMap 方法是在编译时定义的。因此,您必须为每种情况提供一个映射。

你选择<C> Either<A, C> flatMap(Function<B, Either<A, C>> f)。此映射使用 B 类型的值作为输入。这意味着如果映射的 Either either!either.isRight(),则所有后续映射将是 return 和 Either.left(a),其中 a 是第一个 Either.left(a) 的值。所以实际上只有 Either either 其中 either.isRight() 可以映射到另一个值。而且一开始就必须是either.isRight()。这也意味着一旦创建了 Either<A,B> either,所有平面映射将导致 Either<A,?> 种类 。所以当前的 flatMap 限制了一个 Either either 来保持它的左泛型。这是你应该做的吗?

如果您想不受限制地平面映射 Either either,您需要针对两种情况进行映射:either.isRight()!either.isRight()。这将允许您在两个方向上继续平面映射。

我是这样做的:

public class Either<A, B> {
    public final A left;
    public final B right;

    private Either(A a, B b) {
        left = a;
        right = b;
    }

    public boolean isRight() {
        return right != null;
    }

    @Override
    public String toString() {
        return isRight() ?
                right.toString() :
                left.toString();
    }

    public static <A, B> Either<A, B> left(A a) {
        return new Either<>(a, null);
    }

    public static <A, B> Either<A, B> right(B b) {
        return new Either<>(null, b);
    }

    public <C, D> Either<C, D> flatMap(Function<A, Either<C, D>> toLeft, Function<B, Either<C, D>> toRight) {
        if (this.isRight()) {
            return toRight.apply(this.right);
        } else {
            return toLeft.apply(this.left);
        }
    }

    public static void main(String[] args) {
        Either<String, String> left = Either.left(new Foo("left"))
                .flatMap(l -> Either.right(new Bar(l.toString() + ".right")), r -> Either.left(new Baz(r.toString() + ".left")))
                .flatMap(l -> Either.left(l.toString() + ".left"), r -> Either.right(r.toString() + ".right"));
        System.out.println(left); // left.right.right

        Either<String, String> right = Either.right(new Foo("right"))
                .flatMap(l -> Either.right(new Bar(l.toString() + ".right")), r -> Either.left(new Baz(r.toString() + ".left")))
                .flatMap(l -> Either.left(l.toString() + ".left"), r -> Either.right(r.toString() + ".right"))
                .flatMap(l -> Either.right(l.toString() + ".right"), r -> Either.left(r.toString() + ".left"));
        System.out.println(right); // right.left.left.right
    }

    private static class Foo {
        private String s;

        public Foo(String s) {
            this.s = s;
        }

        @Override
        public String toString() {
            return s;
        }
    }

    private static class Bar {
        private String s;

        public Bar(String s) {
            this.s = s;
        }

        @Override
        public String toString() {
            return s;
        }
    }

    private static class Baz {
        private String s;

        public Baz(String s) {
            this.s = s;
        }

        @Override
        public String toString() {
            return s;
        }
    }
}

回答您的问题:是的,可以构造一个 Either returning 不同的左值。但我认为您的意图是了解如何获得正常工作 Either