2011-01-26 15 views
8

Decidí escribir algunas funciones comunes de orden superior en Java (mapa, filtro, reducir, etc.) que son seguras a través de genéricos, y tengo problemas con la coincidencia de comodines en una función particular.Genéricos Java - implementando funciones de orden superior como map

Sólo para estar completa, la interfaz funtor es la siguiente:

/** 
* The interface containing the method used to map a sequence into another. 
* @param <S> The type of the elements in the source sequence. 
* @param <R> The type of the elements in the destination sequence. 
*/ 
public interface Transformation<S, R> { 

    /** 
    * The method that will be used in map. 
    * @param sourceObject An element from the source sequence. 
    * @return The element in the destination sequence. 
    */ 
    public R apply(S sourceObject); 
} 

La función preocupante es como un mapa , pero en lugar de transformar una colecciónque transforma un Mapa (en un primer momento pensé que debería llamarse mapMap, pero sonaba tan estúpido que terminé llamándolo remapEntries).

Mi primera versión era (y tomar una sentada, porque la firma es bastante un monstruo):

/** 
    * <p> 
    * Fills a map with the results of applying a mapping function to 
    * a source map. 
    * </p> 
    * Considerations: 
    * <ul> 
    * <li>The result map must be non-null, and it's the same object what is returned 
    * (to allow passing an unnamed new Map as argument).</li> 
    * <li>If the result map already contained some elements, those won't 
    * be cleared first.</li> 
    * <li>If various elements have the same key, only the last entry given the 
    * source iteration order will be present in the resulting map (it will 
    * overwrite the previous ones).</li> 
    * </ul> 
    * 
    * @param <SK> Type of the source keys. 
    * @param <SV> Type of the source values. 
    * @param <RK> Type of the result keys. 
    * @param <RV> Type of the result values. 
    * @param <MapRes> 
    * @param f The object that will be used to remapEntries. 
    * @param source The map with the source entries. 
    * @param result The map where the resulting entries will be put. 
    * @return the result map, containing the transformed entries. 
    */ 
    public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, Map.Entry<RK,RV>> f, final Map<SK, SV> source, MapRes result) { 
     for (Map.Entry<SK, SV> entry : source.entrySet()) { 
      Map.Entry<RK, RV> res = f.apply(entry); 
      result.put(res.getKey(), res.getValue()); 
     } 
     return result; 
    } 

Y parece ser bastante correcta, pero el problema es que la transformación utilizado debe coincidir exactamente con el escriba parámetros, dificultando la reutilización de las funciones del mapa para los tipos que son compatibles. Así que he decidido añadir comodines para la firma, y ​​terminó así:

public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<? super Map.Entry<? super SK, ? super SV>, ? extends Map.Entry<? extends RK, ? extends RV>> f, final Map<SK, SV> source, MapRes result) { 
    for (Map.Entry<SK, SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply(entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

Pero cuando estoy tratando de probarlo, la coincidencia de comodines falla:

@Test 
public void testRemapEntries() { 
    Map<String, Integer> things = new HashMap<String, Integer>(); 
    things.put("1", 1); 
    things.put("2", 2); 
    things.put("3", 3); 

    Transformation<Map.Entry<String, Number>, Map.Entry<Integer, String>> swap = new Transformation<Entry<String, Number>, Entry<Integer, String>>() { 
     public Entry<Integer, String> apply(Entry<String, Number> sourceObject) { 
      return new Pair<Integer, String>(sourceObject.getValue().intValue(), sourceObject.getKey()); //this is just a default implementation of a Map.Entry 
     } 
    }; 

    Map<Integer, String> expected = new HashMap<Integer, String>(); 
    expected.put(1, "1"); 
    expected.put(2, "2"); 
    expected.put(3, "3"); 

    Map<Integer, String> result = IterUtil.remapEntries(swap, things, new HashMap<Integer, String>()); 
    assertEquals(expected, result); 
} 

El error es:

method remapEntries in class IterUtil cannot be applied to given types 
    required: Transformation<? super java.util.Map.Entry<? super SK,? super SV>,? extends java.util.Map.Entry<? extends RK,? extends RV>>,java.util.Map<SK,SV>,MapRes 
    found: Transformation<java.util.Map.Entry<java.lang.String,java.lang.Number>,java.util.Map.Entry<java.lang.Integer,java.lang.String>>,java.util.Map<java.lang.String,java.lang.Integer>,java.util.HashMap<java.lang.Integer,java.lang.String> 

¿Alguna pista sobre cómo solucionarlo? ¿O debería renunciar y escribir loops explícitos para esto?^_^

+0

Tome un vistazo a https://github.com/GlenKPeterson/fp4java7 Es funciones de orden superior para Java implementadas como transormaciones perezosas sobre colecciones inmutables (o mutables). También se implementan algunas transformaciones perezosas persistentes. Es una interfaz completamente genérica, aunque algunos modelos fueron apropiados en la implementación. – GlenPeterson

+0

jeje, tienes 3 años de retraso @GlenPeterson;) por cierto, ¡agrega algunas pruebas! : D – fortran

Respuesta

5

Creo que debería echar un vistazo a Google Guava API.

Allí puede encontrar una interfaz Function similar a su Transformation. También hay una clase Maps con métodos de utilidad para crear o transformar instancias de mapas.

También debe considerar PECS al implementar métodos para uso de genéricos.

+0

He visto lo que hace Guava, tienen una clase de Transformación separada para los mapas ... No es muy elegante, pero supongo que eso es lo más que puedes llegar con las limitaciones de Java Generics. – fortran

+1

+1 para Guava. Estás reinventando la rueda aquí. Guava tiene métodos y clases que se ajustan a tus necesidades –

+2

@Shervin Ya sabía que estaba reinventando la rueda, pero es un buen ejercicio para tener más fluidez con la semántica de los genéricos. – fortran

5

Esto es difícil. El siguiente conocimiento es totalmente inútil y nadie debería preocuparse por poseer:

Lo primero que se debe corregir es el tipo de swap. El tipo de entrada no debe ser Entry<String,Number>, porque entonces no puede aceptar Entry<String,Integer>, que no es un subtipo de E<S,N>. Sin embargo, E<S,I> es un subtipo de E<? extends S,? extends N>. Entonces nuestro transformador debería tomar eso como entrada. Para la salida, no hay comodín, porque el transformador solo puede instanciar un tipo concreto de todos modos. Sólo queremos ser honesta y exacta de lo que puede ser consumido y lo que se producirá:

/*  */ Transformation< 
        Entry<? extends String, ? extends Number>, 
        Entry<Integer, String> 
       > swap 
     = new Transformation< 
        Entry<? extends String, ? extends Number>, 
        Entry<Integer, String>>() 
    { 
     public Entry<Integer, String> apply(
      Entry<? extends String, ? extends Number> sourceObject) 
     { 
      return new Pair<Integer, String>(
       sourceObject.getValue().intValue(), 
       sourceObject.getKey() 
      ); 
     } 
    }; 

Nota String es final y nadie se extiende, pero me temo que el sistema genérico no es tan inteligente como para saber eso, como una cuestión de principio, hice ? extends String de todos modos, para bien luego.

Entonces, pensemos en remapEntries().Sospechamos que la mayoría de los transformadores pasarán a tener una declaración de tipo similar al swap, debido a las justificaciones que presentamos. Así que es mejor tener

remapEntry( 
    Transformation< 
     Entry<? extends SK, ? extends SV>, 
     Entry<RK,RV> 
     > f, 
    ... 

para que coincida adecuadamente con ese argumento. A partir de ahí, se trabaja a cabo el tipo de fuente y el resultado, que queremos que sean lo más general posible:

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>> 
RM remapEntries(
    Transformation< 
     Entry<? extends SK, ? extends SV>, 
     Entry<RK,RV> 
     > f, 
    Map<? extends SK, ? extends SV> source, 
    RM result 
) 
{ 
    for(Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Entry<RK,RV> res = f.apply(entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

RM no es necesario, que está bien usar directamente Map<? super RK, ? super RV>. Pero parece que desea el tipo de devolución idéntico al tipo result en el contexto de la persona que llama. Simplemente habría hecho el tipo de devolución void - ya hay suficientes problemas.

Esto no funcionará, si swap no utiliza ? extends. Por ejemplo, si el tipo de entrada es String-Integer, es ridículo hacer ? extends de ellos. Pero puede tener un método de sobrecarga con declaración de tipo de parámetro diferente para que coincida con este caso.

Ok, eso funcionó, por pura suerte. Pero, es totalmente no vale la pena. Tu vida es mucho mejor si simplemente te olvidas de ella, y usas el tipo sin procesar, documenta los parámetros en inglés, realiza una verificación de tipo en el tiempo de ejecución. Pregúntate, ¿la versión genérica te compra algo? Muy poco, al enorme precio de renderizar su código completamente incomprensible. Nadie, incluyéndote a ti, y a mí mismo, podrían darle sentido si leemos la firma del método mañana por la mañana. Es mucho peor que regex.

+0

Hubiera apostado a que las capturas comodín (' ') usado en' f' y 'source' no concuerda: -/ – fortran

+0

Me tomó un tiempo, ¡pero ahora creo que tengo la idea! :-) La clave es que la captura se haría solo en el nivel de tipo externo, pero el comodín aquí es en realidad parte de la firma. – fortran

1

Algo de repente me vino a la cabeza: si los comodines en los parámetros genéricos anidados no se capturarán ya que son literalmente parte del tipo, entonces podría usar los límites inversos en los mapas en lugar de usarlos en el Transformation.

public static <SK, SV, RK, RV, MapRes extends Map<? super RK, ? super RV>> 
    MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, 
              Map.Entry<RK, RV>> f, 
         final Map<? extends SK, ? extends SV> source, 
         MapRes result) { 
    for (Map.Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply((Map.Entry<SK, SV>)entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

El único problema es que tenemos que hacer el molde sin comprobar en el Transformation.apply. Sería totalmente seguro si la interfaz Map.Entry fuera de solo lectura, por lo que podemos simplemente cruzar los dedos y esperar que la transformación no intente llamar al Map.Entry.setValue.

Aún podíamos pasar un contenedor inmutable de la interfaz Map.Entry que arrojaba una excepción si se llamaba al método setValue para garantizar al menos la seguridad del tipo de tiempo de ejecución.

o simplemente hacer una interfaz de entrada inmutable explícito y lo utilizan, pero eso es un poco como hacer trampa (como si tuviera dos transformaciones diferentes):

public interface ImmutableEntry<K, V> { 
    public K getKey(); 
    public V getValue(); 
} 

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>> RM remapEntries(final Transformation<ImmutableEntry<SK, SV>, Map.Entry<RK, RV>> f, 
     final Map<? extends SK, ? extends SV> source, 
     RM result) { 
    for (final Map.Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply(new ImmutableEntry<SK, SV>() { 
      public SK getKey() {return entry.getKey();} 
      public SV getValue() {return entry.getValue();} 
     }); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 
Cuestiones relacionadas