是否可以在 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 有两种不同的情况:
- 我们映射右侧的情况
- 我们通过左值的情况
所以,如果我的目标是有时 returning 来自 flatMap 的不同 Left 值,那么我的两个通用变量 <A,B>
不起作用,因为如果案例 1 执行并且我的函数发生变化A
,则情况2无效,因为A
!= A'
。将函数应用于右侧的行为可能已将左侧更改为不同的类型。
所有这些让我想到了这些问题:
- 我对 Either 类型行为的预期是否不正确?
- 是否可以在 flatMap 操作期间 return 不同的
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
的子类型或等于新的 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
有一个泛型类型参数 T
而 Either
有两个:A
和 B
。因此,如果您想平面映射一个 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
。
我想了解 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 有两种不同的情况:
- 我们映射右侧的情况
- 我们通过左值的情况
所以,如果我的目标是有时 returning 来自 flatMap 的不同 Left 值,那么我的两个通用变量 <A,B>
不起作用,因为如果案例 1 执行并且我的函数发生变化A
,则情况2无效,因为A
!= A'
。将函数应用于右侧的行为可能已将左侧更改为不同的类型。
所有这些让我想到了这些问题:
- 我对 Either 类型行为的预期是否不正确?
- 是否可以在 flatMap 操作期间 return 不同的
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
的子类型或等于新的 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
有一个泛型类型参数 T
而 Either
有两个:A
和 B
。因此,如果您想平面映射一个 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
。