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

时间:2018-08-08 03:17:15

标签: java haskell typeclass

问这个问题以澄清我对类型类和更高种类的类型的理解,我不是在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 个答案:

答案 0 :(得分: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);
}

答案 1 :(得分: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]

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

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

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

答案 2 :(得分: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方法。

答案 3 :(得分:7)

我将问题解释为

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

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

这是您的操作方式:

  1. 定义代表类型类的接口

    interface Negatable<T> {
      T negate(T t);
    }
    
  2. 实施一些机制,允许您注册类型类的实例用于各种类型。在这里,静态HashMap可以做到:

    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. 根据传递的对象的运行时类,定义使用上述机制的normalize方法:

      public static <T> T normalize(T t) {
        Negatable<T> inst = Negatable.<T>getInstance(t.getClass());
        return inst.negate(inst.negate(t));
      }
    
  4. 注册各种类的实际实例:

    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. 使用它!

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