2011-02-01 12 views
13

Quiero definir una clase Functor en Java. Esto funciona:Genéricos de Java: cómo codificar una interfaz de Functor en Java?

//a Function 
public interface F<A,R> { 
    public R apply(A a); 
} 

public interface Functor<A> { 
    public <B> Functor<B> fmap(F<A,B> f); 
} 

Sin embargo, el valor de retorno de fmap no debería haber Functor, pero la subclase apropiada. Por lo general, esto se puede codificar con el CRTP, pero aquí parece que toco una pared debido al parámetro adicional A. P.ej. los siguientes y similares codificaciones no funcionan ("parámetro de tipo finst no está dentro de sus límites"):

public interface Functor<A, FInst extends Functor<A,FInst>> { 
    public <B, I extends Functor<B,FInst>> I fmap(F<A,B> f); 
} 

[Aclaración]

Con "subclase apropiada" I significa que el tipo de la clase siendo llamado a sí mismo. P.ej. Las listas son funtores, por lo que me gustaría escribir algo como

public class ListFunctor<A> implements ??? { 
    final private List<A> list; 
    public ListFunctor(List<A> list) { 
    this.list = list; 
    } 

    @Override 
    <B> ListFunctor<B> fmap(F<A,B> f) { 
    List<B> result = new ArrayList<B>(); 
    for(A a: list) result.add(f.apply(a)); 
    return new ListFunctor<B>(result); 
    } 
} 

Soy consciente de que podría escribir esto incluso con la primera definición que di (porque los tipos de retorno covariantes están permitidos), pero yo quiero que el retorno tipo "ListFunctor" es cumplir por el sistema de tipos (por lo que no puedo devolver un FooFunctor en su lugar), lo que significa que la interfaz Functor tiene que devolver el "tipo de auto" (al menos por lo que se le llama en otros idiomas)

[Resultados]

por lo que parece lo que quiero es imposible. Aquí es un relacionado el blog post: http://blog.tmorris.net/higher-order-polymorphism-for-pseudo-java/

+1

Probablemente es una mala forma tener más de 3 parámetros de tipo, especialmente los que incluyen al otro, ya que la declaración se convierte en un gran desastre y nadie sabe qué está pasando. –

+1

Existe una diferencia con respecto al lado del "escritor de la biblioteca" y el lado del uso. Y a menudo puede "ocultar" genéricos, incluso con la inferencia de tipo limitada de Java (por ejemplo, mediante el uso de métodos estáticos). – Landei

Respuesta

3
public interface Functor<A, FInst extends Functor<A,FInst>> { 
    public <B, I extends Functor<B,FInst>> I fmap(F<A,B> f); 
} 

Este código genera un error porque cuando se define I, se define que sea una subclase de Functor<B,FInst>, pero el parámetro finst debe ser una subclase de Functor<B,FInst> en este caso, si bien se define anteriormente como una subclase de Functor<A,FInst>. Como Functor<A,FInst> y Functor<B,FInst> no son compatibles, obtiene este error.

no han sido capaces de resolver este completo, pero que podía hacer al menos una mitad del trabajo:

import java.util.ArrayList; 
import java.util.List; 

interface F<A,R> { 
    public R apply(A a); 
} 

interface Functor<A, FClass extends Functor<?, FClass>> { 
    public <B> FClass fmap(F<A,B> f); 
} 

public class ListFunctor<A> implements Functor<A, ListFunctor<?>> { 
    final private List<A> list; 
    public ListFunctor(List<A> list) { 
    this.list = list; 
    } 

    @Override 
    public <B> ListFunctor<B> fmap(F<A,B> f) { 
    List<B> result = new ArrayList<B>(); 
    for(A a: list) result.add(f.apply(a)); 
    return new ListFunctor<B>(result); 
    } 
} 

Esto funciona, y se limita adecuadamente el conjunto de tipos de retorno permitidas a ListFunctor, pero no lo limita a las subclases de ListFunctor<B> solamente. Podría declararlo como ListFunctor<A> o cualquier otro ListFunctor, y todavía se compilaría. Pero no puedes declarar que devuelve un FooFunctor o cualquier otro Functor.

El principal problema para resolver el resto del problema es que no puede limitar FClass a las subclases de ListFunctor<B> solamente, ya que el parámetro B se declara en el nivel de método, no en el nivel de clase, por lo que no puede escriba

public class ListFunctor<A> implements Functor<A, ListFunctor<B>> { 

porque B no significa nada en ese punto. Tampoco pude hacer que funcione con el segundo parámetro de fmap(), pero incluso si pudiera, simplemente te obligaría a especificar el tipo de devolución dos veces: una vez en el parámetro de tipo y una vez más como el tipo de devolución en sí.

+0

¡Muy interesante! Así que parece que solo podemos "preservar" ** ya sea ** B ** o ** el tipo de subfuncionamiento (que es algo lógico, porque estamos tratando de destruir de algún modo los diferentes "niveles" de genéricos en una definición) – Landei

0

Creo que quiere hacer algo que no tiene sentido (tipo del reloj).

interface Getter<Type> { 
Type get(); 
} 

Si su aplicación necesita un captador que devuelve enteros, no dan en uno que devuelve objetos.

Si usted no sabe si volverá objetos o enteros que están tratando de hacer algo por el camino equivocado.

si usted sabe que volverá enteros, luego envolver el captador de modo que eche a enteros.

Espero que esto es lo que estás buscando.

EDIT: Explicación de por qué (creo), esto no se puede hacer.

Los objetos tienen los tipos establecidos cuando usa los nuevos. Tome cada tipo y reemplácelo por una letra. Tome cualquier cantidad de otros objetos y haga lo mismo.

¿Qué letra desea que devuelva su función? Si la respuesta es que quieres una mezcla, bueno, es demasiado tarde. Los tipos se deciden en nuevos, y ya se ha convertido en algo nuevo.

+0

Cosas similares están funcionando (basta con mirar la definición de java.lang.Enum), y creo que lo que quiero es muy razonable (por ejemplo, no sería un problema en Haskell o Scala).La pregunta es hasta dónde podemos empujar los límites del sistema de tipos lisiados de Java. – Landei

+0

No logré construir construcciones genéricas recursivas usando java, conservando el tipo externo. Como alguien escribe en una publicación aquí, no puedes capturar cosas del nivel anterior. Y sigo con mi explicación de que estás tratando de darle a tu programa un objeto capaz de devolver cosas sobre las que no puedes decir nada. Los genéricos de Java son muy estáticos. Una vez que haya establecido un tipo genérico en un tipo, no podrá volver atrás. Antes de nuevo, tienes genéricos. Después de nuevo, tienes tipos fijos. Entonces, una vez que su objeto pasa el nuevo, se deciden todos sus tipos. – mncl

+0

Excepto la inferencia, pero eso se decide a partir de los objetos, que ya han pasado una nueva declaración. Ninguno de los objetos sabe qué tipo devolvería la combinación (interferencia). – mncl

0

Sobre la base de la respuesta de Sergey, creo que vine cerca de a lo que quería. Parece que puedo combinar su idea con mi intento fallido:

public interface Functor<A, Instance extends Functor<?, Instance>> { 
    public <B, I extends Functor<B,Instance>> I fmap(F<A,B> f); 
} 

public class ListFunctor<A> implements Functor<A, ListFunctor<?>> { 
    final private List<A> list; 
    public ListFunctor(List<A> list) { 
    this.list = list; 
    } 

    @Override 
    public <B, I extends Functor<B, ListFunctor<?>>> I fmap(F<A,B> f) { 
    List<B> result = new ArrayList<B>(); 
    for(A a: list) result.add(f.apply(a)); 
    return (I) new ListFunctor<B>(result); 
    } 
} 

List<String> list = java.util.Arrays.asList("one","two","three"); 
ListFunctor<String> fs = new ListFunctor<String>(list); 
ListFunctor<Integer> fi = fs.<Integer,ListFunctor<Integer>>fmap(stringLengthF); 
//--> [3,3,5] 

El problema restante es que podría escribir, p. ListFunctor<StringBuilder> fi = fs.<Integer,ListFunctor<StringBuilder>> sin quejas del compilador. Al menos puedo buscar una forma de ocultar las feas agallas detrás de un método estático, y aplicar esa relación detrás de las escenas ...

+0

Sí, ese es precisamente el problema. Básicamente, estás limitando el tipo de devolución cuando llamas a la función, lo que te permite hacer muchas cosas peligrosas. Sin embargo, estoy sorprendido de cómo pudiste moverte con ese reparto '(I)'. No entendí por qué Java informó que el tipo de devolución era incompatible, pero no tenía idea de que un elenco ayudaría. –

2

Mirando desde un ángulo diferente, parece que Functor no debería ser modelado como un " Wrapper "alrededor de los datos, pero en realidad más como una clase de tipo, que funciona en los datos. Este cambio de perspectiva permite codificar todo sin un solo molde, y con seguridad de tipos absolutamente (pero aún con una gran cantidad de texto modelo):

public interface Functor<A, B, FromInstance, ToInstance> { 
    public ToInstance fmap(FromInstance instance, F<A,B> f); 
} 

public class ListFunctor<A,B> implements Functor<A, B, List<A>, List<B>> { 

    @Override 
    public List<B> fmap(List<A> instance, F<A, B> f) { 
    List<B> result = new ArrayList<B>(); 
    for(A a: instance) result.add(f.apply(a)); 
    return result; 
    } 
} 

List<String> stringList = Arrays.asList("one","two","three"); 
ListFunctor<String,Integer> functor = new ListFunctor<String,Integer>(); 
List<Integer> intList = functor.fmap(stringList, stringLengthF); 
System.out.println(intList); 
//--> [3, 3, 5] 

Parece que estaba demasiado centrado en el embalaje tanto FromInstance y ToInstance en un tipo parámetro (por ejemplo, List in ListFunctor), que no es estrictamente necesario. Sin embargo, es una pesada carga tener ahora no solo A sino también B como parámetro de tipo, lo que puede hacer que este enfoque sea prácticamente inutilizable.

[Investigación]

he encontrado una manera de hacer esta versión, al menos, un poco útil: El funtor puede ser utilizado para levantar una función. P.ej. si tiene F<String, Integer>, se puede construir un F<Foo<String>, Foo<Integer>> de ella cuando se tiene un FooFunctor definido como se ha mostrado anteriormente:

public interface F<A,B> { 
    public B apply(A a); 

    public <FromInstance, ToInstance> F<FromInstance, ToInstance> lift(
     Functor<A,B,FromInstance, ToInstance> functor); 
} 

public abstract class AbstractF<A,B> implements F<A,B> { 

    @Override 
    public abstract B apply(A a); 

    @Override 
    public <FromInstance, ToInstance> F<FromInstance, ToInstance> lift(
      final Functor<A, B, FromInstance, ToInstance> functor) { 
     return new AbstractF<FromInstance, ToInstance>() { 

      @Override 
      public ToInstance apply(FromInstance fromInstance) { 
       return functor.fmap(fromInstance, AbstractF.this); 
      } 

     }; 
    } 
} 

public interface Functor<A, B, FromInstance, ToInstance> { 
    public ToInstance fmap(FromInstance instance, F<A,B> f); 
} 

public class ListFunctor<A, B> implements Functor<A, B, List<A>, List<B>> { 

    @Override 
    public List<B> fmap(List<A> instance, F<A, B> f) { 
     List<B> result = new ArrayList<B>(); 
     for (A a : instance) { 
      result.add(f.apply(a)); 
     } 
     return result; 
    } 
} 

//Usage: 
F<String, Integer> strLenF = new AbstractF<String, Integer>() { 
      public Integer apply(String a) { 
       return a.length(); 
      } 
     }; 

//Whoa, magick!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
F<List<String>,List<Integer>> liftedF = strLenF.lift(new ListFunctor<String, Integer>()); 

List<String> stringList = Arrays.asList("one", "two", "three"); 
List<Integer> intList = liftedF.apply(stringList); 
System.out.println(intList); 
//--> [3, 3, 5] 

Creo que todavía no es muy útil, pero al menos manera más fría que los otros intentos :-P

+0

Sí, pensé en este enfoque también, pero no me gustó el hecho de que si mueves B al nivel de clase, básicamente estás limitando tu función a exactamente un tipo de devolución. Tu enfoque anterior se ve mejor, creo. –

+0

Estoy de acuerdo, como es, esta versión es bastante inútil. Por otro lado, hace exactamente lo que yo quiero (sin moldes o reflexión) y es fácil de entender, así que espero encontrar una manera de evitar el dolor del parámetro de tipo. Cuando tenga algo más de tiempo, veré qué pueden hacer los métodos estáticos en términos de tipo de inferencia. – Landei