Pseudoclosure

Dostałem zadanie napisania czegoś większego w JavaScripcie. Taka biblioteczka. JavaScript nawet lubię, ale zadanie mi się nie spodobało. Ale podczas jego realizacji nadziałem się na coś, co mnie bardzo rozczarowało (zdaje się, że s już to wytykał). Otóż – JavaScript podobno ma „closures”. Ale popatrzmy na następujący fragment:

	var arr=[];
	for (var i=0; i < 5; i++)
		arr.push(function() {return i;});
	var ret=[];
	for (var j=0; j < arr.length; j++)
		ret.push(arr[j]());
	return ret.join(", ");

Wynik: „5, 5, 5, 5, 5”.

WTF?!?!

Funkcja w interesującej nas pętli mianowicie łapie zmienną i a nie jej wartość. Może to mieć iście katastrofalne skutki – wystarczy w drugiej pętli zamienić j na i i nigdy byśmy się nie dowiedzieli, że coś jest nie tak, ponieważ wynik by był „poprawny”: „0, 1, 2, 3, 4”. Nie pomaga nawet wstawienie pomocniczej zmiennej w naszej funckji: var k=i; oraz return k; – w momencie wykonania tej funkcji zmienna i już ma niepoprawną wartość.

Drugi przykład:

	var arr=[];
	for (var i=0; i < 5; i++)
		(function()
		{
			var k=i;
			arr.push(function() {return k;});
		})();
	var ret=[];
	for (var j=0; j < arr.length; j++)
		ret.push(arr[j]());
	return ret.join(", ");

Tym razem wynik jest „poprawny”. Tym razem tworzymy funkcję i natychmiast wymuszamy jej wykonanie (to ten las nawiasów na końcu pętli :)). Pomocnicza zmienna k jest konieczna, żeby złapać wartość i w momencie wykonania. I działa…

Podejrzewam, że to jest zgodne ze standardem (IE i Mozilla wykonują to tak samo) i ma logiczne uzasadnienie (function() {...} tworzy funkcję, ale to wszystko co robi). Mimo wszystko zdenerwowało mnie to.

11 uwag do wpisu “Pseudoclosure

  1. > …..
    > Ale podczas jego realizacji nadziałem się na coś,
    > co mnie bardzo rozczarowało (zdaje się, że s już to
    > wytykał).

    s wytykał fakt że w JS ''przeciekają'' bloki kodu
    ograniczone nawiasami klamrowymi, co gwałci
    ''principle of least surprise''.

    > var arr=[]; for (var i=0; i < 5; i++) arr.push(function() {return i;});
    > var ret=[]; for (var j=0; j < arr.length; j++) ret.push(arr[j]());
    > return ret.join(", ");

    > Wynik: "5, 5, 5, 5, 5".

    Tak ma być. To czego się domagasz to dodatkowy, domyślny
    poziom ewaluacji :> "closures" są przydatne właśnie
    dlatego że go nie ma :> proponuję żebyś sobie to jeszcze
    raz przemyślał.

    Translacja Twojego przykładu do LISPu daje taki sam wynik:

    CL-USER> (loop repeat 5 for i = 0 then (1+ i) collect (lambda () i))

    (#<CLOSURE (LAMBDA #) {9333095}> #<CLOSURE (LAMBDA #) {93330AD}> #<CLOSURE (LAMBDA #) {93330C5}> #<CLOSURE (LAMBDA #) {93330DD}> #<CLOSURE (LAMBDA #) {93330F5}>)

    CL-USER> (mapcar #'funcall *)

    (4 4 4 4 4)

    i tak ma być !

    W JS jest ''skuźlone coś'' co tfurca JS nazywa lambdą —
    nie jest to LAMBDA — bardziej już EVAL. Dziwadełko
    to pozwala zapisać Twój przykład w nieco bardziej
    zwięzłej formie (no i daje taki wynik jaki byś chciał),
    tzn.:

    <html><head><title></title></head>
    <body>
    var arr=[]; for (var i=0; i var ret=[]; for (var j=0; j document.write(„(„+ret.join(” „)+”)”);
    </body>
    </html>

    W LISPie (mniej-więcej) odpowiednikiem jest:

    (loop repeat 5 for i = 0 then (1+ i) collect (eval `(lambda () ,i)))

    tzn.: (mapcar #'funcall (loop repeat 5 for i = 0 then (1+ i) collect (eval `(lambda () ,i))))
    daje: (0 1 2 3 4)

    Oczywiście w lispie nie trzeba używać EVAL do tak
    trywialnej rzeczy (kwestia wydajności i czytelności
    kodu chociażby) — poniżej 3 sexpy które dają taki
    sam wynik:

    (mapcar #'funcall (loop repeat 5 for i = 0 then (1+ i) collect ((lambda (i) (lambda () i)) i)))

    (mapcar #'funcall (loop repeat 5 for i = 0 then (1+ i) collect (let ((i i)) (lambda () i))))

    (mapcar #'funcall (loop repeat 5 for i = 0 then (1+ i) collect (constantly i)))

    pierwsze dwa przykłay są tylko po to coby pokazać
    że LAMBDA i LET są równoważne (koncepcyjnie rzecz
    biorąc).

    pozdrawiam.

    Polubienie

  2. ps. w sumie to ''closures'' rzadko wykorzystuje się tak:
    (loop repeat 5 for i = 0 then (1+ i) collect (constantly i))
    czy tak(*):
    (loop repeat 5 for i = 0 then (1+ i) collect (eval `(lambda () ,i)))
    na ogół chodzi nie o stałą a o zmienną do której definiujemy
    jakiś interfejs.

    (*) ten przykład z EVAL jest ''przesadzony'', chodziło
    mi o to coby pokazać coś równoważnego pseudolambdzie w
    JS. W LISPie wystarczy DEFUN/LAMBDA/LET. EVAL używa się
    raz do roku ;)

    Polubienie

  3. Btw, początkowo byłem zdziwiony że wynik to piątki a nie
    czwórki, ale po namyśle stwierdziłem że można to zaakceptować
    bo takie użycie 'for' jak w Twoim pierwszym przykładzie
    nie ma praktycznego zastosowania. W LISPie zresztą jest
    tak samo:
    CL-USER> (mapcar #'funcall (loop for i below 5 collect (lambda () i)))
    (5 5 5 5 5)

    Polubienie

  4. W sumie to nie napisałem najważniejszego — to jaki będzie wynik
    tak naprawdę zależy od konstrukcji sterującej (for w przypadku
    javascript-u / LOOP w przypadku LISP-u).

    Mogę w LISP-ie napisać dwie całkowicie równoważne konstrukcje
    serujące… tzn. równoważne pod każdym względem(*) za wyjątkiem tego
    jak będą się zachowywać lambdy. Pierwsza z nich działa tak jak
    chciałeś żeby działał 'for' w javascripcie, druga działa tak jak w
    rzeczywistości działa 'for' w javascripcie lub LOOP w LISP-ie.

    (DEFMACRO FROM-ZERO-UPTO-AND-COLLECT-1 (VAR STOP &BODY BODY &AUX (END (GENSYM))) `(LET* ((,END ,STOP) (RESULT (LIST NIL)) (RESP RESULT)) (LABELS ((_ (,VAR) (SETQ RESP (CDR (RPLACD RESP (LIST ,@BODY)))) (UNLESS (EQL ,VAR ,END) (_ (1+ ,VAR))))) (_ 0)) (CDR RESULT)))

    (DEFMACRO FROM-ZERO-UPTO-AND-COLLECT-2 (VAR STOP &BODY BODY &AUX (END (GENSYM)) (FOO (GENSYM))) `(LET* ((,END ,(1+ STOP)) (RESULT (LIST NIL)) (RESP RESULT) (,VAR 0)) (TAGBODY ,FOO (SETQ RESP (CDR (RPLACD RESP (LIST ,@BODY)))) (INCF ,VAR) (UNLESS (EQL ,VAR ,END) (GO ,FOO))) (CDR RESULT)))

    Konstrukca sterująca FROM-ZERO-UPTO-AND-COLLECT 1 & 2 jest dość
    ograniczona bo napisałem ją tylko na potrzeby tego konkretnego
    zagadnienia. Bierze ona 3 argumenty: pierwszy to nazwa zmiennej,
    druga to liczba całkowita, trzecia to kod który zostanie
    wykonany.

    Na przykład

    (FROM-ZERO-UPTO-AND-COLLECT I 5 I) ; tzn. zarówno wersja 1 jak i 2.

    zwróci nam listę:

    (0 1 2 3 4 5)

    ,zaś

    (FROM-ZERO-UPTO-AND-COLLECT I 5 (* I I)) ; tzn. zarówno wersja 1 jak i 2.

    zwróci:

    (0 1 4 9 16 25)

    Przejdźmy do lambd:

    *** pierwsza ***
    CL-USER> (MAPCAR #'FUNCALL (FROM-ZERO-UPTO-AND-COLLECT-1 I 5 (LAMBDA () I)))

    ——-> (0 1 2 3 4 5)

    *** druga ***
    CL-USER> (MAPCAR #'FUNCALL (FROM-ZERO-UPTO-AND-COLLECT-2 I 5 (LAMBDA () I)))

    ——-> (6 6 6 6 6 6)

    pozdro.

    (*) pierwsze podejście _może_ być _minimalnie_ wolniejsze (mimo ogonówki).

    Polubienie

  5. > BTW: jakbym to zapisał w Javie za pomocą klas wewnętrznych,
    > to by było tak, jak się spodziewałem po tym przykładzie…

    Afaik Java nie ma czegoś co się zwie 'lexical closures', czy
    może się mylę ?
    I co takiego mają do rzeczy 'klasy wewnętrzne' ?

    ps. w przykładach z poprzedniego postu używałem jako nazwy
    zmiennej 'dużego i' które tu wygląda jak jedynka, ale mam
    nadzieję że się połapiesz…

    Polubienie

  6. ps. narobiłem literówek, tzn. miast:

    Bierze ona 3 argumenty: pierwszy to nazwa zmiennej,
    druga to liczba całkowita, trzecia to kod który zostanie
    wykonany.

    powinno być:

    Bierze ona 3 argumenty: pierwszy to nazwa zmiennej,
    drugi to liczba całkowita, trzeci to kod który zostanie
    wykonany.

    Polubienie

  7. Ale natrzaskałeś ;)

    Obawiam się, że gdybym to sprawdził wtedy, to bym i tak napisał tą notkę tak samo :)

    [trochę prymitywne, ale cóż]

    (let ((arr #f))
    (do ((i 0 (+ i 1)))
    ((= i 5))
    (set! arr (cons (lambda () i) arr)))
    (map (lambda (l) (l)) arr))

    To jest w Scheme i przynajmniej MIT/GNU Scheme i scsh dają wyniki dobre "po mojemu" ;) [tyle że od końca]
    Odnoszę wrażenie, że rozchodzi się o coś, co podobno w Scheme było nowością, czyli "lexical scoping", w odróżnieniu od "dynamic scoping" [r5rs, drugi akapit paragrafu 1.1 "Semantics" ;)]

    Java nie ma closur, ale klasy wewnętrzne je całkiem skutecznie imitują (stąd "lambda dla ubogich"). W Javie by to wyglądało tak (pewnie się paskudnie sformatuje):

    List<Callable<Integer>> list=new ArrayList<Callable<Integer>>();
    for (int i=0; i < 5; i++)
    {
    final int j=i;
    list.add(new Callable<Integer>()
    {
    public Integer call() throws Exception
    {
    return j;
    }
    });
    }
    for (Callable<Integer> c : list)
    System.out.println(c.call() + " ");

    Callable to typowy odpowiednik lambdy ;) Do listy dodawane są anonimowe klasy wewnętrzne implementujące Callable. Zauważ, że żeby zwrócić coś z wnętrza call(), muszę najpierw uczynić to final (skutek tego, że klasy wewnętrzne były dodane w wersji 1.1, a nie 1.0). W ten sposób w chwili tworzenia tej "lambdy" łapana jest wartość i. Wynik:
    0
    1
    2
    3
    4

    Polubienie

  8. Myślę że powinieneś zmienić #f w pierwszym LET na '() jako że w Scheme
    (w odróżnieniu od Common Lispu na przykład) lista pusta i fałsz
    logiczny nie są tożsame. Troszkę przerobiłem Twój kod:

    (define list-of-closures
    (do ((i 0 (+ i 1))
    (arr '() (cons (lambda () i) arr)))
    ((= i 5) (reverse arr))))

    (map (lambda (x) (x)) list-of-closures)

    '(0 1 2 3 4)

    Btw, myślę że nie jest tak jak napisałeś tzn. przyczyną tego że lambdy
    w iteratorach(1) scheme ,,zachowują się'' tak jakbyś sobie tego życzył
    nie jest kwestia przestrzeni nazw — tzn. czy jest dynamiczna czy
    leksykalna(2) — po prostu w scheme z definicji używa się tylko
    rekursji(3), tzn. operator DO w scheme jest zdefiniowany przy pomocy
    LETREC(4) (w common lispie odpowiednikem LETREC jest LABELS którego
    użyłem w pierszym makrze — tym które działa ,,jak trzeba''). W LISPie
    masz możliwość użycia albo LABELS/Y albo TAGBODY & GO — stąd lambdy w
    common lispie mogą się w iteratorach różnie zachowywać w zależności od
    rodzaju operatorów użytych do ich konstrukcji.

    (1) ,,iteratorach'' czyli LOOP, DO, DOTIMES, etc. Chodzi mi o
    operatory które służą to iterowania a nie to co potocznie nazywa się
    ,,iteratorem''.

    (2) w drugim z moich makr (chodzi mi o FROM-ZERO-UPTO-AND-COLLECT-2)
    — tym które daje ,,zły'' wynik, tzn. same szóstki nie ma żadnych
    zmiennych dynamicznych/specjalnych — wszystko jest leksykalne. Po
    prostu używam operatora TAGBODY i GO zamiast LABELS (w scheme LABELS
    nazywa się LETREC). LABELS używam w pierwszym makrze i działa ono
    ,,dobrze''.

    (3) chodzi mi rzecz jasna o rdzeń języka, afaik w scheme nie ma
    TAGBODY & GO bo mamy gwarancję ,,ogonowatości'' ;) Skoro więc
    wszystkie iteratory są ,,wtórne'' to (owe iteratory — DO na przykład)
    dziedziczą taką właśnie cechę LETREC (LABELS) która Ci odpowiada (mnie
    zresztą też).

    (4) oczywiście nie musi to być LETREC, można to zrobić przy pomocy
    DEFINE lub węzełka lambd (mam na myśli słynnego Y-greka
    http://www.ece.uc.edu/~franco/C511/html/Scheme/ycomb.html).

    pzdr.

    ps. dzięki za Javę :)

    Polubienie

  9. Wiem, że #f i '() są różnymi rzeczami, ale obie stwarzały jakieś problemy scsh i #f zostało z rozpędu ;)

    Mam do powiedzednia chyba tylko jedno: a wydawało mi się, że dużo rozumiem ;)
    W takim razie wygląda to na właściwie to samo, co ten udziwniony – "poprawiony" – fragment JavaScripta: wymuszenie nowego zasięgu nazw, w którym "i" to nie jest to samo "i" co przy poprzedniej iteracji.
    Hmmm.
    Muszę wymyśleć jakiś inny przykład, żeby to zbadać.

    Polubienie

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s