it-swarm.com.ru

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

Задавая этот вопрос, чтобы прояснить мое понимание классов типов и типов с более высоким родом, я не ищу обходных путей в Java.


В Хаскеле я мог написать что-то вроде

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

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

52
zale

В общем нет.

Вы можете использовать хитрости (как предложено в других ответах), которые сделают эту работу, но они не предоставляют все те же гарантии, что и класс типов 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. Невозможно реализовать класс типов 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]

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

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

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

12
Jules

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

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);
}
63
Silvio Mayolo

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

Как мы можем реализовать специальный полиморфизм, используя классы типов в 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`
    

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

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

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