Как Scala различать() => T и => T

Мой другой вопрос закрыт как дубликат, поэтому я попробую это снова. Я также прочитал этот вопрос, и то, что я прошу, отличается. Мне интересно изучить внутреннюю реализацию того, как 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;
    }
}

РЕДАКТИРОВАТЬ 1: Scala Дерево синтаксиса: 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: Исследование рассылки:

Прочитайте этот интересный пост в списке рассылки, который по существу утверждает, что => T реализован как () => T. Цитата:

Во-первых, посмотрите

f: => Boolean

Хотя это называется "параметром по имени", он фактически реализуется как Function0,

f: () => Boolean

просто с различным синтаксисом, используемым на обоих концах.

Теперь меня еще больше смущает этот ответ, в котором явно указано, что они разные.

Вопросы

  • Как Scala отличить bar от baz? Сигнатуры метода (а не реализации) для обоих идентичны в декомпилированном коде.
  • Различия в двух сценариях не сохраняются в скомпилированном байт-коде?
  • Является ли декомпилированный код неточным?
  • Добавлено после редактирования 1: я обнаружил, что дерево синтаксиса scalac показывает разницу, bar имеет второй аргумент типа _root_.scala.<byname>[Int]. Что оно делает? Любое объяснение, указатели в Scala source или эквивалентный псевдокод будут полезны.
  • См. EDIT 2 выше: Правильно ли указан кавычек? Как и в, есть => T специальный подкласс Function0?

Ответ 1

Как Scala отличить bar от baz? Подписи методов (не реализация) для обоих идентичны в декомпилированном коде.

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 также создают символы, так что, если один этап зависит от конкретной детали реализации, он будет доступен. <byname> - это символ компилятора, который показывает, что этот метод использует "call-by-name", так что одна из фаз может видеть это и что-то делать с ней.

Ответ 2

Scala код анализируется компилятором и превращается в jvm bytecode. На уровне scala у вас есть implicits, очень сильная система типов, вызывается по параметрам имени и тому подобное. В байт-коде все пропало. Никаких параметров в карри, никаких имплицитов, простых методов. Время выполнения не нужно различать () => A и => A, оно просто исполняет байт-код. Все проверки и проверки, ошибки вы получаете, от компилятора, который анализирует scala код, а не байт-код. В процессе компиляции по имени просто заменяется на Function0, и все применения такого параметра имеют метод apply, вызываемый на них, но это не происходит в фазе анализа, но позже, вот почему вы видите <byname> в выводе компилятора. Попробуйте взглянуть на более поздние этапы.

Ответ 3

Потому что Scala работает именно так. Он компилирует Scala код в .class файлы и выполняет в JVM. Следовательно, файл .class должен иметь необходимую и достаточную информацию.

Это так. Эта информация хранится в аннотации под названием @ScalaSignature. javap -v должен показывать свое присутствие, но он не читается человеком.

Это необходимо, потому что в Scala подписях есть много информации, которые не могут быть представлены в байт-коде JVM: не только по имени vs Function0, но и к квалификаторам параметров, именам параметров и т.д.