Как 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
, но и к квалификаторам параметров, именам параметров и т.д.