【问题标题】:Can I define the Negatable interface in Java?我可以在 Java 中定义 Negatable 接口吗?
【发布时间】:2018-08-08 03:17:15
【问题描述】:

问这个问题是为了澄清我对类型类和更高种类的类型的理解,我不是在寻找 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 类型系统的基本限制? 如果是限制,它是否与更高种类有关类型支持?我认为不是:看起来这是另一种限制。如果有,它有名字吗?

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

【问题讨论】:

    标签: java haskell typeclass


    【解决方案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);
    }
    

    【讨论】:

    • 顺便说一句,在 C++ 中这被称为“curiously-recurring template pattern”。
    • 我认为更好的方式应该是public interface Negatable&lt;T extends Negatable&gt;,这样无论怎样都可以将negate链接起来。还是有理由不那样做?
    • 请注意,这完全等同于给定的 Haskell 类型类,因为类型类保证如果你有 v :: Negatable t =&gt; t 那么两者vnegate vt 类型的值,而在 Java 中,您可以有 Negatable&lt;T&gt; v 但实际上没有 v 类型为 t(例如,我可以定义 public class X implements Negatable&lt;Integer&gt; { public void negate () { return 1; } },它满足所有给定的条件,但 Haskell 将不允许这样做,因为它没有按照 typeclass 中指定的正确自反。
    • @Caleth:这不仅合法,而且是exactly how Enum is defined
    • @Wyzard:在其他任何地方,这被称为 F-bounded Polymorphism
    【解决方案2】:

    一般来说不会。

    可以使用技巧(如其他答案中所建议的那样)来完成这项工作,但它们并不能提供与 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&lt;T&gt; 可以由另一种类型实现:

    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 语句编译,断言也会编译,但它不会编译,因为 MyClass 不是与Another 不等同,即使反过来也是如此。但是对于 Haskell Equatable 类型类,我们知道如果 areEqual a b 有效,那么 areEqual b a 也是有效的。 [1]

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

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

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

    【讨论】:

    • 前几段似乎是无效的,因为 F 有界多态性:interface Neg&lt;T extends Neg&lt;T&gt;&gt; { T neg(); } 允许您根据需要多次调用t.neg().neg().neg(),并且总是得到一个T 返回:@987654348 @。也许我只是不太明白前几段在说什么。
    • @AndreyTyukin - 它可以编译,但您仍然不能保证执行结果与原始值的类型相同,因为(只要 T 实现 @ 987654350@) 除了T 之外的某些类型也可以实现Neg&lt;T&gt;。现在,对于这个特定示例,我看不出有什么原因会导致问题,但对于其他情况(如Equatable,如果我们可以调用equals(a,b)也可以拨打equals(b,a)),可能效果不太好。
    【解决方案3】:

    您正在寻找泛型,以及自键入。自键入是等同于实例类的通用占位符的概念。

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

    这可以通过泛型解决。

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

    然后

    public class MyBoolean implements Negatable<MyBoolean>{
    
        @Override
        public MyBoolean negate() {
            //your impl
        }
    
    }
    

    对实施者的一些影响:

    • 他们必须在实现接口时指定自己,例如MyBoolean implements Negatable&lt;MyBoolean&gt;
    • 扩展MyBoolean 需要再次覆盖negate 方法。

    【讨论】:

    • Self typing 可能是我要找的名字。你知道是否有语言本身支持自打字,而不使用这个技巧?
    • @zale Typescript 有this 类型,它使用函数所在的表达式的类型。
    • 你可以强制实现通过public interface Negatable&lt;T extends Negatable&lt;T&gt;&gt;来指定自己
    • @MarDev 这并不强制实现指定自己。示例:class A implements Negatable&lt;A&gt;{} class B implements Negatable&lt;A&gt; {}B 没有指定自己。
    • @zale Scala 允许使用 this 和 mix-ins 进行自我输入。例如自我类型特征:trait Foo { def name: String} trait Bar { this: Foo =&gt; def baz(....)}
    【解决方案4】:

    我把这个问题解释为

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

    可以在 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 中的类型类实现 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));
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-14
      • 2021-12-06
      • 2012-05-20
      • 1970-01-01
      相关资源
      最近更新 更多