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


задав этот вопрос, чтобы прояснить мое понимание классов типов и более высоких типов kinded, я не ищу обходных путей в 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 52

4 ответа:

на самом деле, да. Не напрямую, но вы можете это сделать. Просто включите универсальный параметр, а затем производные от универсального типа.

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

в общем, нет.

вы можете используйте трюки (как предложено в других ответах), которые сделают эту работу, но они не обеспечивают все те же гарантии, что и класс 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 typeclasses, например 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 не equatable с Another хотя все наоборот верно. Но с Хаскеллом Equatable тип класса, мы знаем, что если areEqual a b работает, то areEqual b a также является допустимым. [1]

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

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

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

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

однако самостоятельная типизация не существует в 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 опять способ.

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

как мы можем реализовать ad-hoc полиморфизм с помощью typeclasses в 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 полиморфизм, используя typeclasses в 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));
  }
}