create - set border java
¿Puedo definir la interfaz Negatable en Java? (4)
Actualmente, si. No directamente, pero puedes hacerlo. Simplemente incluya un parámetro genérico y luego derive del tipo genérico.
public interface Negatable<T> {
T negate();
}
public static <T extends Negatable<T>> T normalize(T a) {
return a.negate().negate();
}
Usted implementaría esta interfaz como tal
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);
}
}
De hecho, la biblioteca estándar de Java utiliza este truco exacto para implementar
Comparable
.
public interface Comparable<T> {
int compareTo(T o);
}
Al hacer esta pregunta para aclarar mi comprensión de las clases de tipo y los tipos de tipo más alto, no estoy buscando soluciones en Java.
En Haskell, podría escribir algo como
class Negatable t where
negate :: t -> t
normalize :: (Negatable t) => t -> t
normalize x = negate (negate x)
Luego, asumiendo que
Bool
tiene una instancia de
Negatable
,
v :: Bool
v = normalize True
Y todo funciona bien.
En Java, no parece posible declarar una interfaz
Negatable
adecuada.
Podríamos escribir:
interface Negatable {
Negatable negate();
}
Negatable normalize(Negatable a) {
a.negate().negate();
}
Pero entonces, a diferencia de Haskell, lo siguiente no se compilaría sin una
MyBoolean
(suponga que
MyBoolean
implementa
Negatable
):
MyBoolean val = normalize(new MyBoolean()); // does not compile; val is a Negatable, not a MyBoolean
¿Hay alguna manera de referirse al tipo de implementación en una interfaz Java, o es esta una limitación fundamental del sistema de tipo Java? Si es una limitación, ¿está relacionada con el soporte de tipo de tipo superior? Creo que no: parece que este es otro tipo de limitación. Si es así, ¿tiene un nombre?
Gracias, y hágamelo saber si la pregunta no está clara.
En general, no.
Puede usar trucos (como se sugiere en las otras respuestas) que harán que esto funcione, pero no ofrecen todas las mismas garantías que la clase de tipos de Haskell. Específicamente, en Haskell, podría definir una función como esta:
doublyNegate :: Negatable t => t -> t
doublyNegate v = negate (negate v)
Ahora se sabe que el argumento y el valor de retorno de
doublyNegate
son
t
.
Pero el equivalente de Java:
public <T extends Negatable<T>> T doublyNegate (Negatable<T> v)
{
return v.negate().negate();
}
no, porque
Negatable<T>
podría ser implementado por otro tipo:
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
}
Esto no es particularmente grave para esta aplicación, pero causa problemas para otras clases de tipos de Haskell, por ejemplo,
Equatable
.
No hay forma de implementar una
Equatable
tipos Java
Equatable
sin usar un objeto adicional y enviar una instancia de ese objeto a cualquier lugar donde enviemos valores que deban compararse (por ejemplo:
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!"); }
}
(De hecho, esta es exactamente la forma en que Haskell implementa las clases de tipo, pero oculta el parámetro que pasa de usted para que no tenga que averiguar qué implementación enviar a dónde)
Tratar de hacer esto solo con interfaces Java simples lleva a algunos resultados contraintuitivos:
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));
....
Usted esperaría, debido al hecho de que
equalTo
realmente debería definirse simétricamente, que si la declaración
if
se compila, la afirmación también se compilará, pero no lo hace, porque
MyClass
no es compatible con
Another
, aunque la otra todo es verdad
Pero con una clase de tipo Haskell
Equatable
, sabemos que si
areEqual ab
funciona,
areEqual ba
también es válido.
[1]
Otra limitación de las interfaces frente a las clases de tipo es que una clase de tipo puede proporcionar un medio para crear un valor que implementa la clase de tipo sin tener un valor existente (p. Ej., El operador de
return
para la
Monad
), mientras que para una interfaz ya debe tener un objeto de El tipo para poder invocar sus métodos.
Usted pregunta si hay un nombre para esta limitación, pero no conozco uno.
Es simplemente porque las clases de tipos son realmente diferentes a las interfaces orientadas a objetos, a pesar de sus similitudes, porque se implementan de esta manera fundamentalmente diferente: un objeto es un subtipo de su interfaz, por lo tanto, lleva una copia de los métodos de la interfaz directamente sin modificar su definición, mientras que una clase de tipo es una lista separada de funciones, cada una de las cuales se personaliza mediante la sustitución de variables de tipo.
No existe una relación de subtipo entre un tipo y una clase de tipo que tiene una instancia para el tipo (un
Integer
Haskell no es un subtipo de
Comparable
, por ejemplo: simplemente existe una instancia
Comparable
que se puede pasar cuando una función necesita Ser capaz de comparar sus parámetros y esos parámetros resultan ser enteros).
[1]: El operador Haskell
==
realidad se implementa usando una clase de tipo,
Eq
... No he usado esto porque la sobrecarga del operador en Haskell puede ser confusa para las personas que no están familiarizadas con la lectura del código de Haskell.
Estás buscando genéricos, además de auto escribir. La auto escritura es la noción de marcador de posición genérico que equivale a la clase de la instancia.
Sin embargo, la auto escritura no existe en java.
Esto se puede resolver con genéricos sin embargo.
public interface Negatable<T> {
public T negate();
}
Entonces
public class MyBoolean implements Negatable<MyBoolean>{
@Override
public MyBoolean negate() {
//your impl
}
}
Algunas implicaciones para los implementadores:
-
Deben especificarse cuando implementen la interfaz, por ejemplo, los
MyBoolean implements Negatable<MyBoolean>
-
Extender
MyBoolean
requeriría uno para anular de nuevo el método denegate
.
Interpreto la pregunta como
¿Cómo podemos implementar el polimorfismo ad-hoc utilizando clases de tipos en Java?
Puede hacer algo muy similar en Java, pero sin las garantías de seguridad de tipo de Haskell: la solución que se presenta a continuación puede generar errores en el tiempo de ejecución.
Aquí sabrás como podrás hacerlo:
-
Definir la interfaz que representa la clase de tipos.
interface Negatable<T> { T negate(T t); }
-
Implemente algún mecanismo que le permita registrar instancias de la clase de tipos para varios tipos. Aquí, un
HashMap
estático hará: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); }
-
Defina el método de
normalize
que utiliza el mecanismo anterior para obtener la instancia adecuada en función de la clase de tiempo de ejecución del objeto pasado:public static <T> T normalize(T t) { Negatable<T> inst = Negatable.<T>getInstance(t.getClass()); return inst.negate(inst.negate(t)); }
-
Registrar instancias reales para varias clases:
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; } });
-
¡Úsalo!
System.out.println(normalize(false)); // Boolean `false` System.out.println(normalize(42)); // Integer `42`
El principal inconveniente es que, como ya se mencionó, la búsqueda de instancia de typeclass puede fallar en tiempo de ejecución, no en tiempo de compilación (como en Haskell). El uso de un mapa hash estático también es subóptimo, ya que trae todos los problemas de una variable global compartida, esto podría mitigarse con mecanismos de inyección de dependencia más sofisticados. La generación automática de instancias de clase de tipos a partir de otras instancias de clase de tipos, requeriría aún más infraestructura (podría hacerse en una biblioteca). Pero, en principio, implementa un polimorfismo ad-hoc que utiliza clases de tipos en Java.
Código completo:
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));
}
}