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

zal*_*ale 52 java haskell typeclass

问这个问题是为了澄清我对类型类和更高级别类型的理解,我不是在寻找Java中的变通方法.


在Haskell中,我可以写出像

class Negatable t where
    negate :: t -> t

normalize :: (Negatable t) => t -> t
normalize x = negate (negate x)
Run Code Online (Sandbox Code Playgroud)

假设Bool有一个实例Negatable,

v :: Bool
v = normalize True
Run Code Online (Sandbox Code Playgroud)

一切正常.


在Java中,似乎不可能声明适当的Negatable接口.我们可以写:

interface Negatable {
    Negatable negate();
}

Negatable normalize(Negatable a) {
    a.negate().negate();
}
Run Code Online (Sandbox Code Playgroud)

但是,与Haskell不同,以下不会在没有强制转换的情况下编译(假设MyBoolean工具Negatable):

MyBoolean val = normalize(new MyBoolean()); // does not compile; val is a Negatable, not a MyBoolean
Run Code Online (Sandbox Code Playgroud)

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

谢谢,如果问题不清楚,请告诉我!

Sil*_*olo 62

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

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

public static <T extends Negatable<T>> T normalize(T a) {
    return a.negate().negate();
}
Run Code Online (Sandbox Code Playgroud)

您可以像这样实现此接口

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

}
Run Code Online (Sandbox Code Playgroud)

事实上,Java标准库使用这个精确的技巧来实现Comparable.

public interface Comparable<T> {
    int compareTo(T o);
}
Run Code Online (Sandbox Code Playgroud)

  • 顺便说一句,在C++中,这被称为"[奇怪的 - 重复出现的模板模式](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern)". (27认同)
  • 注意,这不是*与给定的Haskell类型类完全等价,因为类型类*保证*如果你有`v :: Negatable t => t`那么`v`和`negate v`都是值输入`t`,而在Java中你可以有`Negatable <T> v`但是'v`实际上不是`t`类型(例如我可以定义`public class X implements Negatable <Integer> {public void negate (){return 1;}}`,它满足所有给定的条件,但Haskell因为没有像类型类中指定的那样正确反身而不允许这样做. (8认同)
  • @Wyzard:在其他地方,这被称为*F-有界多态*. (8认同)
  • @Caleth:这不仅是合法的,而且[确切地说Enum的定义](https://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html). (4认同)
  • 我认为更好的方法应该是`public interface Negatable <T extends Negatable>`,这样无论如何都可以将否定链接起来.或者有没有理由不这样做? (3认同)
  • 这是一种错误的多态性.它允许您添加*Negatable`接口的*更多实现*与*相同的函数*.Haskell的ad-hoc多态性允许您在*相同数据*上添加*更多函数*.如果你需要更多具有相同功能的"东西"(例如千种不同的GUI小部件和一些方法,比如`repaint`和`reactOnMouseClick`),那么这种方法可以很好地扩展,另一种多态可以让你添加更多*函数*在相同的数据上(从`Bool`开始,你可以稍后在`Negatable`,`Monoid`,`Lattice`,`Ring`等等).它的比例不同. (3认同)

Jul*_*les 12

一般来说,没有.

可以使用技巧(如其他答案中所建议的那样),但它们不能提供Haskell类型类所做的所有相同保证.具体来说,在Haskell中,我可以定义一个这样的函数:

doublyNegate :: Negatable t => t -> t
doublyNegate v = negate (negate v)
Run Code Online (Sandbox Code Playgroud)

现在已知参数和返回值doublyNegate都是t.但Java等价:

public <T extends Negatable<T>> T doublyNegate (Negatable<T> v)
{
    return v.negate().negate();
}
Run Code Online (Sandbox Code Playgroud)

没有,因为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
}
Run Code Online (Sandbox Code Playgroud)

对于这个应用程序来说这并不是特别严重,但确实会给其他Haskell类型类带来麻烦,例如Equatable.Equatable没有使用额外的对象并且在我们发送需要比较的值的任何地方发送该对象的实例时,无法实现Java 类型类(例如:

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!"); }
}  
Run Code Online (Sandbox Code Playgroud)

(事实上​​,这正是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));
....
Run Code Online (Sandbox Code Playgroud)

equalTo应该期望,由于真的应该对称地定义,如果if那里的语句编译,断言也会编译,但它不会,因为即使相反的方式是真的MyClass,Another也不是等于.但是对于Haskell Equatable类型类,我们知道如果areEqual a b有效,那么areEqual b a也是有效的.[1]

接口与类型类的另一个限制是类型类可以提供一种创建值的方法,该值实现类型类而不具有现有值(例如,return运算符Monad),而对于接口,您必须已经具有该类型的对象为了能够调用它的方法.

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

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

  • @AndreyTyukin - 它编译,但你仍然没有保证在执行结果时与原始值的类型相同,因为(只要`T`实现`Neg <T>`)某种类型除了`T`之外,还可以实现`Neg <T>`.现在,*对于这个特定的例子*,我看不出任何可能导致问题的原因,但对于其他情况(如`Equatable`,如果我们可以调用`equals(a,b)`我们可以预期也可以称为'equals(b,a)`),这可能效果不佳. (3认同)

Tay*_*lor 7

你正在寻找泛型,加上自我打字.自键型是通用占位符的概念,它等同于实例的类.

但是,java中不存在自我键入.

这可以通过泛型来解决.

public interface Negatable<T> {
    public T negate();
}
Run Code Online (Sandbox Code Playgroud)

然后

public class MyBoolean implements Negatable<MyBoolean>{

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

}
Run Code Online (Sandbox Code Playgroud)

对实施者的一些影响:

  • 他们必须在实现界面时指定自己,例如 MyBoolean implements Negatable<MyBoolean>
  • 扩展MyBoolean需要negate再次覆盖该方法.

  • @MarDev这并不强制实现指定自己.示例:`类A实现Negatable <A> {}类B实现Negatable <A> {}`.`B`没有指定自己. (2认同)

And*_*kin 7

我将这个问题解释为

我们如何在Java中使用类型类实现ad-hoc多态?

可以在Java中执行非常类似的操作,但没有Haskell的类型安全保证 - 下面介绍的解决方案可能会在运行时抛出错误.

以下是如何做到这一点:

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

    interface Negatable<T> {
      T negate(T t);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 定义normalize使用上述机制的方法,以根据传递的对象的运行时类获取适当的实例:

      public static <T> T normalize(T t) {
        Negatable<T> inst = Negatable.<T>getInstance(t.getClass());
        return inst.negate(inst.negate(t));
      }
    
    Run Code Online (Sandbox Code Playgroud)
  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;
      }
    });
    
    Run Code Online (Sandbox Code Playgroud)
  5. 用它!

    System.out.println(normalize(false)); // Boolean `false`
    System.out.println(normalize(42));    // Integer `42`
    
    Run Code Online (Sandbox Code Playgroud)

主要缺点是,如前所述,类型类实例查找可能在运行时失败,而不是在编译时失败(如在Haskell中).使用静态哈希映射也是次优的,因为它带来了共享全局变量的所有问题,这可以通过更复杂的依赖注入机制来缓解.从其他类型类实例自动生成类型类实例将需要更多的基础结构(可以在库中完成).但原则上,它在Java中使用类型类实现了ad-hoc多态.

完整代码:

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));
  }
}
Run Code Online (Sandbox Code Playgroud)