是否可以在Java中构造一个Both,允许flatMap返回不同的Left值?



我试图理解Both是如何实现的。我一直在以一种允许在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 值,这将允许在每个点返回相关错误。

因此,例如,给定以下函数:

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. 我们传递左值的情况

因此,如果我的目标是启用有时从flatMap返回不同的Left值,那么我的两个通用变量<A,B>不起作用,因为如果情况1执行并且我的函数更改A,则情况2无效,因为A!=A'。将函数应用于右侧的操作可能已将左侧更改为其他类型。

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

  1. 我对任一类型行为的期望是否不正确?
  2. 是否可以
  3. 在平面映射操作期间返回不同的Left类型?
  4. 如果是这样,您如何让类型解决?

由于参数化,没有您想要的合理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的子类型或等于,所以它可以被抛弃。 我对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 >: AA指定为A1的子类型。 我知道Java有一个<A extends A1>语法,但我不确定它可以用来描述对A1的约束,就像我们在这种情况下需要的那样。

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

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

一个映射它应该映射什么?"不null的价值"你会说 - 不是吗?好的,但您首先在运行时知道这一点。flatMap方法是在编译时定义的。因此,您必须为每个案例提供映射。

您选择<C> Either<A, C> flatMap(Function<B, Either<A, C>> f)。此映射使用B类型的值作为输入。这意味着如果映射的Either either!either.isRight()则所有后续映射都将返回一个Either.left(a)其中a是第一个Either.left(a)的值。所以实际上只有一个Either eithereither.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;
}
}
}

回答您的问题:是的,可以构造一个返回不同 left 值的 Both。但我认为您的意图是知道如何获得适当的工作Either.

最新更新