W jaki sposób zamknięcia Scali przekształcają się w Obiekty Javy?

Obecnie patrzę na implementacje zamknięcia w różnych językach. Jeśli chodzi o Scalę, nie jestem jednak w stanie znaleźć żadnej dokumentacji na temat mapowania zamknięcia do obiektów Java.

Jest dobrze udokumentowane, że funkcje Scali są mapowane do obiektów FunctionN. Zakładam, że odniesienie do wolnej zmiennej zamknięcia musi być zapisane gdzieś w tym obiekcie funkcji (jak to się robi np. w C++0x).

Próbowałem też skompilować następujące z scalakiem, a następnie dekompilacja plików klasy za pomocą JD:

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

W dekompilowanych źródłach widzę anonimowy Podtyp funkcji 1, który powinien być moim zamknięciem. Ale metoda apply() jest pusta, a klasa anonimowa nie ma pól (które potencjalnie mogłyby przechowywać zmienne zamykające). Przypuszczam, że dekompilator nie zdołał wyciągnąć interesującej części z plików klasowych...

A teraz do pytań:

  • Czy wiesz, jak dokładnie odbywa się transformacja?
  • czy wiesz, gdzie to jest udokumentowane?
  • Masz inny pomysł, jak mogę rozwiązać zagadkę?
Author: theDmi, 2010-04-19

2 answers

Podzielmy zestaw przykładów, abyśmy mogli zobaczyć, jak się różnią. (Jeśli używasz RC1, skompiluj za pomocą -no-specialization, aby rzeczy były łatwiejsze do zrozumienia.)

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
}

Po pierwsze, zobaczmy co method robi:

public int method(int);
  Code:
   0:   iload_1
   1:   aload_0
   2:   invokevirtual   #17; //Method n:()I
   5:   iadd
   6:   ireturn
Całkiem proste. To metoda. Załaduj parametr, wywołaj getter dla n, add, return. Wygląda jak Jawa.

A może function? W rzeczywistości nie zamyka żadnych danych, ale jest funkcją anonimową (zwaną Close$$anonfun$function$1). Jeśli zignorujemy jakiekolwiek w 2011 roku został wybrany do Izby Reprezentantów.]}

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

Tak więc, ładujesz wskaźnik" ten " i tworzysz nowy obiekt, który przyjmuje klasę zamykającą jako swój argument. To standard dla każdej klasy wewnętrznej. Funkcja nie musi nic robić z zewnętrzną klasą, więc wywołuje konstruktor super. Następnie, podczas wywoływania apply, wykonujesz triki box/unbox, a następnie wywołujesz rzeczywistą math-czyli po prostu dodaj 5.

Ale co jeśli użyjemy zamknięcia zmienna wewnątrz blisko? Konfiguracja jest dokładnie taka sama, ale teraz konstruktor Close$$anonfun$closure$1 wygląda tak:

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

To znaczy sprawdza, czy dane wejściowe są inne niż null (tzn. Klasa zewnętrzna jest inna niż null) i zapisuje je w polu. Teraz, gdy przychodzi czas, aby go zastosować, po boxing / unboxing wrapper:

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

Widzisz, że używa tego pola do odwoływania się do klasy nadrzędnej i wywołuje getter dla n. Dodaj, wróć, gotowe. Tak więc zamknięcie jest dość łatwe: anonimowy function constructor zapisuje klasę w prywatnym polu.

A jeśli zamkniemy Nie zmienną wewnętrzną, ale argument metody? To właśnie robi Close$$anonfun$mixed$1. Po pierwsze, spójrz na to, co robi metoda 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

Ładuje parametr m przed wywołaniem konstruktora! Nic więc dziwnego, że konstruktor wygląda tak: {]}

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

Gdzie parametr jest zapisany w polu prywatnym. Nie przechowuje się odniesienia do klasy zewnętrznej bo tego nie potrzebujemy. I Ty też nie powinieneś się dziwić apply:

public final int apply(int);
  Code:
   0:   iload_1
   1:   aload_0
   2:   getfield    #18; //Field m$1:I
   5:   iadd
   6:   ireturn
Tak, załadujemy to pole i policzymy.

Nie jestem pewien, co robiłeś, aby nie zobaczyć tego na swoim przykładzie--obiekty są trochę trudne, ponieważ mają zarówno MyObject, jak i MyObject$ klasy, A metody są dzielone między nimi w sposób, który może nie być intuicyjny. Ale apply zdecydowanie dotyczy rzeczy, i ogólnie cały system działa prawie tak, jak można się go spodziewać (po usiąść i myśleć o tym naprawdę ciężko przez naprawdę długi czas).

 31
Author: Rex Kerr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-04-19 20:53:15

W przeciwieństwie do anonimowych klas wewnętrznych Javy, które są pseudo-zamknięciami i nie mogą modyfikować zmiennych, które wydają się być zamknięte w swoim środowisku, zamknięcia Scali są rzeczywiste, więc kod zamknięcia bezpośrednio odwołuje się do wartości w otaczającym środowisku. Takie wartości są kompilowane inaczej, gdy odwołują się do nich z zamknięcia, aby było to możliwe (ponieważ kod metody nie ma możliwości dostępu do lokalnych ramek aktywacji innych niż bieżąca jeden).

W przeciwieństwie do Javy ich wartości są kopiowane do pól w klasie wewnętrznej, dlatego język wymaga, aby oryginalne wartości w otaczającym środowisku były final, więc nigdy nie mogą się różnić.

Ponieważ wszystkie odwołania do funkcji Scala dosłowne / zamknięcia do wartości w środowisku otaczającym znajdują się w kodzie metody apply() dosłownej funkcji, nie pojawiają się one jako pola w rzeczywistej podklasie Function Wygenerowanej dla funkcji dosłownej.

I don ' t wiem, jak dekompilujesz, ale szczegóły tego, jak to zrobiłeś, prawdopodobnie wyjaśniają, dlaczego nie widzisz żadnego kodu dla ciała metody apply().

 5
Author: Randall Schulz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-04-19 20:28:28