Как замыкания Scala преобразуются в объекты Java?

В настоящее время я просматриваю реализации закрытия на разных языках. Однако когда дело доходит до Scala, я не могу найти никакой документации о том, как замыкание отображается на объекты Java.

Хорошо задокументировано, что функции Scala сопоставляются с объектами FunctionN. Я предполагаю, что ссылка на свободную переменную замыкания должна храниться где-то в этом функциональном объекте (как это делается, например, в C++0x).

Я также попытался скомпилировать следующее с помощью scalac, а затем декомпилировать файлы классов с помощью JD:

object ClosureExample extends Application { 
  def addN(n: Int) = (a: Int) => a + n
  var add5 = addN(5)
  println(add5(20))
}

В декомпилированных исходниках я вижу анонимный подтип Function1, который должен быть моим замыканием. Но метод apply() пуст, а в анонимном классе нет полей (которые потенциально могут хранить переменные замыкания). Я предполагаю, что декомпилятору не удалось извлечь интересную часть из файлов классов...

Теперь к вопросам:

  • Вы точно знаете, как происходит трансформация?
  • Вы знаете, где это задокументировано?
  • У тебя есть другая идея, как я могу разгадать тайну?

person theDmi    schedule 19.04.2010    source источник
comment
Вы уверены, что используете javap -c 'ClassName$$anonfun$etcetc' для декомпиляции?   -  person Rex Kerr    schedule 20.04.2010


Ответы (2)


Давайте разберем набор примеров, чтобы увидеть, чем они отличаются. (Если вы используете RC1, скомпилируйте с -no-specialization, чтобы упростить понимание.)

class Close {
  var n = 5
  def method(i: Int) = i+n
  def function = (i: Int) => i+5
  def closure = (i: Int) => i+n
  def mixed(m: Int) = (i: Int) => i+m
}

Во-первых, давайте посмотрим, что делает method:

public int method(int);
  Code:
   0:   iload_1
   1:   aload_0
   2:   invokevirtual   #17; //Method n:()I
   5:   iadd
   6:   ireturn

Довольно просто. Это метод. Загрузите параметр, вызовите геттер для n, добавьте, верните. Похоже на Яву.

Как насчет function? На самом деле она не закрывает никаких данных, но это является анонимной функцией (называется Close$$anonfun$function$1). Если игнорировать какую-либо специализацию, наибольший интерес представляют конструктор и применение:

public scala.Function1 function();
  Code:
   0:   new #34; //class Close$$anonfun$function$1
   3:   dup
   4:   aload_0
   5:   invokespecial   #35; //Method Close$$anonfun$function$1."<init>":(LClose;)V
   8:   areturn

public Close$$anonfun$function$1(Close);
  Code:
   0:   aload_0
   1:   invokespecial   #43; //Method scala/runtime/AbstractFunction1."<init>":()V
   4:   return

public final java.lang.Object apply(java.lang.Object);
  Code:
   0:   aload_0
   1:   aload_1
   2:   invokestatic    #26; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
   5:   invokevirtual   #28; //Method apply:(I)I
   8:   invokestatic    #32; //Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
   11:  areturn

public final int apply(int);
  Code:
   0:   iload_1
   1:   iconst_5
   2:   iadd
   3:   ireturn

Итак, вы загружаете указатель this и создаете новый объект, который принимает в качестве аргумента окружающий класс. На самом деле это стандарт для любого внутреннего класса. Функции не нужно ничего делать с внешним классом, поэтому она просто вызывает конструктор super. Затем при вызове apply вы делаете трюки с коробкой/распаковкой, а затем вызываете фактическую математику, то есть просто добавляете 5.

Но что, если мы используем замыкание переменной внутри Close? Настройка точно такая же, но теперь конструктор Close$$anonfun$closure$1 выглядит так:

public Close$$anonfun$closure$1(Close);
  Code:
   0:   aload_1
   1:   ifnonnull   12
   4:   new #48; //class java/lang/NullPointerException
   7:   dup
   8:   invokespecial   #50; //Method java/lang/NullPointerException."<init>":()V
   11:  athrow
   12:  aload_0
   13:  aload_1
   14:  putfield    #18; //Field $outer:LClose;
   17:  aload_0
   18:  invokespecial   #53; //Method scala/runtime/AbstractFunction1."<init>":()V
   21:  return

То есть он проверяет, не равен ли ввод null (т. е. внешний класс не равен null), и сохраняет его в поле. Теперь, когда пришло время применить его, после упаковки/распаковки:

public final int apply(int);
  Code:
   0:   iload_1
   1:   aload_0
   2:   getfield    #18; //Field $outer:LClose;
   5:   invokevirtual   #24; //Method Close.n:()I
   8:   iadd
   9:   ireturn

вы видите, что он использует это поле для ссылки на родительский класс и вызывает геттер для n. Добавить, вернуться, готово. Таким образом, замыкания достаточно просты: конструктор анонимной функции просто сохраняет окружающий класс в приватном поле.

А что, если мы закроем не внутреннюю переменную, а аргумент метода? Это то, что делает Close$$anonfun$mixed$1. Во-первых, посмотрите, что делает метод mixed:

public scala.Function1 mixed(int);
  Code:
   0:   new #39; //class Close$$anonfun$mixed$1
   3:   dup
   4:   aload_0
   5:   iload_1
   6:   invokespecial   #42; //Method Close$$anonfun$mixed$1."<init>":(LClose;I)V
   9:   areturn

Он загружает параметр m перед вызовом конструктора! Поэтому неудивительно, что конструктор выглядит так:

public Close$$anonfun$mixed$1(Close, int);
  Code:
   0:   aload_0
   1:   iload_2
   2:   putfield    #18; //Field m$1:I
   5:   aload_0
   6:   invokespecial   #43; //Method scala/runtime/AbstractFunction1."<init>":()V
   9:   return

где этот параметр сохраняется в частном поле. Никакая ссылка на внешний класс не сохраняется, потому что он нам не нужен. И вас не должно удивлять применение:

public final int apply(int);
  Code:
   0:   iload_1
   1:   aload_0
   2:   getfield    #18; //Field m$1:I
   5:   iadd
   6:   ireturn

Да, мы просто загружаем это сохраненное поле и выполняем расчеты.

Я не уверен, что вы делали, чтобы не увидеть это в своем примере - объекты немного сложны, потому что они имеют классы MyObject и MyObject$, а методы разделяются между ними таким образом, что это может быть не интуитивно понятно. Но apply определенно применяет вещи, и в целом вся система работает в значительной степени так, как вы ожидаете (после того, как вы сядете и очень долго обдумаете это).

person Rex Kerr    schedule 19.04.2010
comment
Спасибо большое, очень подробно. Я пытался использовать декомпилятор, который восстанавливает код Java (он называется JD), потому что я не привык к байт-коду, и это не сработало. Я думаю, что декомпилятор неправильно обрабатывает анонимные классы. Теперь, когда я изучил основы байт-кода Java, ваш ответ легко понять. Знаете ли вы какие-либо официальные документы об этом преобразовании? - person theDmi; 20.04.2010
comment
@iguana: я нигде не знаю, чтобы это преобразование было подробно задокументировано. В любом случае я склонен верить байт-коду больше, чем документам. - person Rex Kerr; 20.04.2010

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

Напротив, в Java их значения копируются в поля внутреннего класса, поэтому язык требует, чтобы исходные значения во внешней среде были равны final, чтобы они никогда не расходились.

Поскольку все ссылки / замыкания функционального литерала Scala на значения во внешней среде находятся в коде метода apply() функционального литерала, они не отображаются как поля в фактическом подклассе Function, сгенерированном для функционального литерала.

Я не знаю, как вы декомпилируете, но подробности того, как вы это делаете, вероятно, объясняют, почему вы не видите никакого кода для тела метода apply().

person Randall Schulz    schedule 19.04.2010
comment
Спасибо, я так и подозревал. Я буду исследовать мою проблему декомпиляции дальше. Есть ли у вас какие-либо официальные источники, в которых задокументировано такое поведение? Это было бы очень полезно, потому что мне нужно что-то, что я могу процитировать :-). - person theDmi; 20.04.2010
comment
Ничто из того, что я написал, не является авторитетным. Полагаю, я должен был сказать, что... Существует Уголок компилятора Scala (sts.tu-harburg.de/people/mi.garcia/ScalaCompilerCorner), который является хорошим ресурсом и может содержать такую ​​информацию. - person Randall Schulz; 20.04.2010