haskell Java标记的联合/求和类型

tzcvj98z  于 2022-11-14  发布在  Java
关注(0)|答案(6)|浏览(157)

有没有办法在Java中定义sum类型?Java似乎天生就直接支持乘积类型,我认为枚举可能允许它支持sum类型,而继承看起来可能可以做到这一点,但至少有一种情况我无法解决。详细说明一下,sum类型是一种类型,它可以只拥有一组不同类型中的一个,就像C中的标记联合。在我的情况下,我正在尝试在Java中实现Haskell的Either type:

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值才有可能,我希望完全避免这种情况,除非有一种方法可以安全地一次完成,并能够对所有求和类型使用。

b4lqfgs4

b4lqfgs41#

使Either成为一个抽象类,没有字段,只有一个构造函数(private、no-args、empty),并将“数据构造函数”(leftright静态工厂方法)嵌套在类中,这样它们就可以看到私有构造函数,而其他任何方法都看不到,从而有效地密封了类型。
使用抽象方法either来模拟穷举模式匹配,在静态工厂方法返回的具体类型中进行适当的重写。根据either实现方便的方法(如fromLeftfromRightbimapfirstsecond)。

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)。类型参数B没有出现在参数列表中。所以,给定一个A类型的值,你可以得到一个Either<A, B>,用于 * 任何 * B类型。

eulz3vhy

eulz3vhy2#

一个标准的编码和类型的方法是Boehm-Berarducci编码(通常被称为它的表亲,教堂编码的名字),它表示一个代数数据类型作为它的 * 消除器 *,即,一个函数,做模式匹配。在Haskell:

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值,而不必每次都提到类型参数;如果您希望模式匹配选项不产生结果,您还可以添加接受Consumer<A> left, Consumer<B> right而不是Function<A, R> left, Function<B, R> rightmatch版本。

nhhxz33t

nhhxz33t3#

好的,继承解决方案无疑是最有前途的。我们想做的是class Left<L> extends Either<L, ?>,但不幸的是,由于Java的泛型规则,我们不能这样做。然而,如果我们做出让步,LeftRight的类型必须编码“替代”的可能性,我们可以这样做。

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和显式强制转换,但至少这应该足以开始。

fae0ux8s

fae0ux8s4#

继承 * 可以 * 用于模拟求和类型(不相交联合),但有几个问题需要处理:
1.你需要注意防止其他人在你的类型中添加新的case。如果你想彻底地处理你可能遇到的每一个case,这一点尤其重要。对于非final超类和包私有构造函数来说,这是可能的。
1.缺少模式修补使得使用这种类型的值非常困难。如果你想用编译器检查的方法来保证你已经彻底处理了所有的情况,你需要自己实现一个匹配函数。

  • 您被迫使用两种风格的API之一,但这两种风格都不理想:
  • 所有情况都实现了一个公共API,在不支持自己的API上抛出错误。考虑Optional.get()。理想情况下,该方法只能在已知值为some而不是none的不相交类型上使用。但没有办法做到这一点。所以它是一般Optional型别的执行严修成员。如果您在选择项上呼叫它,而选择项的case是none,它就会掷回NoSuchElementException
  • 每个case都有一个唯一的API,它确切地告诉您它的功能,但是每次您希望调用这些特定于子类的方法之一时,都需要手动进行类型检查和类型转换。
  • 改变“cases”需要新的对象分配(如果经常这样做,会增加GC的压力)。

TL;DR:用Java进行函数式编程并不是一种愉快的体验。

5ssjco0h

5ssjco0h5#

不如

import java.util.Optional;

public interface Either<L, R> {
  default Optional<L> left() { return Optional.empty();}
  default Optional<R> right() { return Optional.empty();}

  static <L, R> Either<L, R> fromLeft(L left) {
    return new Either<L, R>() {
      @Override public Optional<L> left() { return Optional.of(left); }
    };
  }

  static <L, R> Either<L, R> fromRight(R right) {
    return new Either<L, R>() {
      @Override public Optional<R> right() { return Optional.of(right); }
    };
  }
}

与这里提出的其他解决方案的区别不是很深,而是风格上的。

cngwdvgl

cngwdvgl6#

让我建议一个非常不同的解决方案,它不使用继承/抽象类/接口。缺点是,它需要为每个新定义的“求和类型”付出一些努力。然而,我认为它有很多优点:它是安全的,它只使用基本概念,使用起来感觉很自然,并且允许2个以上的“子类型”。
这里是二叉树的概念证明,因为它比“Either”更实用,但是您可以使用注解作为指导来构建自己的sum类型。

public class Tree {

// 1) Create an enum listing all "subtypes" (there may be more than 2)
enum Type { Node, Leaf }

// 2) Create a static class for each subtype (with the same name for clarity)
public static class Node {
    
    Tree l,r;
    
    public Node(Tree l, Tree r) {
        
        this.l = l;
        this.r = r;
    }
    
}

public static class Leaf {
    
    int label;
    
    public Leaf(int label) {
        this.label = label;
    }
    
}

// 3) Each instance must have:
// One variable to indicate which subtype it corresponds to
Type type;

// One variable for each of the subtypes (only one will be different from null)
Leaf leaf;
Node node;

// 4) Create one constructor for each subtype (it could even be private)
public Tree(Node node) {
    
    this.type = Type.Node;
    this.node = node;
    
}

// 5) Create one "factory" method for each subtype (not mandatory but quite convenient)
public static Tree newNode(Tree l, Tree r) {
    return new Tree(new Node(l,r));
}

public Tree(Leaf leaf) {
    
    this.type = Type.Leaf;
    this.leaf = leaf;
    
}

public static  Tree newLeaf(int label) {
    return new Tree(new Leaf(label));
}

// 6) Create a generic "matching" function with one argument for each subtype
// (the constructors ensure that no "null pointer exception" can be raised)
public <T> T match(Function<Node,T> matchNode, Function<Leaf,T> matchLeaf) {
    
    switch (type) {
    case Node:
        return matchNode.apply(node);
    case Leaf:
        return matchLeaf.apply(leaf);
    }
    return null;
    
}

// 7) Have fun !
// Note that matchings are quite natural to write.
public int size() {
    
    return match(
            node -> 1 + node.l.size() + node.r.size(),
            leaf -> 1
    );
    
}

public String toString() {
    
    return match(
            node -> {
                String sl = node.l.toString();
                String sr = node.r.toString();
                return "Node { "+sl+" , "+sr+" }";
            },
            leaf -> "Leaf: "+leaf.label
    );
    
}

public static void main(String [] args) {
    
    Tree node1 = Tree.newNode(Tree.newLeaf(1),Tree.newLeaf(2));
    Tree node2 = Tree.newNode(node1,Tree.newLeaf(3));
    
    System.out.println(node2.size());
    System.out.println(node2);
    
}
}

请随意提出批评,我对这个主题真的很感兴趣,很乐意了解更多。

相关问题