Могу ли я определить интерфейс 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 - представленное ниже решение может вызывать ошибки во время выполнения.
Вот как вы можете это сделать:
-
Определить интерфейс, представляющий класс
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-полиморфизм с использованием классных классов в 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));
}
}