Java 标记的联合/总和类型

2022-08-31 17:32:24

有没有办法在Java中定义求和类型?Java似乎自然地直接支持产品类型,我认为枚举可能允许它支持总和类型,而继承看起来也许可以做到这一点,但至少有一种情况我无法解决。为了详细说明,sum类型是一种类型,它可以恰好具有一组不同类型的一个,就像C中的标记并集一样。在我的情况下,我试图在Java中实现haskell的Ory类型:

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


答案 1

创建一个没有字段、只有一个构造函数(private、no-args、empty)的抽象类,并将“数据构造函数”(和静态工厂方法)嵌套在类中,以便它们可以看到私有构造函数,但其他任何东西都看不到,从而有效地密封了类型。Eitherleftright

使用抽象方法来模拟穷举模式匹配,并在静态工厂方法返回的具体类型中适当地重写。在 方面实现方便的方法(如 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<A, B>() {
            @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<A, B>() {
            @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());
    }
}

令人愉快和安全!没有办法搞砸它。由于类型是有效的密封的,因此您可以放心,只有两种情况,并且每个操作最终都必须根据方法进行定义,这迫使调用方处理这两种情况。either

关于您尝试解决的问题,请考虑 签名 。类型参数不会显示在参数列表中。因此,给定某种类型的值,您可以获得任何类型的。class Left<L> extends Either<L,?><A, B> Either<A, B> left(A value)BAEither<A, B>B


答案 2

编码和类型的标准方法是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);

为方便起见,您可以创建一个用于创建和值的工厂,而不必每次都提及类型参数;您还可以添加可接受的版本,而不是如果您想要在不产生结果的情况下进行模式匹配的选项。LeftRightmatchConsumer<A> left, Consumer<B> rightFunction<A, R> left, Function<B, R> right


推荐