Это «лексическое замыкание», и вы правы, что num
, «закрытая переменная» похожа на статическую переменную, например в C: она видна только коду в форме let
( это «лексическая область действия»), но она сохраняется на протяжении всего выполнения программы, а не переинициализируется при каждом вызове функции.
Я думаю, что часть, в которой вы запутались, такова: «num
создается с помощью let inside next-num
, это своего рода локальная переменная». Это не так, потому что блок let
не является частью функции next-num
: на самом деле это выражение, которое создает и возвращает функцию, которая затем связывается с next-num
. (Это очень отличается, например, от C, где функции могут быть созданы только во время компиляции и путем определения их на верхнем уровне. В Scheme функции - это значения, такие как целые числа или списки, которые может возвращать любое выражение).
Вот еще один способ написать (почти) ту же вещь, которая проясняет, что define
просто связывает next-num
со значением выражения, возвращающего функцию:
(define next-num #f) ; dummy value
(let ((num 0))
(set! next-num
(lambda () (set! num (+ num 1)) num)))
Важно отметить разницу между
(define (some-var args ...) expression expression ...)
, которая делает some-var
функцией, которая выполняет все expressions
при вызове, и
(define some-var expression)
, который связывает some-var
со значением expression
, оцененным тут же. Строго говоря, предыдущая версия не нужна, потому что она эквивалентна
(define some-var
(lambda (args ...) expression expression ...))
Ваш код почти такой же, как этот, с добавлением переменной лексической области num
вокруг формы lambda
.
Наконец, вот ключевое отличие между замкнутыми и статическими переменными, что делает замыкания намного более мощными. Если вы написали следующее:
(define make-next-num
(lambda (num)
(lambda () (set! num (+ num 1)) num)))
тогда каждый вызов make-next-num
будет создавать анонимную функцию с новой, отдельной переменной num
, которая является частной для этой функции:
(define f (make-next-num 7))
(define g (make-next-num 2))
(f) ; => 8
(g) ; => 3
(f) ; => 9
Это действительно крутой и мощный трюк, который объясняет большую силу языков с лексическими замыканиями.
Отредактировано, чтобы добавить: Вы спрашиваете, как Схема «знает», какую num
изменить, когда вызывается next-num
. В общих чертах, если не в реализации, это на самом деле довольно просто. Каждое выражение в Scheme оценивается в контексте среды (таблицы поиска) привязок переменных, которые являются ассоциациями имен с местами, которые могут содержать значения. Каждая оценка формы let
или вызова функции создает новую среду, расширяя текущую среду новыми привязками. Чтобы формы lambda
вели себя как замыкания, реализация представляет их как структуру, состоящую из самой функции и среды, в которой она была определена. Вызовы этой функции затем оцениваются путем расширения среды привязки, в которой была определена функция - , а не среды, в которой она была вызвана.
Более ранние Лиспы (включая Emacs Lisp до недавнего времени) имели lambda
, но не лексическую область, поэтому, хотя вы могли создавать анонимные функции, вызовы к ним будут оцениваться в среде вызова, а не в среде определения, и поэтому нет закрытий. Я считаю, что Схема была первым языком, который понял это правильно. Оригинальные лямбда-бумаги от Sussman и Steele, посвященные реализации Схемы, являются отличным чтением, расширяющим кругозор, для всех, кто хочет понять область видимости, среди многих других вещей.