Песочница

Замыкания, которые запоминают значения

Создайте два счётчика и выберите, будет ли каждый хранить своё значение или оба будут менять одно общее.

Вызовите один счётчик дважды

Сохраните всю программу ниже в counter.lspx. Сначала установите Native CLI или запускайте каждый пример отдельно в песочнице.

LISPEX
(define counter (let ((n 0)) (lambda () (set! n (+ n 1)) n)))
(list (counter) (counter))
SH
lispex run counter.lspx

Вывод

ВЫВОД
(1 2)

Команда завершается с кодом 0 и не создаёт файлов.

let задаёт для n начальное значение 0. lambda создаёт процедуру, которая прибавляет к n единицу и возвращает новое значение. counter — имя этой процедуры.

Первый вызов меняет n на 1, второй — на 2. Процедура может использовать n и после завершения let. Процедура, сохраняющая такой доступ к именам из окружающего кода, называется замыканием.

Дайте каждому счётчику своё значение

Сохраните следующую программу в separate-counters.lspx.

LISPEX
(define (make-counter)
  (let ((n 0))
    (lambda ()
      (set! n (+ n 1))
      n)))

(define counter-a (make-counter))
(define counter-b (make-counter))
(list (counter-a) (counter-b) (counter-a) (counter-b))
SH
lispex run separate-counters.lspx

Вывод

ВЫВОД
(1 1 2 2)

Команда завершается с кодом 0 и не создаёт файлов.

Каждый вызов make-counter снова выполняет let и создаёт отдельную n. Вызов counter-a меняет только его собственное значение. counter-b тоже начинает с 0, но увеличивает своё значение независимо.

Дайте двум счётчикам одно общее значение

Сохраните следующую программу в shared-counters.lspx.

LISPEX
(define counters
  (let ((n 0))
    (list (lambda () (set! n (+ n 1)) n)
          (lambda () (set! n (+ n 1)) n))))

(define counter-a (car counters))
(define counter-b (car (cdr counters)))
(list (counter-a) (counter-b) (counter-a) (counter-b))
SH
lispex run shared-counters.lspx

Вывод

ВЫВОД
(1 2 3 4)

Команда завершается с кодом 0 и не создаёт файлов.

Здесь let выполняется один раз. Он создаёт одну n и две процедуры, использующие её. car выбирает первую процедуру, а car после cdr — вторую.

Вызовы выполняются слева направо. counter-a меняет n на 1, затем counter-b меняет ту же n на 2. Следующие вызовы возвращают 3 и 4.

Что сохраняют замыкания

Связывание соединяет имя с местом хранения значения — ячейкой. Сохранение доступа к этому связыванию называется захватом. Замыкание захватывает ячейку и поэтому видит последующие изменения хранящегося в ней значения.

ПрограммаГде создаётся nРезультат чередующихся вызовов
Отдельные счётчикиновая n при каждом вызове make-counter(1 1 2 2)
Общие счётчикиодна n для обеих процедур в одном let(1 2 3 4)

В обеих программах после завершения let доступ к n дают возвращённые процедуры. Храните состояние рядом с процедурами, которым оно нужно. Меняя код, проверяйте последовательность вызовов, как в примерах выше.

Если внутреннее связывание скрывает внешнее имя, у каждого связывания своя ячейка. set! меняет ближайшее связывание, определяемое структурой исходного кода. Повторный define верхнего уровня обновляет исходную глобальную ячейку, поэтому ранее созданные замыкания видят новое значение. Оба случая показаны в главе о связываниях.

Проверьте себя

В separate-counters.lspx замените (define counter-b (make-counter)) на (define counter-b counter-a). Что вернёт последняя строка?

Ответ

Она вернёт (1 2 3 4). Оба имени теперь относятся к одной процедуре, которая меняет одну n. Новое имя для процедуры не вызывает make-counter повторно.

Куда дальше

При запуске с начала эти программы дают одинаковые значения при одинаковом порядке вызовов и одинаковых условиях выполнения. Если состояние правила можно передать на вход, продолжите с руководством по правилам решений.

Связывания и область · Записи решения