我试图理解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 有两种不同的情况:
- 我们映射右侧的情况
- 我们传递左值的情况
因此,如果我的目标是启用有时从flatMap返回不同的Left值,那么我的两个通用变量<A,B>
不起作用,因为如果情况1执行并且我的函数更改A
,则情况2无效,因为A
!=A'
。将函数应用于右侧的操作可能已将左侧更改为其他类型。
所有这些都让我想到了这些问题:
- 我对任一类型行为的期望是否不正确? 是否可以
- 在平面映射操作期间返回不同的
Left
类型? - 如果是这样,您如何让类型解决?
由于参数化,没有您想要的合理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 >: A
将A
指定为A1
的子类型。 我知道Java有一个<A extends A1>
语法,但我不确定它可以用来描述对A1
的约束,就像我们在这种情况下需要的那样。
关于您对Either
(doThing(...)
(的使用,您的平面映射似乎还不够。我假设您希望您的平面映射像Optional<T>
一样工作.
Optional.flatMap
的映射器采用T
类型的值并返回一个Optional<U>
其中U
是此方法的泛型类型参数。但是Optional
有一个泛型类型参数T
而Either
有两个:A
和B
。因此,如果要平面映射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 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;
}
}
}
回答您的问题:是的,可以构造一个返回不同 left 值的 Both。但我认为您的意图是知道如何获得适当的工作Either
.