部分应用和返回功能之间有区别吗?

Ale*_*lex 5 scala currying partial-application

在引擎盖方面:堆栈/堆分配,垃圾收集,资源和性能,以下三个有什么区别:

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")
Run Code Online (Sandbox Code Playgroud)

除了声明中每个采取和返回多少参数的明显表面差异

Sil*_*eak 2

反编译以下类(注意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")
}
Run Code Online (Sandbox Code Playgroud)

产生这个纯 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$1) {
                Test.this.Do3("a", x$1);
            }
        }.apply("b");
    }
}
Run Code Online (Sandbox Code Playgroud)

(此代码无法编译,但足以进行分析)


让我们逐个查看它(每个清单中都有 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));
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

无论如何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));
}
Run Code Online (Sandbox Code Playgroud)

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


Do1("a")("b")

Do1("a").apply("b");
Run Code Online (Sandbox Code Playgroud)

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

Do2("a")("b")

Do2("a", "b");
Run Code Online (Sandbox Code Playgroud)

使用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$1) {
        Test.this.Do3("a", x$1);
    }
}.apply("b");
Run Code Online (Sandbox Code Playgroud)

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


结论:

我想说,Do2Do3在生成的字节码中大部分是等效的。完整的应用程序会产生简单、廉价的方法调用。部分应用程序在调用者处生成匿名函数类。您使用什么变体主要取决于您想要传达的意图。

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