Как замыкания Scala преобразуются в объекты Java? - PullRequest
18 голосов
/ 19 апреля 2010

В настоящее время я смотрю на реализации замыкания на разных языках. Однако когда дело доходит до 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 () пуст, и у анонимного класса нет полей (которые потенциально могут хранить переменные замыкания). Полагаю, декомпилятору не удалось вытащить интересную часть из файлов классов ...

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

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

Ответы [ 2 ]

31 голосов
/ 20 апреля 2010

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

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

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

5 голосов
/ 20 апреля 2010

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

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

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

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

...