部分应用和返回函数之间有区别吗?

Is there a difference between partial application and returning a function?

就底层而言:stack/heap分配、垃圾收集、资源和性能,以下三者之间有什么区别:

def Do1(a:String) = { (b:String) => { println(a,b) }}
def Do2(a:String)(b:String) = { println(a,b) }
def Do3(a:String, b:String) = { println(a,b) }

Do1("a")("b")
Do2("a")("b")
(Do3("a", _:String))("b")

除了声明中关于每个参数的明显表面差异和 returns

反编译以下 class(注意与您的问题相比,对 Do2 的额外调用):

class Test {
  def Do1(a: String) = { (b: String) => { println(a, b) } }
  def Do2(a: String)(b: String) = { println(a, b) }
  def Do3(a: String, b: String) = { println(a, b) }

  Do1("a")("b")
  Do2("a")("b")
  (Do2("a") _)("b")
  (Do3("a", _: String))("b")
}

产生这个纯 Java 代码:

public class Test {
    public Function1<String, BoxedUnit> Do1(final String a) {
        new AbstractFunction1() {
            public final void apply(String b) {
                Predef..MODULE$.println(new Tuple2(a, b));
            }
        };
    }

    public void Do2(String a, String b) {
        Predef..MODULE$.println(new Tuple2(a, b));
    }

    public void Do3(String a, String b) {
        Predef..MODULE$.println(new Tuple2(a, b));
    }

    public Test() {
        Do1("a").apply("b");
        Do2("a", "b");
        new AbstractFunction1() {
            public final void apply(String b) {
                Test.this.Do2("a", b);
            }
        }.apply("b");
        new AbstractFunction1() {
            public final void apply(String x) {
                Test.this.Do3("a", x);
            }
        }.apply("b");
    }
}

(这段代码编译不过,但足够分析)


让我们逐个看一下(每个清单中的 Scala 和 Java):

def Do1(a: String) = { (b: String) => { println(a, b) } }

public Function1<String, BoxedUnit> Do1(final String a) {
    new AbstractFunction1() {
        public final void apply(String b) {
            Predef.MODULE$.println(new Tuple2(a, b));
        }
    };
}

无论Do1如何调用,都会创建一个新的Function对象。


def Do2(a: String)(b: String) = { println(a, b) }

public void Do2(String a, String b) {
    Predef.MODULE$.println(new Tuple2(a, b));
}

def Do3(a: String, b: String) = { println(a, b) }

public void Do3(String a, String b) {
    Predef.MODULE$.println(new Tuple2(a, b));
}

Do2Do3 编译成相同的字节码。区别仅在于 @ScalaSignature 注释。


Do1("a")("b")

Do1("a").apply("b");

Do1 很简单:立即应用返回的函数。

Do2("a")("b")

Do2("a", "b");

对于 Do2,编译器发现这不是部分应用程序,并将其编译为单个方法调用。


(Do2("a") _)("b")

new AbstractFunction1() {
    public final void apply(String b) {
        Test.this.Do2("a", b);
    }
}.apply("b");

(Do3("a", _: String))("b")

new AbstractFunction1() {
    public final void apply(String x) {
        Test.this.Do3("a", x);
    }
}.apply("b");

这里先部分应用Do2Do3,然后立即应用返回的函数。


结论:

我会说 Do2Do3 在生成的字节码中大部分是等价的。完整的应用程序会导致简单、廉价的方法调用。部分应用程序在调用方生成匿名函数 classes。您使用哪种变体主要取决于您试图传达的意图。

Do1 总是创建一个立即函数对象,但在被调用的代码中这样做。如果您希望对函数进行大量的部分应用,使用此变体将减少您的代码大小,并且可能更早地触发 JIT 编译器,因为相同的代码被更频繁地调用。完整的应用程序会变慢,至少在 JIT 编译器内联并随后消除各个调用站点的对象创建之前是这样。我不是这方面的专家,所以我不知道您是否可以期待这种优化。对于纯函数,我最好的猜测是你可以。