Подтвердить что ты не робот

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

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

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

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

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

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

Теперь на вопросы:

  • Знаете ли вы, как это делается точно?
  • Вы знаете, где это задокументировано?
  • Есть ли у вас другая идея, как я могу решить эту тайну?
4b9b3361

Ответ 1

Давайте разберем набор примеров, чтобы мы могли видеть, как они отличаются. (Если вы используете 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

Довольно просто. Это метод. Загрузите параметр, запустите getter для n, добавьте, верните. Выглядит так же, как Java.

Как насчет 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" и создаете новый объект, который в качестве аргумента принимает охватывающий класс. Это действительно стандарт для любого внутреннего класса. Функция не должна ничего делать с внешним классом, поэтому она просто вызывает супер-конструктор. Затем, при вызове apply, вы делаете тэки box/unbox, а затем вызываете фактическую математику, то есть просто добавляете 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) и сохраняет его в поле. Теперь, когда пришло время применить его, после обертки бокса/распаковки:

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

вы видите, что оно использует это поле для обращения к родительскому классу и вызывает getter для 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$, и методы становятся разделенными между ними таким образом, что не может быть интуитивным. Но применять определенно применяет вещи, и в целом вся система работает почти так, как вы ожидали (после того, как вы сядете и подумаете об этом очень тяжело на очень долгое время).

Ответ 2

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

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

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

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