【问题标题】:How does Scala distinguish between () => T and => TScala 如何区分 () => T 和 => T
【发布时间】:2017-03-08 04:25:29
【问题描述】:

我的other question 因重复而被关闭,所以我会再试一次。我也读过this question,我问的是不同的。我有兴趣了解Call-by-Name: => Type() => Type 的不同之处的内部实现。

我的困惑来自于查看 javap 和 cfr 反汇编,这两种情况没有区别。

例如ParamTest.scala

object ParamTest {
  def bar(x: Int, y: => Int) : Int = if (x > 0) y else 10
  def baz(x: Int, f: () => Int) : Int = if (x > 0) f() else 20
}

javap 输出 javap ParamTest.scala:

public final class ParamTest {
  public static int baz(int, scala.Function0<java.lang.Object>);
  public static int bar(int, scala.Function0<java.lang.Object>);
}

CFR 反编译输出 java -jar cfr_0_118.jar ParamTest$.class:

import scala.Function0;

public final class ParamTest$ {
    public static final ParamTest$ MODULE$;

    public static {
        new ParamTest$();
    }

    public int bar(int x, Function0<Object> y) {
        return x > 0 ? y.apply$mcI$sp() : 10;
    }

    public int baz(int x, Function0<Object> f) {
        return x > 0 ? f.apply$mcI$sp() : 20;
    }

    private ParamTest$() {
        MODULE$ = this;
    }
}

编辑 1Scala 语法树scalac -Xprint:parse ParamTest.scala

package <empty> {
  object ParamTest extends scala.AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    def bar(x: Int, y: _root_.scala.<byname>[Int]): Int = if (x.$greater(0))
      y
    else
      10;
    def baz(x: Int, f: _root_.scala.Function0[Int]): Int = if (x.$greater(0))
      f()
    else
      20
  }
}

编辑 2邮件列表研究

在邮件列表中阅读此interesting post,它基本上声明=&gt; T 实现为() =&gt; T。引用:

首先,看看

f: =&gt; Boolean

虽然这被称为“按名称参数”,但实际上是作为Function0实现的,

f: () =&gt; Boolean

只是两端使用不同的语法。

现在我对this answer 更加困惑,它明确指出两者是不同的。

问题

  • Scala 如何区分barbaz?两者的方法签名(不是实现)在反编译代码中是相同的。
  • 两种方案的差异不会被持久化到编译后的字节码中吗?
  • 反编译的代码是否不准确?
  • 在编辑 1 之后添加:我发现 scalac 语法树确实有所不同,bar 具有_root_.scala.&lt;byname&gt;[Int] 类型的第二个参数。它有什么作用?任何解释、scala source 中的指针或等效的伪代码都会有所帮助。
  • 参见上面的编辑 2:引用的块是否正确?例如,=&gt; TFunction0 的特殊子类吗?

【问题讨论】:

  • 为什么这些类型在 JVM 字节码中的表示方式很重要?它们在 Scala 代码中有所不同,这很重要(除非您尝试从 Java 调用 Scala,但这不是您要的)。
  • 因为 Scala 就是这样工作的。它将 scala 代码编译为 .class 文件并在 JVM 中执行。因此.class 文件应该有必要和充分的信息。 Scala 源文件根本不重要。参见linked question,删除ParamTest.scala源文件后可以重现调用foo(baz,100)的错误。
  • 这个问题的另一个动机是this answer,其中最受好评的评论 (160) 断言,我引用:“此外,“按名称调用”与名称无关。=&gt; Int是与Int 不同的类型;它是“没有参数的函数会生成一个 Int”,而不仅仅是 Int”。但正如我们在上面看到的,事实并非如此。 call-by-name 与惰性求值有关,并不是说它是一个没有参数的函数...
  • 您将编程语言与字节码混淆了。如果你翻译 C++,你会得到一堆指令,比如mov bx, ax,它们几乎不会告诉你关于原始代码的信息。 Call-by-name 代表惰性求值,它不是 Scala 术语中的函数,但字节码不会告诉你这一点。
  • 我试图了解 Scala 如何在ParamTest.class 的两种方法中区分() =&gt; T=&gt; T。这是在删除源文件后测试的。我在 Scala 源代码中看到了一些参考资料,请参阅 thisthis,将进一步挖掘。

标签: java scala


【解决方案1】:

Scala 如何区分barbaz?方法签名(不是 实现)在反编译代码中是相同的。

Scala 不需要区分两者。从它的角度来看,这是两种不同的方法。有趣的是(至少对我而言),如果我们将 baz 重命名为 bar 并尝试使用“按名称调用”参数创建重载,我们会得到:

Error:(12, 7) double definition:
method bar:(x: Int, f: () => Int)Int and
method bar:(x: Int, y: => Int)Int at line 10
have same type after erasure: (x: Int, f: Function0)Int
  def bar(x: Int, f: () => Int): Int = if (x > 0) f() else 20

这对我们来说是一个暗示,在幕后,Function0 的翻译正在发生一些事情。

这两种情况的差异不会持续到 编译字节码?

在 Scala 发出 JVM 字节码之前,它有额外的编译阶段。在这种情况下,一个有趣的事情是查看“uncurry”阶段(-Xprint:uncurry):

[[syntax trees at end of uncurry]] 
package testing {
  object ParamTest extends Object {
    def <init>(): testing.ParamTest.type = {
      ParamTest.super.<init>();
      ()
    };
    def bar(x: Int, y: () => Int): Int = if (x.>(0))
      y.apply()
    else
      10;
    def baz(x: Int, f: () => Int): Int = if (x.>(0))
      f.apply()
    else
      20
  }
}

甚至在我们发出字节码之前,bar 就被翻译成Function0

反编译的代码是否不准确

不,绝对准确。

我发现 scalac 语法树确实有区别,bar 有 root.scala.[Int] 类型的第二个参数。它有什么作用 怎么办?

Scala 编译是分阶段完成的,每个阶段的输出都是下一个阶段的输入。除了解析的 AST 之外,Scala 阶段还创建符号,这样如果一个阶段依赖于特定的实现细节,它将使其可用。 &lt;byname&gt; 是一个编译器符号,它表明此方法使用“按名称调用”,因此其中一个阶段可以看到并对其进行处理。

【讨论】:

  • 非常有帮助,感谢您抽出宝贵时间回答这个问题并指导我正确的方向!
  • @vsnyc 欢迎您。继续问这些问题,这就是我们所有人的学习方式。很高兴我能帮上忙。
  • 很好,很好的答案!
【解决方案2】:

因为 Scala 就是这样工作的。它将 scala 代码编译为 .class 文件并在 JVM 中执行。因此,.class 文件应该有必要和充分的信息。

确实如此。此信息存储在名为@ScalaSignature 的注释中。 javap -v 应该显示它的存在,但它不是人类可读的。

这是必要的,因为 Scala 签名中有很多信息无法在 JVM 字节码中表示:不仅仅是按名称与 Function0 参数,还有访问限定符、参数名称等。

【讨论】:

  • 感谢您的确认。 Yuval 的回答非常有帮助,它引导我朝着正确的方向前进。我现在正在阅读更多内容,例如this answer 来自VonC
  • 请注意,它只适用于相当古老的 Scala 版本:ScalaSig 属性在 Scala 2.8 中被 @ScalaSignature 取代。
  • 谢谢,我会记下来的。
【解决方案3】:

Scala 代码由编译器分析并转换为 jvm 字节码。在 scala 级别上,您有隐式、非常强大的类型系统、按名称参数调用和其他类似的东西。在字节码中,一切都消失了。没有柯里化参数,没有隐式,只有简单的方法。运行时不需要区分() =&gt; A=&gt; A,它只是执行字节码。所有检查和验证,您得到的错误都来自分析 scala 代码的编译器,而不是字节码。在按名称编译的过程中只是替换为Function0,并且此类参数的所有用法都调用了apply方法,但这在解析阶段不会发生,但稍后,这就是为什么你会看到@987654325 @ 在编译器输出中。尝试查看后面的阶段。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-03-25
    • 1970-01-01
    • 2017-08-21
    • 1970-01-01
    • 2014-03-24
    • 1970-01-01
    • 1970-01-01
    • 2012-03-10
    相关资源
    最近更新 更多