Могу ли я определить интерфейс Negatable в Java?

Задав этот вопрос, чтобы прояснить мое понимание классов типов и более высоких типов, я не ищу обходные пути в 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? Если это ограничение, связано ли это с поддержкой более высокого типа? Я думаю, что нет: похоже, это еще одно ограничение. Если да, то имеет ли это имя?

Спасибо, и, пожалуйста, дайте мне знать, если вопрос неясен!

Ответы

Ответ 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);
}

Ответ 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<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. Equatable реализовать Java Equatable typeclass без использования дополнительного объекта и отправки экземпляра этого объекта туда, куда мы отправляем значения, которые нужно сравнить (например:

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 хотя другой это правда. Но с Equatable класса Haskell Equatable мы знаем, что если areEqual ab работает, то также действительны areEqual ba. [1]

Другим ограничением интерфейсов по сравнению с типами классов является то, что класс типа может предоставить средство создания значения, которое реализует класс типа без наличия существующего значения (например, оператор return для Monad), тогда как для интерфейса у вас уже должен быть объект тип, чтобы иметь возможность ссылаться на его методы.

Вы спрашиваете, есть ли название для этого ограничения, но я не знаю об этом. Это просто потому, что классы типов на самом деле отличаются от объектно-ориентированных интерфейсов, несмотря на их сходство, потому что они реализованы в этом принципиально другом виде: объект является подтипом его интерфейса, поэтому он непосредственно использует копию методов интерфейса, не изменяя их а класс типа - это отдельный список функций, каждый из которых настраивается подстановкой переменных типа. Между типом и типом класса нет отношения подтипа, у которого есть экземпляр для типа (Haskell Integer не является подтипом Comparable, например: существует просто экземпляр Comparable который может передаваться всякий раз, когда функция должна быть в состоянии сравнить его параметры, и эти параметры оказываются целыми).

[1]: Оператор Haskell == фактически реализуется с использованием класса type, Eq... Я не использовал это, потому что перегрузка оператора в Haskell может смущать людей, не знакомых с чтением кода Haskell.

Ответ 3

Вы ищете дженерики, а также самонастраивающийся. Self typing - это понятие родового заполнителя, которое приравнивается к классу экземпляра.

Однако самонастраивание не существует в 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.

Ответ 4

Я интерпретирую этот вопрос как

Как мы можем реализовать ad-hoc-полиморфизм с использованием классных классов в 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'
    

Главный недостаток заключается в том, что, как уже упоминалось, поиск экземпляра typeclass может завершиться неудачно во время выполнения, а не во время компиляции (как в Haskell). Использование статической хэш-карты тоже субоптимально, поскольку оно приносит все проблемы общей глобальной переменной, это можно смягчить с помощью более сложных механизмов впрыска зависимостей. Автоматическое создание экземпляров typeclass из других экземпляров typeclass потребует еще большей инфраструктуры (может быть сделано в библиотеке). Но в принципе он реализует ad-hoc-полиморфизм с использованием классных классов в 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));
  }
}