如何在Java中模拟Haskell的"Either a b"?

35

我该如何编写一个类型安全的Java方法,可以返回类a或类b的某些内容? 例如:

public ... either(boolean b) {
  if (b) {
    return new Integer(1);
  } else {
    return new String("hi");
  }
}

什么是最清晰的方式?

我能想到的唯一方法就是使用异常处理,但这明显是不好的做法,因为它滥用了错误处理机制来实现一般语言特性...

public String either(boolean b) throws IntException {
  if (b) {
    return new String("test");
  } else {
    throw new IntException(new Integer(1));
  }
}

)


10
很遗憾,在Java中尝试使用优秀的风格(无论是否类似于Haskell)往往会产生冗长的代码,这不是好的风格,因此这是不可能的。老实说,虽然这让我很痛苦,但我可能会采用一种折中的方法,抛出两个异常作为hack... - alternative
这里有一个类似的问题,为想要更多阅读资源的人提供:https://dev59.com/jWcs5IYBdhLWcg3wjUuR - Tarrasch
更新:这里是Philip Wadler在LambdaWorld 2016中提供的通用SUM类型解决方案(Either是真正的SUM类型):https://www.youtube.com/watch?v=V10hzjgoklA&t=1742s - Dierk
14个回答

35

我用来模拟代数数据类型的一般公式是:

  • 类型是一个抽象基类,构造函数是该基类的子类
  • 每个构造函数的数据都在每个子类中定义。(这样允许具有不同数量数据的构造函数正常工作。它还消除了维护不变量(比如只有一个变量是非空等)的必要性)。
  • 子类的构造函数用于构造每个构造函数的值。
  • 要解构它,使用instanceof检查构造函数,并向适当的类型进行向下强制转换以获取数据。

因此对于 Either a b,可能是这样的:

abstract class Either<A, B> { }
class Left<A, B> extends Either<A, B> {
    public A left_value;
    public Left(A a) { left_value = a; }
}
class Right<A, B> extends Either<A, B> {
    public B right_value;
    public Right(B b) { right_value = b; }
}

// to construct it
Either<A, B> foo = new Left<A, B>(some_A_value);
Either<A, B> bar = new Right<A, B>(some_B_value);

// to deconstruct it
if (foo instanceof Left) {
    Left<A, B> foo_left = (Left<A, B>)foo;
    // do stuff with foo_left.a
} else if (foo instanceof Right) {
    Right<A, B> foo_right = (Right<A, B>)foo;
    // do stuff with foo_right.b
}

甚至更好:-)简短而简洁。所有掌握代数数据类型和基本Java知识的人都应该能够理解这个。做得好! - Bastl
6
这种方法与拥有isLeftgetAsLeftgetAsRight方法的方法大致相同。它不是类型安全的(它依赖于正确的客户端代码):如果您在客户端弄错了类型转换,就可能会出现异常。 - ziggystar
如果你要使用Either,你应该使用Disjunction(一个右偏的Either)。这样,你可以直接使用map和flatMap(如果是Right,则执行操作,否则不执行任何操作)。通常左边会有一个异常。这是Scalaz中的行为。 - jordan3
1
这是我在面向对象语言中使用sum类型的方式,除了(在没有适当的模式匹配的情况下)我更喜欢访问者模式而不是instanceof,因为它更加类型安全。我很惊讶其他答案都没有提到这一点。 - Benjamin Hodgson
@BenjaminHodgson,但是我至少在3年前的回答中实现了访问者模式。很抱歉我不知道这些有趣的名字... - ziggystar

26
这里有一个静态检查的类型安全解决方案;这意味着你不能创建运行时错误。请按照原意阅读前一句话。是的,你可以以某种方式引发异常...
它相当冗长,但嘿,这是Java!
public class Either<A,B> {
    interface Function<T> {
        public void apply(T x);
    }

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

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

    /* Here's the important part: */
    public void fold(Function<A> ifLeft, Function<B> ifRight) {
        if(right == null)
            ifLeft.apply(left);
        else
            ifRight.apply(right);
    }

    public static void main(String[] args) {
        Either<String,Integer> e1 = Either.left("foo");
        e1.fold(
                new Function<String>() {
                    public void apply(String x) {
                        System.out.println(x);
                    }
                },
                new Function<Integer>() {
                    public void apply(Integer x) {
                        System.out.println("Integer: " + x);
                    }
                });
    }
}

你可能想要查看Functional Java和Tony Morris的博客

这里是Functional Java中Either实现的链接。在我的示例中,fold被称为either。他们有一个更复杂的fold版本,可以返回值(对于函数式编程风格似乎很合适)。


那么你是通过定义函数来绑定类型的,对吗?这只是为了欺骗编译器,它也可以是空的吗? - Bastl
只是为了记录:Either.left/right 是代数意义上的实际构造函数吗? - Bastl
3
left/right是构造函数。我不是一个Haskell专业人士,也不太理解你所说的“绑定类型”的含义。Function只是一个助手类(用于一等函数),可以在其他地方定义。这里没有任何东西可以为空。技巧在于fold只调用正确类型的函数。调用代码总是必须提供处理AB的函数。 - ziggystar
请注意,在FunctionalJava中,数据存储在两个内部类“Left”或“Right”之一中,这些类仅具有相应的私有字段。上面的示例始终具有两个字段,其中至少一个为空。(但是,上面的构造函数不强制执行此操作!) - Chris Kuklewicz
1
@ChrisKuklewicz 嗯,没错。构造函数是私有的,工厂方法强制执行这一点。 - ziggystar
2
就 Haskell 而言,你在这里所做的是利用同构 (∀ r . (a -> r) -> r)a,以便 Either a b(∀ r . (Either a b -> r) -> r)(∀ r . (a -> r, b -> r) -> r) - leftaroundabout

9

通过编写一个通用类Either,参数化两种类型LR,并具有两个构造函数(一个接受L,一个接受R)和两个方法L getLeft()R getRight(),您可以与Haskell进行密切的对接。两个方法将返回构造时传递的值或抛出异常。


2
Either抽象化并声明L getLeft()R getRight()bool isRight()。现在,使用正确的构造函数创建LeftEitherRightEither以避免两个构造函数之间的类型擦除冲突。[该死的类型擦除!] - alternative
@alternative:感谢您的指定,我没有考虑到类型擦除。 - Riccardo T.
1
或者,可以指定两个构造函数Either(L left,Void right)Either(Void left,R right)。由于Void无法实例化,因此这两个参数中的一个必须始终为null,从而解决了歧义(尽管现在包装在Either中的实际值现在不能为null本身,但这也可以看作是一种特性而不是错误)。 - arne.b

7

已经提供的建议虽然可行,但并不完整,因为它们依赖于一些null引用,并且有效地使“Either”伪装成值元组。一个不相交的和显然是一种类型或另一种类型。

我建议查看FunctionalJavaEither实现作为示例。


1
FunctionalJava中的编码是这些答案中最准确的(至少在我写下这个评论时是这样)。此外,有关Java中sum类型的更多灵感,请参阅http://blog.tmorris.net/understanding-practical-api-design-static-typing-and-functional-programming/上的更长示例。 - Chris Kuklewicz
@ChrisKuklewicz 感谢您提供的链接 - 当时我找不到它 :) - gpampara
1
404。新链接:http://blog.tmorris.net/posts/understanding-practical-api-design-static-typing-and-functional-programming/ - Philip Durbin

4
重要的是不要在一种语言中写作,而在另一种语言中写作。通常,在Java中,您要将行为放入对象中,而不是通过get方法破坏封装在外部运行的“脚本”。这里没有提供进行此类建议的上下文。
处理此特定片段的一个安全方法是将其编写为回调。类似于非常简单的访问者模式。
public interface Either {
    void string(String value);
    void integer(int value);
}

public void either(Either handler, boolean b) throws IntException {
    if (b) {
        handler.string("test");
    } else {
        handler.integer(new Integer(1));
    }
}

您可能希望使用纯函数并返回值给调用上下文。
public interface Either<R> {
    R string(String value);
    R integer(int value);
}

public <R> R either(Either<? extends R> handler, boolean b) throws IntException {
    return b ?
        handler.string("test") :
        handler.integer(new Integer(1));
}

如果你想重新变得不关心返回值,请使用大写的 "Void"(注意大小写)。

许多人认为您提出的控制反转会导致代码更加晦涩难懂。但是,更客观地说,Java会带来重大的性能损失:没有尾调用优化。考虑一下,将一堆接受/返回Either的函数链接在一起使用恒定的堆栈空间。使用访问者模式,无论您传递一堆处理程序还是甚至硬编码特定处理程序,都会使用O(处理程序数量)堆栈空间。简而言之,在Java中,访问者模式不必要地降低了空间效率。 - Ericson2314

4

我已按照 Scala 的方式实现了以下内容。它有点啰嗦(毕竟是 Java :)),但它是类型安全的。

public interface Choice {    
  public enum Type {
     LEFT, RIGHT
  }

  public Type getType();

  interface Get<T> {
     T value();
  }
}

public abstract class Either<A, B> implements Choice {

  private static class Base<A, B> extends Either<A, B> {
    @Override
    public Left leftValue() {
      throw new UnsupportedOperationException();
    }

    @Override
    public Right rightValue() {
      throw new UnsupportedOperationException();
    }

    @Override
    public Type getType() {
      throw new UnsupportedOperationException();
    }
  }

  public abstract Left leftValue();

  public abstract Right rightValue();

  public static <A, B> Either<A, B> left(A value) {
    return new Base<A, B>().new Left(value);
  }

  public static <A, B> Either<A, B> right(B value) {
    return new Base<A, B>().new Right(value);
  }

  public class Left extends Either<A, B> implements Get<A> {

    private A value;

    public Left(A value) {
      this.value = value;
    }

    @Override
    public Type getType() {
      return Type.LEFT;
    }

    @Override
    public Left leftValue() {
      return Left.this;
    }

    @Override
    public Right rightValue() {
      return null;
    }

    @Override
    public A value() {
      return value;    
    }
  }

  public class Right extends Either<A, B> implements Get<B> {

    private B value;

    public Right(B value) {
      this.value = value;
    }

    @Override
    public Left leftValue() {
      return null;
    }

    @Override
    public Right rightValue() {
      return this;
    }

    @Override
    public Type getType() {
      return Type.RIGHT;
    }

    @Override
    public B value() {
      return value;
    }
  }
}

你可以在代码中传递 Either<A,B> 实例。 Type 枚举主要用于 switch 语句。

创建 Either 值很简单,只需要:

Either<A, B> underTest;

A value = new A();

underTest = Either.left(value);

assertEquals(Choice.Type.LEFT, underTest.getType());
assertSame(underTest, underTest.leftValue());
assertNull(underTest.rightValue());
assertSame(value, underTest.leftValue().value());

或者,在通常情况下,它被用于代替异常处理。
public <Error, Result> Either<Error,Result> doSomething() {
    // pseudo code
    if (ok) {
        Result value = ...
        return Either.right(value);
    } else {
        Error errorMsg = ...
        return Either.left(errorMsg);
    }
}

// somewhere in the code...

Either<Err, Res> result = doSomething();
switch(result.getType()) {
   case Choice.Type.LEFT:
      // Handle error
      Err errorValue = result.leftValue().value();
      break;
   case Choice.Type.RIGHT:
      // Process result
      Res resultValue = result.rightValue().value();
      break;
}

希望这能有所帮助。

3

我从 http://blog.tmorris.net/posts/maybe-in-java/ 学到,你可以将外部类的构造函数设置为私有,这样只有嵌套类可以继承它。这个技巧与上面最佳方法一样类型安全,但更简洁,适用于任何你想要的ADT,比如Scala的case类。

public abstract class Either<A, B> {
    private Either() { } // makes this a safe ADT
    public abstract boolean isRight();
    public final static class Left<L, R> extends Either<L, R>  {
        public final L left_value;
        public Left(L l) { left_value = l; }
        public boolean isRight() { return false; }
    }
    public final static class Right<L, R> extends Either<L, R>  {
        public final R right_value;
        public Right(R r) { right_value = r; }
        public boolean isRight() { return true; }
    }
}

(从顶部答案的代码和样式开始)

请注意:

  • 子类中的 final 是可选的。如果没有它们,您可以对 Left 和 Right 进行子类型化,但仍然不能直接对 Either 进行子类型化。因此,如果没有 final,Either 的宽度是有限的,但深度是无限的。

  • 对于像这样的 ADT,我看不出为什么要跳上整个反-instanceof 队伍。布尔值适用于 Maybe 或 Either,但通常情况下,instanceof 是您最好且唯一的选择。


2
由于 Derive4J,在Java中实现代数数据类型变得非常简单。您只需要创建以下类即可:
import java.util.function.Function;

@Data
public abstract class Either<A, B> {

  Either(){}

  /**
   * The catamorphism for either. Folds over this either breaking into left or right.
   *
   * @param left  The function to call if this is left.
   * @param right The function to call if this is right.
   * @return The reduced value.
   */
  public abstract <X> X either(Function<A, X> left, Function<B, X> right);
}

Derive4J 将负责创建左侧和右侧情况的构造函数,以及类似 Haskell 的模式匹配语法、每个方面的映射器方法等。

非常好 - Derive4J 似乎值得关注。 - davidbak

2
在一个小型库“ambivalence”中,有一个Java 8的Either的独立实现:http://github.com/poetix/ambivalence。它与Scala标准实现最接近,例如提供左右投影以进行maphashMap操作。你无法直接访问左侧或右侧的值;相反,你需要提供lambda将它们映射为单个结果类型来join这两种类型。
Either<String, Integer> either1 = Either.ofLeft("foo");
Either<String, Integer> either2 = Either.ofRight(23);
String result1 = either1.join(String::toUpperCase, Object::toString);
String result2 = either2.join(String::toUpperCase, Object::toString);

您可以从 Maven 中央仓库获取它:
<dependency>
    <groupId>com.codepoetics</groupId>
    <artifactId>ambivalence</artifactId>
    <version>0.2</version>
</dependency>

2
你不需要使用 instanceof 检查或冗余字段。令人惊讶的是,Java 的类型系统提供了足够的功能来清晰地模拟 sum types。
背景
首先,你知道任何数据类型都可以用函数进行编码吗?这被称为 Church编码。例如,使用 Haskell 签名,Either 类型可以定义如下:
type Either left right =
  forall output. (left -> output) -> (right -> output) -> output

你可以将其解释为“给定左值上的函数和右值上的函数,生成任意一个函数的结果”。
定义:
基于这个想法,在Java中我们可以定义一个名为Matcher的接口,它包括两个函数,然后根据如何在Sum类型上进行模式匹配来定义Sum类型。下面是完整的代码:
/**
 * A sum class which is defined by how to pattern-match on it.
 */
public interface Sum2<case1, case2> {

  <output> output match(Matcher<case1, case2, output> matcher);

  /**
   * A pattern-matcher for 2 cases.
   */
  interface Matcher<case1, case2, output> {
    output match1(case1 value);
    output match2(case2 value);
  }

  final class Case1<case1, case2> implements Sum2<case1, case2> {
    public final case1 value;
    public Case1(case1 value) {
      this.value = value;
    }
    public <output> output match(Matcher<case1, case2, output> matcher) {
      return matcher.match1(value);
    }
  }

  final class Case2<case1, case2> implements Sum2<case1, case2> {
    public final case2 value;
    public Case2(case2 value) {
      this.value = value;
    }
    public <output> output match(Matcher<case1, case2, output> matcher) {
      return matcher.match2(value);
    }
  }

}

使用方法

然后你可以像这样使用它:

import junit.framework.TestCase;

public class Test extends TestCase {

  public void testSum2() {
    assertEquals("Case1(3)", longOrDoubleToString(new Sum2.Case1<>(3L)));
    assertEquals("Case2(7.1)", longOrDoubleToString(new Sum2.Case2<>(7.1D)));
  }

  private String longOrDoubleToString(Sum2<Long, Double> longOrDouble) {
    return longOrDouble.match(new Sum2.Matcher<Long, Double, String>() {
      public String match1(Long value) {
        return "Case1(" + value.toString() + ")";
      }
      public String match2(Double value) {
        return "Case2(" + value.toString() + ")";
      }
    });
  }

}

使用这种方法,您甚至可以在像Haskell和Scala这样的语言中找到模式匹配的直接相似之处。

此代码作为我的复合类型(和积,又称联合和元组)的库的一部分进行分发。它位于GitHub上:

https://github.com/nikita-volkov/composites.java


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接