【问题标题】:Abusing generics to implement a curried composition function in Java滥用泛型在 Java 中实现柯里化组合函数
【发布时间】:2012-02-13 09:28:18
【问题描述】:

所以,在玩了一点 Java 泛型之后,为了更深入地了解它们的功能,我决定尝试实现函数式程序员熟悉的组合函数的柯里化版本。 Compose 的类型(在函数式语言中)(b -> c) -> (a -> b) -> (a -> c)。做柯里化算术函数并不太难,因为它们只是多态的,但 compose 是一个高阶函数,事实证明它对我对 Java 泛型的理解很费力。

这是我目前创建的实现:

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);
}

add、mult 和 compose 的实现都编译得很好,但我发现自己在实际使用 compose 时遇到了问题。第 12 行出现以下错误(第一次在 main 中使用 compose):

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);

我认为这个错误是因为泛型类型是不变的,但我不知道如何解决这个问题。根据我的阅读,通配符类型变量在某些情况下可用于缓解不变性,但我不确定如何在这里使用它,甚至不确定它是否有用。

免责声明:我无意在任何实际项目中编写这样的代码。这是一个有趣的“可以做到”的事情。此外,我无视标准 Java 实践,使变量名称简短化,因为否则此示例将成为甚至一堵难以理解的文本墙。

【问题讨论】:

    标签: java generics functional-programming currying


    【解决方案1】:

    我自己根据通用的长度为 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();
    }
    

    我认为这是对泛型的更大滥用,因为 Chainer 类仅使用泛型来确保后续的 .then() 调用基于提供的最后一个函数正确键入,并且这些函数只是存储在一个以已知的安全调用顺序列出以供以后使用,但它确实有效并且概括性很好:chain(first).then(second).then(third).then(fourth).build() 是完全有效的用这种方法。

    请明确一点,这是基于 guava 的 Function,但应该可以移植到任何 Function 接口。

    【讨论】:

      【解决方案2】:

      这里的基本问题是,在对compose() 的原始调用中,编译器无法推断出A、B 和C 的绑定,因此它假定它们都是Object。您可以通过明确指定类型绑定来修复它:

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

      当然,这样你就失去了类型推断所带来的清晰性。如果需要类型推断,可以定义一些中间类来进行推断:

      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));
                  }
              };
          }
      }
      

      但是,那么柯里化的中间步骤就不再是Fns了。

      【讨论】:

      • 好的!感谢您的回答,我想我找到了解决问题的方法!
      • 我认为这是最能满足我要求的解决方案(非推断版本)。它没有得到推断,但我认为不必定义额外的类是值得的。
      【解决方案3】:

      感谢 Russell Zahniser 的洞察力,即我没有为 Java 提供足够的信息来使用,我稍微改变了布局,以便我们实例化一个填充了适当类型变量的“Composer”对象。这是我当前的工作解决方案:

      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;
                }
              };
            }
          };
        }
      }
      

      【讨论】:

      • 实际上,如果像这样手动指定类型是可以的,如果您将违规行更改为 Fn&lt;Integer, Integer&gt; times3plus2 = Currying.&lt;Integer, Integer, Integer&gt;compose().ap(plus2).ap(times3);
      • 啊,是的,那会很好用!我不会在 Java 类型推理引擎上投入太多。
      猜你喜欢
      • 2023-03-03
      • 1970-01-01
      • 2020-08-20
      • 1970-01-01
      • 2021-01-08
      • 2016-01-01
      • 1970-01-01
      • 2021-02-07
      • 2012-10-06
      相关资源
      最近更新 更多