Возможно ли, чтобы Scala обновил дженерики без изменения JVM?
Недавно я начал изучать Scala и был разочарован (но не удивлен), что их дженерики также реализованы с помощью стирания типа.
Мой вопрос: возможно ли, чтобы Scala обновил дженерики, или же JVM нужно каким-то образом изменить? Если JVM необходимо изменить, что именно нужно изменить?
Ответы
Ответ 1
Нет - невозможно, чтобы Scala выполнялся как байт-код, эквивалентный Java, если этот байт-код не поддерживает повторяющиеся дженерики.
Когда вы спрашиваете "что это нужно изменить?", ответ: спецификация байт-кода. В настоящее время байт-код не позволяет определить параметризованный тип переменной. Было решено, что в качестве модификации байт-кода для поддержки редифицированных генериков будет отменить обратную совместимость, что generics должны быть реализованы с помощью стирания типа.
Чтобы обойти это, Scala использовал силу своего механизма implicit
для определения Manifest
, который может быть импортирован в любую область для обнаружения информации о типе во время выполнения. Манифесты являются экспериментальными и в основном недокументированными, но они поступают как часть библиотеки в 2.8. Вот еще один хороший ресурс в Scala reified generics/Manifests
Ответ 2
Просто чтобы дополнить oxbow_lakes, есть вопрос о переполнении стека о о том, как обойти стирание стилей в Scala.
Ответ 3
"implicit Manifest" представляет собой трюк компилятора Scala, и он не генерирует дженериков в Scala reified. Компилятор Scala, когда он видит функцию с "неявным параметром m: Манифест [A]", а знает общий тип A на сайте вызова, он перенесет класс A и его параметры типового типа в манифест и сделать его доступным внутри функции. Однако, если он не мог определить истинный тип A, тогда у него нет способа создать манифест. Другими словами, манифест должен передаваться по цепочке вызова функции, если ей нужна внутренняя функция.
scala> def typeName[A](a: A)(implicit m: reflect.Manifest[A]) = m.toString
typeName: [A](a: A)(implicit m: scala.reflect.Manifest[A])java.lang.String
scala> typeName(List(1))
res6: java.lang.String = scala.collection.immutable.List[int]
scala> def foo[A](a: A) = typeName(a)
<console>:5: error: could not find implicit value for parameter m:scala.reflect.Manifest[A].
def foo[A](a: A) = typeName(a)
^
scala> def foo[A](a: A)(implicit m: reflect.Manifest[A]) = typeName(a)
foo: [A](a: A)(implicit m: scala.reflect.Manifest[A])java.lang.String
scala> foo(Set("hello"))
res8: java.lang.String = scala.collection.immutable.Set[java.lang.String]
Ответ 4
Чтобы дополнить oxbow_lakes ответ: это невозможно, и похоже, что это никогда не произойдет (по крайней мере, скоро).
(опровержимые) причины JVM не будет поддерживать повторяющиеся дженерики, кажется:
- Более низкая производительность.
- Он нарушает обратную совместимость. Это можно решить, дублируя и фиксируя множество библиотек.
- Он может быть реализован с использованием манифестов: "Решение" и самое большое препятствие.
Литература:
Вы можете легко сравнить его и увидеть, что влияние производительности очень заметно. Особенно сильно увеличивается потребление памяти.
Я считаю, что путь - это необязательное овеществление, как мы начните делать в Scala с помощью Manifests/TypeTags.
Если вы можете и объединить его со специализацией во время выполнения, вы можете высокопроизводительный и общий код. Однако, возможно, для Scala 2.12 или 2.13.
Ответ 5
Как только scalac является компилятором, у него есть потенциал, позволяющий украсить сгенерированный код любыми структурами данных, необходимыми для реализации повторяющихся генериков.
Я имею в виду, что scalac будет иметь возможность видеть...
// definition
class Klass[T] {
value : T
}
//calls
floats = Klass[float]
doubles = Klass[double]
... и "expand" примерно так:
// definition
class Klass_float {
value : float
}
class Klass_double {
value : double
}
// calls
floats = Klass_float
doubles = Klass_double
Edit
Дело в том, что компилятор имеет возможность создавать все необходимые структуры данных, которые демонстрируют необходимость предоставления дополнительной информации о типе во время выполнения. Как только информация этого типа будет доступна, среда выполнения Scala воспользуется ею и сможет выполнять все операции с типом, которые мы можем себе представить. Не имеет значения, поддерживает ли JVM байт-код для генерируемых дженериков или нет. Работа не выполняется JVM, а библиотекой Scala.
Если вы уже написали символический отладчик (я сделал!), вы знаете, что вы можете в основном "сбрасывать" всю информацию, которую компилятор имеет во время компиляции, в сгенерированный двоичный код, принимая любую организацию данных, которая демонстрирует, что она более удобна для дальнейшая обработка. Это точно та же идея: "дамп" вся информация о типе, которую имеет компилятор Scala.
Вкратце, я не понимаю, почему это невозможно, неважно, поддерживает ли JVM собственные операции для генерируемых дженериков или нет.
Другое редактирование
IBM X10 демонстрирует способность, о которой я говорю: он компилирует код X10 на Java-код, используя обобщенные генерики на платформах Java. Как я уже упоминал ранее: это можно сделать, но только люди, которые знают, как работает компилятор, и как библиотека времени выполнения может использовать информацию, "сбрасываемую" компилятором во время компиляции на сгенерированный код. Дополнительная информация: http://x10.sourceforge.net/documentation/papers/X10Workshop2012/slides/Takeuchi.pdf