To jest "zamknięcie leksykalne" i masz rację, że num
, "zmienna zamknięta" jest podobna do zmiennej statycznej, na przykład w C: jest widoczna tylko dla kodu w postaci let
(jej "leksykalny" scope "), ale trwa przez cały czas trwania programu, a nie jest ponownie inicjowany przy każdym wywołaniu funkcji.
Wydaje mi się, że część, o której się mylisz, brzmi: "num
jest tworzony przez domenę next-num
, jest rodzajem zmiennej lokalnej". Nie jest to prawdą, ponieważ blok let
nie jest częścią funkcji next-num
: w rzeczywistości jest to wyrażenie, które tworzy i zwraca funkcję, która jest następnie powiązana z next-num
. (Jest to bardzo odmienne, np. Od C, gdzie funkcje mogą być tworzone tylko podczas kompilacji i poprzez definiowanie ich na najwyższym poziomie.W Schematach funkcje są wartościami takimi jak liczby całkowite lub listy, które każde wyrażenie może zwrócić).
Oto kolejny sposób napisać (prawie) to samo, co sprawia, że jaśniejsze że define
właśnie skojarzenie next-num
do wartości wyrażenia funkcyjnego-powrót:
(define next-num #f) ; dummy value
(let ((num 0))
(set! next-num
(lambda() (set! num (+ num 1)) num)))
Ważne jest, aby zwrócić uwagę na różnicę pomiędzy
(define (some-var args ...) expression expression ...)
co sprawia some-var
funkcję, która wykonuje wszystkie expressions
po nazwie, a
(define some-var expression)
, która wiąże some-var
z wartością expression
, ocenianą wtedy i tam. Ściśle mówiąc, byłego wersja jest niepotrzebne, ponieważ jest to równoznaczne z
(define some-var
(lambda (args ...) expression expression ...))
kod jest prawie taki sam, jak ten, z dodatkiem zawężona leksykalnie zmiennej, num
wokół postaci lambda
.
Wreszcie, tutaj jest kluczowa różnica między zmiennymi zamkniętymi a statycznymi, co sprawia, że zamknięcia są znacznie potężniejsze.Gdybyś napisał następujące zamiast:
(define make-next-num
(lambda (num)
(lambda() (set! num (+ num 1)) num)))
wtedy każde wywołanie make-next-num
stworzyłoby anonimową funkcję z nowym, odrębnym num
zmiennej, która jest prywatny do tej funkcji:
(define f (make-next-num 7))
(define g (make-next-num 2))
(f) ; => 8
(g) ; => 3
(f) ; => 9
To jest naprawdę fajna i potężna sztuczka, która tłumaczy moc języków z leksykalnymi zamknięciami.
Edytowane w celu dodania: Pytasz, jak Schemat "wie", który num
modyfikuje po wywołaniu next-num
. W zarysie, jeśli nie w realizacji, jest to całkiem proste. Każde wyrażenie na Schemacie jest oceniane w kontekście środowiska (tabeli odnośników) zmiennych powiązań, które są skojarzeniami nazw z miejscami, w których można przechowywać wartości. Każda ocena formularza let
lub wywołania funkcji tworzy nowe środowisko, rozszerzając bieżące środowisko o nowe powiązania. Aby formularze miały postać zamknięć, implementacja reprezentuje je jako strukturę składającą się z samej funkcji oraz środowiska, w którym została zdefiniowana. Wywołania tej funkcji są następnie analizowane przez rozszerzenie środowiska powiązania, w którym zdefiniowano tę funkcję - , a nie środowiska, w którym została wywołana.
Starsze Lispy (w tym Emacs Lisp do niedawna) miały lambda
, ale nie zakres leksykalny, więc chociaż można było tworzyć funkcje anonimowe, połączenia z nimi byłyby oceniane w środowisku wywołującym, a nie w środowisku definicji, a więc nie było żadnych zamknięcia. Uważam, że Scheme był pierwszym językiem, który to poprawił. Oryginalna wersja Sussmana i Steele'a Lambda Papers dotycząca realizacji Schematu sprawia, że lektura rozwijająca umysł jest świetna dla każdego, kto chce zrozumieć scoping i wiele innych rzeczy.