Безопасно ли приводить функцию [X, Y] X => Y к X => (), пока я игнорирую возвращаемое значение?

#scala #jvm-bytecode

#scala #jvm-байт-код

Вопрос:

 val f = { x :Int => x }
val g = f.asInstanceOf[Int, Unit] 
g(1) //works
 

Насколько хрупким является приведенный выше код? В настоящее время это работает, но, конечно, это не является частью спецификации языка. По этой причине, конечно, возможно, что однажды этого не произойдет; мой вопрос в том, насколько это вероятно, понимается в смысле разумных альтернатив для компилятора. Я плохо знаю байт-код, но я мог бы легко представить, что он ломается, пытаясь поместить an Int в Unit регистр. С другой стороны, эта работа может фактически быть недокументированным следствием документированной спецификации
компилятора Scala и самого байт-кода Java.

Моя мотивация заключается в первую очередь в @specialized коде. Например, Iterable содержит следующий метод:

 trait Iterable[E] /* ... */
    def foreach[T](f :E => T) :Unit
 

Вероятно, эта подпись была выбрана для того, чтобы разрешить передачу любой функции E , которая может быть в качестве аргумента.
Однако на практике это почти всегда будет значение лямбда / метода. Проблема с приведенным выше методом
заключается в том, что для этого потребуется специализироваться на обоих E и T , что приводит к квадратному числу вариантов метода, чего следует избегать. Не специализироваться на T бессмысленно, потому что scalac не выделяет @specialized подклассы для подмножеств параметров типа: если какой-либо из @specialized параметров получит ссылочный (или неспециализированный) тип в качестве аргумента, весь объект будет экземпляром удаленного суперкласса. Аналогично, нет apply(Int):Object метода — если возвращаемый тип стерт, то apply(Object):Object будет вызван erased, что приведет к боксу. Таким образом, в этом случае было бы гораздо более желательно иметь

 def foreach(f :E => Unit) :Unit
 

Введение такого метода (с другим именем) и использование его в качестве целевого объекта делегирования обычного foreach возможно и не приводит к какому-либо боксу, по крайней мере, до тех пор, пока созданный лямбда-выражение не имеет ссылочного типа в качестве возвращаемого типа.

Комментарии:

1. Я немного растерялся, мотивом этого вопроса является то, что вы хотите добавить свою собственную итерацию , которая является специализированной?

2. Моя собственная коллекция, в этом примере, да. И иметь линейное число, а не n ^ 2, специализированных методов.

Ответ №1:

Зависит от контекста.

Большую часть времени Scala будет компилировать код, так что функция, возвращающая Unit , будет вызываться как JVM void (без возвращаемого результата) или будет рассматривать ее как единственное возвращаемое scala.Unit значение, которое можно отбросить. Пока JVM не будет пытаться вызвать что-либо для этого возвращаемого значения, она будет только предполагать, что это то java.lang.Object , что следует проверять scala.Unit , если оно используется для чего-либо, чего никогда не может произойти.

Но это не значит, что это невозможно.

 val f: Int => Int = _ * 2
val g: Unit => Int = _ => 10
(f.asInstanceOf[Int => Unit] andThen g)(10)
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class scala.runtime.BoxedUnit (java.lang.Integer is in module java.base of loader 'bootstrap'; scala.runtime.BoxedUnit is in unnamed module of loader 'app')
  scala.Function1.$anonfun$andThen$1(Function1.scala:85)
  scala.Function1.apply$mcII$sp(Function1.scala:69)
  ammonite.$sess.cmd27$.<clinit>(cmd27.sc:1)
 

Пока вы вызываете ее в своем собственном коде, что у вас есть полный контроль, Всегда знаете контекст, тест и т. Д., Все должно быть в порядке.

Что, по моему мнению, является плохим предположением практически для каждой библиотеки в мире или любой кодовой базы, которая будет поддерживаться более одной недели.

 f.andThen(_ => ())
 

всегда было бы безопаснее, и у меня должны были быть действительно веские причины использовать оптимизацию, например .asInstanceOf , и рисковать некоторыми забавными ошибками в будущем.

Ответ №2:

asInstanceOf является по своей сути небезопасным универсальным аварийным люком. Когда вы его используете, вы действительно говорите проверяющему типу слепо доверять вам.

В общем, когда вы пишете x.asInstanceOf[Y] , вы даете обещание компилятору, что значение x действительно имеет тип Y .

Это гарантированно работает только в том случае, если x действительно имеет тип Y . Если нет, компилятор выполнит некоторую специальную обработку, если x статически известно, что это примитивный тип, или null . Тогда произойдет одно из двух:

  • Среда выполнения раскусит вашу ложь и выдаст a ClassCastException .
  • Вам сходит с рук ваша ложь и, возможно, другие вещи нарушают линию.

В вашем примере f имеет тип Int => Int , который явно не является подтипом Int => Unit . Другими словами: вы лжете компилятору, и все гарантии недействительны.

Мораль этой истории такова: вы никогда не должны полагаться на детали реализации для корректности, и вы всегда должны относиться asInstanceOf к ней как к небезопасной.

Как писал Матеуш, f.andThen(_ => ()) или, что эквивалентно { x => {f(x); ()}} , это единственный правильный способ преобразовать некоторые A => B в A => Unit .

Вы также должны отметить, что, хотя возможно генерировать специализированные методы, которые принимают специализированные функции в качестве аргументов, (x => x).asInstanceOf[Int => Unit] они всегда будут возвращать неспециализированный Function1[Int,Unit] , а не специализированный тип, так что это вам совсем не поможет с производительностью.