2012-01-18 17 views
6

Entonces, después de jugar con los genéricos de Java un poco, para obtener una comprensión más profunda de sus capacidades, decidí tratar de implementar la versión curried de la función de composición, familiar para funcional programadores. Compose tiene el tipo (en idiomas funcionales) (b -> c) -> (a -> b) -> (a -> c). Hacer las funciones aritméticas de currículum no fue demasiado difícil, ya que son solo polimórficas, pero componer es una función de orden superior, y su comprensión de los genéricos en Java es comprobadamente comprobada.Abusar genéricos para implementar una función de composición al curry en Java

Aquí es la aplicación que he creado hasta ahora:

public class Currying { 

    public static void main(String[] argv){ 
    // Basic usage of currying 
    System.out.println(add().ap(3).ap(4)); 
    // Next, lets try (3 * 4) + 2 
    // First lets create the (+2) function... 
    Fn<Integer, Integer> plus2 = add().ap(2); 
    // next, the times 3 function 
    Fn<Integer, Integer> times3 = mult().ap(3); 
    // now we compose them into a multiply by 2 and add 3 function 
    Fn<Integer, Integer> times3plus2 = compose().ap(plus2).ap(times3); 
    // now we can put in the final argument and print the result 
    // without compose: 
    System.out.println(plus2.ap(times3.ap(4))); 
    // with compose: 
    System.out.println(times3plus2.ap(new Integer(4))); 
    } 

    public static <A,B,C> 
       Fn<Fn<B,C>, // (b -> c) -> -- f 
       Fn<Fn<A,B>, // (a -> b) -> -- g 
       Fn<A,C>>> // (a -> c) 
       compose(){ 
    return new Fn<Fn<B,C>, 
       Fn<Fn<A,B>, 
       Fn<A,C>>>() { 
     public Fn<Fn<A,B>, 
      Fn<A,C>> ap(final Fn<B,C> f){ 
     return new Fn<Fn<A,B>, 
        Fn<A,C>>() { 
      public Fn<A,C> ap(final Fn<A,B> g){ 
      return new Fn<A,C>(){ 
       public C ap(final A a){ 
       return f.ap(g.ap(a)); 
       } 
      }; 
      } 
     }; 
     } 
    }; 
    } 

    // curried addition 
    public static Fn<Integer, Fn<Integer, Integer>> add(){ 
    return new Fn<Integer, Fn<Integer, Integer>>(){ 
     public Fn<Integer,Integer> ap(final Integer a) { 
     return new Fn<Integer, Integer>() { 
      public Integer ap(final Integer b){ 
      return a + b; 
      } 
     }; 
     } 
    }; 
    } 

    // curried multiplication 
    public static Fn<Integer, Fn<Integer, Integer>> mult(){ 
    return new Fn<Integer, Fn<Integer, Integer>>(){ 
     public Fn<Integer,Integer> ap(final Integer a) { 
     return new Fn<Integer, Integer>() { 
      public Integer ap(final Integer b){ 
      return a * b; 
      } 
     }; 
     } 
    }; 
    } 
} 

interface Fn<A, B> { 
    public B ap(final A a); 
} 

Las implementaciones del complemento, mult, y componer todo compilar muy bien, pero me encuentro con un problema cuando se trata de utilizar realmente componer . Me sale el siguiente error de la línea 12 (el primer uso de de composición en principal):

Currying.java:12: ap(Fn<java.lang.Object,java.lang.Object>) in 
Fn<Fn<java.lang.Object,java.lang.Object>,Fn<Fn<java.lang.Object,java.lang.Object>,Fn<java.lang.Object,java.lang.Object>>> 
cannot be applied to (Fn<java.lang.Integer,java.lang.Integer>) 
    Fn<Integer,Integer> times3plus2 = compose().ap(plus2).ap(times3); 

Asumo este error se debe a que los tipos genéricos son invariantes, pero no estoy seguro de cómo resolver el problema. Por lo que he leído, las variables de tipo comodín se pueden usar para aliviar la invarianza en algunos casos, pero no estoy seguro de cómo usarlo aquí o incluso si será útil.

Descargo de responsabilidad: No tengo intención de escribir código como este en ningún proyecto real. Este es un tipo de cosas divertidas de "se puede hacer". Además, hice que los nombres de las variables fueran breves, desafiando la práctica estándar de Java porque de lo contrario este ejemplo se convierte en aún más de una pared de texto incomprensible.

Respuesta

4

El problema básico aquí es que en la llamada original a compose(), no hay forma de que el compilador infiera los enlaces de A, B y C, por lo que asume que todos son Objeto. Se puede arreglar mediante la especificación de los enlaces de tipo explícitamente:

Fn<Integer, Integer> times3plus2 = 
    Currying.<Integer, Integer, Integer>compose().ap(plus2).ap(times3); 

Por supuesto, entonces se pierde la claridad que viene de la inferencia de tipos. Si necesita la inferencia de tipos, se podría definir algunas clases intermedias para hacer la inferencia:

public static ComposeStart compose() { 
    return new ComposeStart(); 
} 

class ComposeStart { 
    public <B,C> ComposeContinuation<B,C> ap(Fn<B,C> f) { 
     return new ComposeContinuation<B, C>(f); 
    } 
} 

class ComposeContinuation<B, C> { 
    private final Fn<B,C> f; 

    ComposeContinuation(Fn<B,C> f) { 
     this.f = f; 
    } 

    public <A> Fn<A,C> ap(final Fn<A,B> g) { 
     return new Fn<A,C>() { 
      public C ap(A a) { 
       return f.ap(g.ap(a)); 
      } 
     }; 
    } 
} 

Sin embargo, a continuación, los pasos intermedios de currificación ya no son Fn s.

+0

Ok!¡Gracias a su respuesta creo que encontré una solución que resuelve el problema! – deontologician

+0

Creo que esa es la solución que satisface mis requisitos de la mejor manera (la versión no inferida). No se deduce, pero creo que no vale la pena definir una clase extra. – deontologician

0

Gracias a la visión de Russell Zahniser de que no le estaba dando a Java suficiente información para trabajar, cambié un poco el diseño para crear una instancia de un objeto "Composer" con las variables de tipo correspondientes. Aquí está mi trabajo actual solución:

interface Fn<A, B> { 
    public B ap(final A a); 
} 

public class Currying { 

    public static void main(String[] argv){ 
    // Basic usage of currying 
    System.out.println(add().ap(3).ap(4)); 
    // Next, lets try (3 * 4) + 2 
    // First lets create the (+2) function... 
    Fn<Integer, Integer> plus2 = add().ap(2); 
    // next, the times 3 function 
    Fn<Integer, Integer> times3 = mult().ap(3); 
    // now we compose them into a multiply by 2 and add 3 function 
    Fn<Integer, Integer> times3plus2 = new Composer<Integer,Integer,Integer>() 
     .compose().ap(plus2).ap(times3); 
    // without compose 
    System.out.println(plus2.ap(times3.ap(4))); 
    // with compose 
    System.out.println(times3plus2.ap(4)); 
    } 

    static class Composer<A,B,C> { 
    public 
     Fn<Fn<B,C>, // (b -> c) -> -- f 
     Fn<Fn<A,B>, // (a -> b) -> -- g 
     Fn<A,C>>> // (a -> c) 
     compose(){ 
     return new Fn<Fn<B,C>, 
     Fn<Fn<A,B>, 
     Fn<A,C>>>() { 
     public Fn<Fn<A,B>, 
      Fn<A,C>> ap(final Fn<B,C> f){ 
      return new Fn<Fn<A,B>, 
      Fn<A,C>>() { 
      public Fn<A,C> ap(final Fn<A,B> g){ 
       return new Fn<A,C>(){ 
       public C ap(final A a){ 
        return f.ap(g.ap(a)); 
       } 
       }; 
      } 
      }; 
     } 
     }; 
    } 
    } 

    public static Fn<Integer, Fn<Integer, Integer>> add(){ 
    return new Fn<Integer, Fn<Integer, Integer>>(){ 
     public Fn<Integer,Integer> ap(final Integer a) { 
     return new Fn<Integer, Integer>() { 
      public Integer ap(final Integer b){ 
      return a + b; 
      } 
     }; 
     } 
    }; 
    } 

    public static Fn<Integer, Fn<Integer, Integer>> mult(){ 
    return new Fn<Integer, Fn<Integer, Integer>>(){ 
     public Fn<Integer,Integer> ap(final Integer a) { 
     return new Fn<Integer, Integer>() { 
      public Integer ap(final Integer b){ 
      return a * b; 
      } 
     }; 
     } 
    }; 
    } 
} 
+0

En realidad, si especificar manualmente los tipos como este es correcto, su código original hubiera funcionado si hubiera cambiado la línea ofensiva a 'Fn times3plus2 = Currying. compose() .ap (plus2) .ap (times3); ' –

+0

¡Ah sí, eso funcionaría bien! No pongo demasiado stock en el motor de inferencia tipo Java. – deontologician

0

Implementé esta funcionalidad yo mismo en términos de una longitud genérica de llamadas de función n.

public static final <X, Y> Chainer<X, Y> chain(
     final Function<X, Y> primary 
) { 
    return new Chainer<X, Y>(primary); 
} 

private static final class FunctionChain<IN, OUT> implements Function<IN, OUT> { 

    @SuppressWarnings("rawtypes") 
    private final List<Function> chain = new LinkedList<Function>(); 

    private FunctionChain(@SuppressWarnings("rawtypes") final List<Function> chain) { 
     this.chain.addAll(chain); 
    } 

    @SuppressWarnings("unchecked") 
    @Override 
    public OUT apply(final IN in) { 
     Object ret = in; 
     for (final Function<Object, Object> f : chain) { 
      ret = f.apply(ret); 
     } 
     return (OUT) ret; 
    } 
} 

public static final class Chainer<IN, OUT> { 
    @SuppressWarnings("rawtypes") 
    private final LinkedList<Function> functions = new LinkedList<Function>(); 

    @SuppressWarnings("unchecked") 
    private Chainer(@SuppressWarnings("rawtypes") final Function func) { 
     then(func); 
    } 

    @SuppressWarnings("unchecked") 
    public <OUT2> Chainer<IN, OUT2> then(final Function<OUT, OUT2> func) { 
     if (func instanceof FunctionChain) { 
      functions.addAll(((FunctionChain<?, ?>)func).chain); 
     } else { 
      functions.add(func); 
     } 
     return (Chainer<IN, OUT2>) this; 
    } 

    @SuppressWarnings("unchecked") 
    public Function<IN, OUT> build() { 
     // If empty, it's a noop function. If one element, there's no need for a chain. 
     return new FunctionChain<IN, OUT>(functions); 
    } 
} 

public static final <X, Y, Z> Function<X, Z> combine(
     final Function<X, Y> primary, 
     final Function<Y, Z> secondary 
) { 
    return chain(primary).then(secondary).build(); 
} 

yo sostendría que esto califica como un mayor abuso de medicamentos genéricos, ya que la clase Chainer sólo utiliza los genéricos para asegurar que .then() posterior llamadas están adecuadamente con tipo basada en la última función suministrado y las funciones se simplemente almacenado en una lista en lo que se conoce como un orden de llamada seguro para más tarde, pero funciona y se generaliza bien: cadena (primero) .then (segundo) .then (tercero) .then (cuarto) .build () es completamente válido con este enfoque.

Simplemente sea explícito, esto se basa en la función de guayaba, pero debe portar a cualquier interfaz de función muy bien.