我可以在Java中定义否定接口吗?

2022-08-31 20:34:37

问这个问题是为了澄清我对类型类和更高种类类型的理解,我不是在寻找Java中的解决方法。


在Haskell中,我可以写这样的东西

class Negatable t where
    negate :: t -> t

normalize :: (Negatable t) => t -> t
normalize x = negate (negate x)

然后假设有一个实例,BoolNegatable

v :: Bool
v = normalize True

一切都很好。


在Java中,似乎不可能声明一个正确的接口。我们可以这样写:Negatable

interface Negatable {
    Negatable negate();
}

Negatable normalize(Negatable a) {
    a.negate().negate();
}

但是,与Haskell不同,如果没有强制转换(假设实现),以下内容将无法编译:MyBooleanNegatable

MyBoolean val = normalize(new MyBoolean()); // does not compile; val is a Negatable, not a MyBoolean

有没有办法在Java接口中引用实现类型,或者这是Java类型系统的基本限制?如果是限制,是否与高级类型支持有关?我认为不是:看起来这是另一种限制。如果是这样,它有名字吗?

谢谢,如果问题不清楚,请让我知道!


答案 1

实际上,是的。不是直接的,但你可以做到。只需包含一个泛型参数,然后从泛型类型派生。

public interface Negatable<T> {
    T negate();
}

public static <T extends Negatable<T>> T normalize(T a) {
    return a.negate().negate();
}

你会像这样实现这个接口

public static class MyBoolean implements Negatable<MyBoolean> {
    public boolean a;

    public MyBoolean(boolean a) {
        this.a = a;
    }

    @Override
    public MyBoolean negate() {
        return new MyBoolean(!this.a);
    }

}

实际上,Java标准库使用这个确切的技巧来实现。Comparable

public interface Comparable<T> {
    int compareTo(T o);
}

答案 2

一般来说,不可以。

您可以使用技巧(如其他答案中所建议的)来使这项工作发挥作用,但它们并不能提供与Haskell typeclass相同的所有保证。具体来说,在Haskell中,我可以定义一个这样的函数:

doublyNegate :: Negatable t => t -> t
doublyNegate v = negate (negate v)

现在已知 的参数和返回值都是 。但 Java 的等价物是:doublyNegatet

public <T extends Negatable<T>> T doublyNegate (Negatable<T> v)
{
    return v.negate().negate();
}

没有,因为可以通过另一种类型实现:Negatable<T>

public class X implements Negatable<SomeNegatableClass> {
    public SomeNegatableClass negate () { return new SomeNegatableClass(); }
    public static void main (String[] args) { 
       new X().negate().negate();   // results in a SomeNegatableClass, not an X
}

对于此应用程序来说,这并不是特别严重,但确实会给其他 Haskell 类型类带来麻烦,例如.如果不使用其他对象并将该对象的实例发送到我们需要比较的值的任何位置,就无法实现Java类型类(例如:EquatableEquatable

public interface Equatable<T> {
    boolean equal (T a, T b);
}
public class MyClass
{
    String str;

    public static class MyClassEquatable implements Equatable<MyClass> 
    { 
         public boolean equal (MyClass a, MyClass b) { 
             return a.str.equals(b.str);
         } 
    }
}
...
public <T> methodThatNeedsToEquateThings (T a, T b, Equatable<T> eq)
{
    if (eq.equal (a, b)) { System.out.println ("they're equal!"); }
}  

(实际上,这正是Haskell实现类型类的方式,但它隐藏了从您那里传递的参数,因此您无需弄清楚要将哪个实现发送到哪里)

尝试仅使用普通的Java接口来执行此操作会导致一些违反直觉的结果:

public interface Equatable<T extends Equatable<T>>
{
    boolean equalTo (T other);
}
public MyClass implements Equatable<MyClass>
{
    String str;
    public boolean equalTo (MyClass other) 
    {
        return str.equals(other.str);
    }
}
public Another implements Equatable<MyClass>
{
    public boolean equalTo (MyClass other)
    {
        return true;
    }
}

....
MyClass a = ....;
Another b = ....;

if (b.equalTo(a))
    assertTrue (a.equalTo(b));
....

你会期望,由于它确实应该对称地定义,如果那里的语句编译,断言也会编译,但它不会,因为即使相反的方式是正确的,它也不能等同于。但是对于Haskell类型类,我们知道如果有效,那么也是有效的。[1]equalToifMyClassAnotherEquatableareEqual a bareEqual b a

接口与类型类的另一个限制是,类型类可以提供一种创建值的方法,该值在没有现有值(例如运算符 for )的情况下实现类型类,而对于接口,您必须已经具有该类型的对象才能调用其方法。returnMonad

你问这个限制是否有名字,但我不知道有一个。这仅仅是因为类型类实际上与面向对象的接口不同,尽管它们相似,因为它们是以这种根本不同的方式实现的:对象是其接口的子类型,因此直接携带接口方法的副本而不修改它们的定义,而类型类是一个单独的函数列表,每个函数都是通过替换类型变量来自定义的。类型和具有该类型实例的类型类之间没有子类型关系(例如,Haskell 不是 的子类型:只要函数需要能够比较其参数,并且这些参数恰好是 Integers),就可以传递一个实例。IntegerComparableComparable

[1]:Haskell 运算符实际上是使用类型类实现的,...我没有使用过这个,因为Haskell中的运算符重载可能会让不熟悉Haskell代码的人感到困惑。==Eq


推荐