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

时间:2022-02-27 17:04:26

Asking this question to clarify my understanding of type classes and higher kinded types, I'm not looking for workarounds in Java.

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


In Haskell, I could write something like

在Haskell中,我可以写出类似的东西

class Negatable t where
    negate :: t -> t

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

Then assuming Bool has an instance of Negatable,

然后假设Bool有一个Negatable实例,

v :: Bool
v = normalize True

And everything works fine.

一切正常。


In Java, it does not seem possible to declare a proper Negatable interface. We could write:

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

interface Negatable {
    Negatable negate();
}

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

But then, unlike in Haskell, the following would not compile without a cast (assume MyBoolean implements Negatable):

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

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

Is there a way to refer to the implementing type in a Java interface, or is this a fundamental limitation of the Java type system? If it is a limitation, is it related to higher-kinded type support? I think not: it looks like this is another sort of limitation. If so, does it have a name?

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

Thanks, and please let me know if the question is unclear!

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

4 个解决方案

#1


59  

Actually, yes. Not directly, but you can do it. Simply include a generic parameter and then derive from the generic type.

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

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

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

You would implement this interface like so

您可以像这样实现此接口

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

}

In fact, the Java standard library uses this exact trick to implement Comparable.

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

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

#2


11  

In general, no.

一般来说,没有。

You can use tricks (as suggested in the other answers) that will make this work, but they do not provide all of the same guarantees that the Haskell typeclass does. Specifically, in Haskell, I could define a function like this:

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

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

It is now known that the argument and return value of doublyNegate are both t. But the Java equivalent:

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

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

doesn't, because Negatable<T> could be implemented by another type:

不,因为Negatable 可以用另一种类型实现:

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
}

This isn't particularly serious for this application, but does cause trouble for other Haskell typeclasses, e.g. Equatable. There is no way of implementing a Java Equatable typeclass without using an additional object and sending an instance of that object around wherever we send values that need comparing, (e.g:

这对于这个应用程序来说并不是特别严重,但确实会给其他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!"); }
}  

(In fact, this is exactly how Haskell implements type classes, but it hides the parameter passing from you so you don't need to figure out which implementation to send where)

(实际上,这正是Haskell实现类型类的方式,但它隐藏了从您传递的参数,因此您无需确定要在哪个实现发送的位置)

Trying to do this with just plain Java interfaces leads to some counterintuitive results:

尝试使用普通的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));
....

You'd expect, due to the fact that equalTo really ought to be defined symmetrically, that if the if statement there compiles, the assertion would also compile, but it doesn't, because MyClass isn't equatable with Another even though the other way around is true. But with a Haskell Equatable type class, we know that if areEqual a b works, then areEqual b a is also valid. [1]

你期望,由于equalTo实际上应该对称定义,如果if语句编译,断言也会编译,但它不会,因为MyClass不等于另一个,即使另一个方式是真的。但是对于Haskell Equatable类型类,我们知道如果areEqual a b有效,那么areEqual b a也是有效的。 [1]

Another limitation of interfaces versus type classes is that a type class can provide a means of creating a value which implements the type class without having an existing value (e.g. the return operator for Monad), whereas for an interface you must already have an object of the type in order to be able to invoke its methods.

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

You ask whether there is a name for this limitation, but I'm not aware of one. It's simply because type classes are actually different to object-oriented interfaces, despite their similarities, because they are implemented in this fundamentally different way: an object is a subtype of its interface, thus carries around a copy of the interface's methods directly without modifying their definition, while a type class is a separate list of functions each of which is customised by substituting type variables. There is no subtype relationship between a type and a type class that has an instance for the type (a Haskell Integer isn't a subtype of Comparable, for example: there simply exists a Comparable instance that can be passed around whenever a function needs to be able to compare its parameters and those parameters happen to be Integers).

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

[1]: The Haskell == operator is actually implemented using a type class, Eq ... I haven't used this because operator overloading in Haskell can be confusing to people not familiar with reading Haskell code.

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

#3


7  

You're looking for generics, plus self typing. Self typing is the notion of generic placeholder that equates to the class of the instance.

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

However, self typing doesn't exist in java.

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

This can be solved with generics though.

这可以通过泛型来解决。

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

Then

然后

public class MyBoolean implements Negatable<MyBoolean>{

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

}

Some implications for implementers:

对实施者的一些影响:

  • They must specify themselves when they implement the interface, e.g. MyBoolean implements Negatable<MyBoolean>
  • 他们必须在实现界面时指定自己,例如MyBoolean实现Negatable
  • Extending MyBoolean would require one to override the negate method again.
  • 扩展MyBoolean将需要再次覆盖否定方法。

#4


5  

I interpret the question as

我将这个问题解释为

How can we implement ad-hoc polymorphism using typeclasses in Java?

You can do something very similar in Java, but without the type safety guarantees of Haskell - the solution presented below can throw errors at runtime.

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

Here is how you can do it:

以下是如何做到这一点:

  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:

    实现一些允许您为各种类型注册类型类的实例的机制。这里,静态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. Define the normalize method that uses the above mechanism to get the appropriate instance based on the runtime class of the passed object:

    定义使用上述机制的normalize方法,以根据传递的对象的运行时类获取适当的实例:

      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`
    

The main drawback is that, as already mentioned, the typeclass instance lookup can fail at runtime, not at compile-time (as in Haskell). Using a static hash map is suboptimal too, because it brings all the problems of a shared global variable, this could be mitigated with more sophisticated dependency injection mechanisms. Automatically generating typeclass instances from other typeclass instances, would require even more infrastructure (could be done in a library). But in principle, it implements ad-hoc polymorphism using typeclasses in Java.

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

Full code:

完整代码:

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

#1


59  

Actually, yes. Not directly, but you can do it. Simply include a generic parameter and then derive from the generic type.

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

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

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

You would implement this interface like so

您可以像这样实现此接口

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

}

In fact, the Java standard library uses this exact trick to implement Comparable.

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

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

#2


11  

In general, no.

一般来说,没有。

You can use tricks (as suggested in the other answers) that will make this work, but they do not provide all of the same guarantees that the Haskell typeclass does. Specifically, in Haskell, I could define a function like this:

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

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

It is now known that the argument and return value of doublyNegate are both t. But the Java equivalent:

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

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

doesn't, because Negatable<T> could be implemented by another type:

不,因为Negatable 可以用另一种类型实现:

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
}

This isn't particularly serious for this application, but does cause trouble for other Haskell typeclasses, e.g. Equatable. There is no way of implementing a Java Equatable typeclass without using an additional object and sending an instance of that object around wherever we send values that need comparing, (e.g:

这对于这个应用程序来说并不是特别严重,但确实会给其他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!"); }
}  

(In fact, this is exactly how Haskell implements type classes, but it hides the parameter passing from you so you don't need to figure out which implementation to send where)

(实际上,这正是Haskell实现类型类的方式,但它隐藏了从您传递的参数,因此您无需确定要在哪个实现发送的位置)

Trying to do this with just plain Java interfaces leads to some counterintuitive results:

尝试使用普通的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));
....

You'd expect, due to the fact that equalTo really ought to be defined symmetrically, that if the if statement there compiles, the assertion would also compile, but it doesn't, because MyClass isn't equatable with Another even though the other way around is true. But with a Haskell Equatable type class, we know that if areEqual a b works, then areEqual b a is also valid. [1]

你期望,由于equalTo实际上应该对称定义,如果if语句编译,断言也会编译,但它不会,因为MyClass不等于另一个,即使另一个方式是真的。但是对于Haskell Equatable类型类,我们知道如果areEqual a b有效,那么areEqual b a也是有效的。 [1]

Another limitation of interfaces versus type classes is that a type class can provide a means of creating a value which implements the type class without having an existing value (e.g. the return operator for Monad), whereas for an interface you must already have an object of the type in order to be able to invoke its methods.

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

You ask whether there is a name for this limitation, but I'm not aware of one. It's simply because type classes are actually different to object-oriented interfaces, despite their similarities, because they are implemented in this fundamentally different way: an object is a subtype of its interface, thus carries around a copy of the interface's methods directly without modifying their definition, while a type class is a separate list of functions each of which is customised by substituting type variables. There is no subtype relationship between a type and a type class that has an instance for the type (a Haskell Integer isn't a subtype of Comparable, for example: there simply exists a Comparable instance that can be passed around whenever a function needs to be able to compare its parameters and those parameters happen to be Integers).

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

[1]: The Haskell == operator is actually implemented using a type class, Eq ... I haven't used this because operator overloading in Haskell can be confusing to people not familiar with reading Haskell code.

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

#3


7  

You're looking for generics, plus self typing. Self typing is the notion of generic placeholder that equates to the class of the instance.

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

However, self typing doesn't exist in java.

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

This can be solved with generics though.

这可以通过泛型来解决。

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

Then

然后

public class MyBoolean implements Negatable<MyBoolean>{

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

}

Some implications for implementers:

对实施者的一些影响:

  • They must specify themselves when they implement the interface, e.g. MyBoolean implements Negatable<MyBoolean>
  • 他们必须在实现界面时指定自己,例如MyBoolean实现Negatable
  • Extending MyBoolean would require one to override the negate method again.
  • 扩展MyBoolean将需要再次覆盖否定方法。

#4


5  

I interpret the question as

我将这个问题解释为

How can we implement ad-hoc polymorphism using typeclasses in Java?

You can do something very similar in Java, but without the type safety guarantees of Haskell - the solution presented below can throw errors at runtime.

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

Here is how you can do it:

以下是如何做到这一点:

  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:

    实现一些允许您为各种类型注册类型类的实例的机制。这里,静态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. Define the normalize method that uses the above mechanism to get the appropriate instance based on the runtime class of the passed object:

    定义使用上述机制的normalize方法,以根据传递的对象的运行时类获取适当的实例:

      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`
    

The main drawback is that, as already mentioned, the typeclass instance lookup can fail at runtime, not at compile-time (as in Haskell). Using a static hash map is suboptimal too, because it brings all the problems of a shared global variable, this could be mitigated with more sophisticated dependency injection mechanisms. Automatically generating typeclass instances from other typeclass instances, would require even more infrastructure (could be done in a library). But in principle, it implements ad-hoc polymorphism using typeclasses in Java.

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

Full code:

完整代码:

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