Java标记了union / sum类型

时间:2018-01-08 01:41:59

标签: java haskell algebraic-data-types

有没有办法在Java中定义和类型? Java似乎自然地直接支持产品类型,我认为枚举可能允许它支持和类型,并且继承看起来可能它可以做到,但至少有一个案例我无法解决。 详细说明,sum类型是一种类型,它可以具有一组不同类型中的一种,如C中的标记并集。 就我而言,我试图在Java中实现haskell的任何一种类型:

data Either a b = Left a | Right b

但在基层,我必须将其作为产品类型实施,而忽略其中的一个字段:

public class Either<L,R>
{
    private L left = null;
    private R right = null;

    public static <L,R> Either<L,R> right(R right)
    {
        return new Either<>(null, right);
    }

    public static <L,R> Either<L,R> left(L left)
    {
        return new Either<>(left, null);
    }

    private Either(L left, R right) throws IllegalArgumentException
    {
        this.left = left;
        this.right = right;
        if (left != null && right != null)
        {
            throw new IllegalArgumentException("An Either cannot be created with two values");
        }
        if (left == right)
        {
            throw new IllegalArgumentException("An Either cannot be created without a value");
        }
    }

    .
    .
    .
}

我尝试使用继承来实现它,但我必须使用通配符类型参数或等效的Java泛型不允许的参数:

public class Left<L> extends Either<L,?>

我还没有多少使用过Java的Enums,但他们看起来是次佳候选人,但我并不乐观。 在这一点上,我认为这可能只能通过类型转换Object值来实现,我希望完全避免这些值,除非有一种方法可以安全地执行,并且能够使用它适用于所有类型。

4 个答案:

答案 0 :(得分:51)

使用一个私有构造函数创建Either一个抽象类,并在类中嵌套“数据构造函数”(leftright静态工厂方法),以便它们可以看到私有构造函数,但没有别的可以,有效地密封类型。

使用抽象方法either来模拟详尽的模式匹配,适当地覆盖静态工厂方法返回的具体类型。根据{{​​1}}实现便捷方法(如fromLeftfromRightbimapfirstsecond)。

either

愉快又安全!没办法搞砸了。

关于您尝试import java.util.Optional; import java.util.function.Function; public abstract class Either<A, B> { private Either() {} public abstract <C> C either(Function<? super A, ? extends C> left, Function<? super B, ? extends C> right); public static <A, B> Either<A, B> left(A value) { return new Either<>() { @Override public <C> C either(Function<? super A, ? extends C> left, Function<? super B, ? extends C> right) { return left.apply(value); } }; } public static <A, B> Either<A, B> right(B value) { return new Either<>() { @Override public <C> C either(Function<? super A, ? extends C> left, Function<? super B, ? extends C> right) { return right.apply(value); } }; } public Optional<A> fromLeft() { return this.either(Optional::of, value -> Optional.empty()); } // other convenience methods } 的问题,请考虑签名class Left<L> extends Either<L,?>。类型参数<A, B> Either<A, B> left(A value)未出现在参数列表中。因此,给定某种类型B的值,您可以为任何类型A获取Either<A, B>

答案 1 :(得分:19)

编码和类型的标准方法是Boehm-Berarducci编码(通常由其堂兄的名称,Church编码),它代表代数数据类型作为其消除器,即函数做模式匹配。在哈斯克尔:

left :: a -> (a -> r) -> (b -> r) -> r
left x l _ = l x

right :: b -> (a -> r) -> (b -> r) -> r
right x _ r = r x

match :: (a -> r) -> (b -> r) -> ((a -> r) -> (b -> r) -> r) -> r
match l r k = k l r

-- Or, with a type synonym for convenience:

type Either a b r = (a -> r) -> (b -> r) -> r

left :: a -> Either a b r
right :: b -> Either a b r
match :: (a -> r) -> (b -> r) -> Either a b r -> r

在Java中,这看起来像访问者:

public interface Either<A, B> {
    <R> R match(Function<A, R> left, Function<B, R> right);
}

public final class Left<A, B> implements Either<A, B> {

    private final A value;

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

    public <R> R match(Function<A, R> left, Function<B, R> right) {
        return left.apply(value);
    }

}

public final class Right<A, B> implements Either<A, B> {

    private final B value;

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

    public <R> R match(Function<A, R> left, Function<B, R> right) {
        return right.apply(value);
    }

}

使用示例:

Either<Integer, String> result = new Left<Integer, String>(42);
String message = result.match(
  errorCode -> "Error: " + errorCode.toString(),
  successMessage -> successMessage);

为方便起见,您可以创建工厂来创建LeftRight值,而无需每次都提及类型参数;如果您希望选择模式匹配而不产生结果,则还可以添加match的{​​{1}}版本,而不是Consumer<A> left, Consumer<B> right

答案 2 :(得分:6)

好的,所以继承解决方案绝对是最有希望的。我们想要做的是class Left<L> extends Either<L, ?>,由于Java的通用规则,我们遗憾地无法做到这一点。但是,如果我们做出让步,LeftRight的类型必须编码&#34;替代&#34;可能性,我们可以这样做。

public class Left<L, R> extends Either<L, R>`

现在,我们希望能够将Left<Integer, A>转换为Left<Integer, B>,因为它实际上并未使用第二个类型参数。我们可以定义一种在内部进行此转换的方法,从而将该自由编码到类型系统中。

public <R1> Left<L, R1> phantom() {
  return new Left<L, R1>(contents);
}

完整示例:

public class EitherTest {

  public abstract static class Either<L, R> {}

  public static class Left<L, R> extends Either<L, R> {

    private L contents;

    public Left(L x) {
      contents = x;
    }

    public <R1> Left<L, R1> phantom() {
      return new Left<L, R1>(contents);
    }

  }

  public static class Right<L, R> extends Either<L, R> {

    private R contents;

    public Right(R x) {
      contents = x;
    }

    public <L1> Right<L1, R> phantom() {
      return new Right<L1, R>(contents);
    }

  }

}

当然,您需要添加一些功能来实际访问内容,并检查值是Left还是Right,这样您就不必撒上instanceof和各地的明确演员表,但这至少应该足以开始。

答案 3 :(得分:1)

继承可以用于模拟和类型(Disjoint unions),但是您需要处理一些问题:

  1. 您需要注意让其他人不要在您的类型中添加新案例。如果您想要详尽地处理可能遇到的每个案例,这一点尤为重要。使用非final超类和package-private构造函数是可能的。
  2. 缺少模式修补使得使用此类型的值非常困难。如果您希望使用编译器检查的方式来保证您已经详尽地处理了所有情况,那么您需要自己实现匹配功能。
  3. 您被迫使用两种API中的一种,这两种方式都不理想:
    • 所有案例都实现了一个通用的API,在他们自己不支持的API上抛出错误。考虑Optional.get()。理想情况下,此方法仅适用于已知值为some而不是none的不相交类型。但是没有办法做到这一点,因此它是一般Optional类型的实例成员。如果你在一个名为“none”的可选项上调用它,它会抛出NoSuchElementException
    • 每个案例都有一个独特的API,可以准确地告诉您它的功能,但每次要调用其中一个特定于子类的方法时,都需要手动进行类型检查和转换。
  4. 更改“案例”需要新的对象分配(如果经常这样做,会增加GC的压力)。
  5. TL; DR:Java中的函数式编程并不是一种愉快的体验。