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.
> …..
> 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.
PolubieniePolubienie
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 ;)
PolubieniePolubienie
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)
PolubieniePolubienie
Właśnie się trochę wahałem czy tak ma być, czy nie :) Ale nie chciało mi się sprawdzać w jakimś Lispie jak to tam będzie wyglądać.
PolubieniePolubienie
BTW: jakbym to zapisał w Javie za pomocą klas wewnętrznych, to by było tak, jak się spodziewałem po tym przykładzie…
PolubieniePolubienie
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).
PolubieniePolubienie
> 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…
PolubieniePolubienie
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.
PolubieniePolubienie
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
PolubieniePolubienie
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ę :)
PolubieniePolubienie
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ć.
PolubieniePolubienie