在Java中,我能否定义Negatable接口?

52

我提出这个问题是为了澄清我对类型类和高阶类型的理解,不是在寻找Java的变通方法。


在Haskell中,我可以写出以下代码:

class Negatable t where
    negate :: t -> t

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

假设Bool有一个Negatable实例,
v :: Bool
v = normalize True

一切正常。


在Java中,似乎不可能声明一个合适的 Negatable 接口。我们可以编写:

interface Negatable {
    Negatable negate();
}

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

然而,与Haskell不同的是,以下代码将无法编译通过(假设MyBoolean实现了Negatable):

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

有没有办法在Java接口中引用实现类型,或者这是Java类型系统的根本限制?如果是限制,它是否与高级类型支持相关?我认为不是:看起来这是另一种限制。如果是这样,它有一个名称吗?
谢谢,如果问题不清楚,请告诉我!
4个回答

63

实际上可以。虽然不能直接实现,但是您可以通过包含一个通用参数并从通用类型派生来实现。

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);
}

27
顺带提一句,在C++中这被称为“奇异递归模板模式”(Curiously Recurring Template Pattern)。 - Wyzard
3
我认为更好的方式应该是 public interface Negatable<T extends Negatable>,这样无论如何都可以链接否定操作。或者有什么原因不这样做吗? - kutschkem
8
请注意,这并不完全等同于给定的Haskell类型类,因为类型类保证如果您有v :: Negatable t => t,那么vnegate v都是类型为t的值,而在Java中,您可以拥有Negatable <T> v但实际上没有v是类型t(例如,我可以定义public class X implements Negatable<Integer> { public void negate() {return 1; }},它满足所有给定条件,但由于不符合类型类中指定的正确反射性,Haskell将禁止此操作。 - Jules
4
这不仅是合法的,而且正是枚举类型的定义方式(参见https://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html)。 - BlueRaja - Danny Pflughoeft
8
@Wyzard:在其他地方,这被称为F边界多态性 - Jörg W Mittag
显示剩余3条评论

12
一般来说,不行。
你可以使用一些技巧(如其他答案中建议的),使其工作,但它们不能提供Haskell类型类所提供的所有保证。具体来说,在Haskell中,我可以定义这样一个函数:
doublyNegate :: Negatable t => t -> t
doublyNegate v = negate (negate v)

现在已知 doublyNegate 的参数和返回值都是 t。但 Java 中的相应函数为:
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类型类带来麻烦,例如 Equatable。没有办法实现Java Equatable 类型类而不使用额外的对象,并将该对象的实例发送到需要比较值的任何地方(例如:

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));
....

由于equalTo应该被对称地定义,因此您预期如果那里的if语句编译,则断言也会编译,但实际上并没有,因为MyClassAnother不可比较,即使相反情况是正确的。 但是使用Haskell的Equatable类型类,我们知道如果areEqual a b有效,则areEqual b a也是有效的。 [1]
接口与类型类的另一个限制是,类型类可以提供创建实现类型类的值的方法,而无需存在现有值(例如Monadreturn运算符),而对于接口,必须已经拥有该类型的对象才能调用其方法。
您问是否有这种限制的名称,但我不知道。这只是因为尽管它们具有相似之处,但类型类实际上与面向对象接口不同,因为它们以根本性不同的方式实现:对象是其接口的子类型,因此直接携带接口的方法副本而不修改其定义,而类型类是一系列函数列表,每个函数都通过替换类型变量进行自定义。类型和具有该类型实例的类型类之间不存在子类型关系(例如,Haskell的Integer不是Comparable的子类型,只是存在一个Comparable实例,每当函数需要能够比较其参数并且这些参数恰好是整数时,该实例就可以传递)。
[1]:Haskell的==运算符实际上使用类型类Eq实现...我没有使用它,因为Haskell中的运算符重载可能会使不熟悉阅读Haskell代码的人感到困惑。

似乎前几段因为F-bounded多态而无效:interface Neg<T extends Neg<T>> { T neg(); }允许您随意调用t.neg().neg().neg(),并始终获得一个T<T extends Neg<T>> T foo(T t) { return t.neg().neg().neg(); }。也许我只是没有完全理解前几段在谈论什么。 - Andrey Tyukin
4
它可以编译,但仍无法保证执行结果与原始值具有相同的类型,因为只要T实现了Neg<T>,某些类型(而不是T)也可以实现Neg<T>。对于此特定示例,我看不出任何原因会导致问题,但对于其他情况(例如Equatable),人们期望如果我们可以调用equals(a,b),也可以调用equals(b,a),这可能效果不佳。 - Jules

7
您正在寻找泛型和自我类型。 自我类型是一种等同于实例类的通用占位符的概念。
然而,在Java中不存在自我类型。
您可以通过泛型接近,但它很笨重:
public interface Negatable<T> {
    public T negate();
}

那么

public class MyBoolean implements Negatable<MyBoolean>{

    @Override
    public MyBoolean negate() {
        //your impl
    }
    
}

对于实现者的一些影响:

  • 他们在实现接口时必须自行指定,例如 MyBoolean implements Negatable<MyBoolean>
  • 扩展 MyBoolean 需要再次重写 negate 方法。

自我打字可能是我正在寻找的名称。您知道是否有语言本地支持自我打字,而不使用这种技巧吗? - zale
1
Typescript有this类型,它使用函数所在表达式的类型。 - Octavia Togami
1
你可以通过 public interface Negatable<T extends Negatable<T>> 强制实现类来指定自己。 - Mario Ishac
2
@MarDev 这并不强制实现指定自己。例如:class A implements Negatable<A>{} class B implements Negatable<A> {}B 没有指定自己。 - Olivier Grégoire
1
@zale Scala允许使用this和mix-ins进行自我类型化。例如,要对trait进行自我类型化:trait Foo { def name: String} trait Bar { this: Foo => def baz(....)} - LinkBerest

7

我理解这个问题是:

如何在Java中使用类型类实现临时多态?

你可以在Java中做类似的事情,但是没有Haskell的类型安全保证 - 下面介绍的解决方案可能会在运行时抛出错误。

以下是实现方法:

  1. Define interface that represents the typeclass

    interface Negatable<T> {
      T negate(T t);
    }
    
  2. Implement some mechanism that allows you to register instances of the typeclass for various types. Here, a static HashMap will do:

    static HashMap<Class<?>, Negatable<?>> instances = new HashMap<>();
    static <T> void registerInstance(Class<T> clazz, Negatable<T> inst) {
      instances.put(clazz, inst);
    }
    @SuppressWarnings("unchecked")
    static <T> Negatable<T> getInstance(Class<?> clazz) {
      return (Negatable<T>)instances.get(clazz);
    }
    
  3. Define the normalize method that uses the above mechanism to get the appropriate instance based on the runtime class of the passed object:

      public static <T> T normalize(T t) {
        Negatable<T> inst = Negatable.<T>getInstance(t.getClass());
        return inst.negate(inst.negate(t));
      }
    
  4. Register actual instances for various classes:

    Negatable.registerInstance(Boolean.class, new Negatable<Boolean>() {
      public Boolean negate(Boolean b) {
        return !b;
      }
    });
    
    Negatable.registerInstance(Integer.class, new Negatable<Integer>() {
      public Integer negate(Integer i) {
        return -i;
      }
    });
    
  5. Use it!

    System.out.println(normalize(false)); // Boolean `false`
    System.out.println(normalize(42));    // Integer `42`
    
主要缺点是,如前所述,类型类实例查找可能会在运行时而不是编译时失败(与Haskell不同)。使用静态哈希映射也不是最佳选择,因为它带来了共享全局变量的所有问题,这可以通过更复杂的依赖注入机制来缓解。从其他类型类实例自动生成类型类实例需要更多的基础设施(可以在库中完成)。但原则上,它使用Java中的类型类实现特定的多态性。
import java.util.HashMap;

class TypeclassInJava {
  
  static interface Negatable<T> {
    T negate(T t);

    static HashMap<Class<?>, Negatable<?>> instances = new HashMap<>();
    static <T> void registerInstance(Class<T> clazz, Negatable<T> inst) {
      instances.put(clazz, inst);
    }
    @SuppressWarnings("unchecked")
    static <T> Negatable<T> getInstance(Class<?> clazz) {
      return (Negatable<T>)instances.get(clazz);
    }
  }

  public static <T> T normalize(T t) {
    Negatable<T> inst = Negatable.<T>getInstance(t.getClass());
    return inst.negate(inst.negate(t));
  }

  static {
    Negatable.registerInstance(Boolean.class, new Negatable<Boolean>() {
      public Boolean negate(Boolean b) {
        return !b;
      }
    });
  
    Negatable.registerInstance(Integer.class, new Negatable<Integer>() {
      public Integer negate(Integer i) {
        return -i;
      }
    });
  }

  public static void main(String[] args) {
    System.out.println(normalize(false));
    System.out.println(normalize(42));
  }
}

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