Могу ли я определить отрицательный интерфейс в 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 ответа:
на самом деле, да. Не напрямую, но вы можете это сделать. Просто включите универсальный параметр, а затем производные от универсального типа.
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
. Нет никакого способа реализации JavaEquatable
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-решение, представленное ниже, может вызывать ошибки во время выполнения.
вот как вы можете сделать это:
определить интерфейс, представляющий класс
interface Negatable<T> { T negate(T t); }
выполнить какой-то механизм, который позволяет зарегистрировать экземпляры типа для различных типов. Вот, статический
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); }
определение
normalize
метод, который использует описанный выше механизм для получения соответствующего экземпляра на основе класса времени выполнения переданного объекта:public static <T> T normalize(T t) { Negatable<T> inst = Negatable.<T>getInstance(t.getClass()); return inst.negate(inst.negate(t)); }
Регистрация фактических экземпляров для разных классов:
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; } });
использовать это!
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)); } }