Вызовите один счётчик дважды
Сохраните всю программу ниже в counter.lspx. Сначала установите Native CLI или запускайте каждый пример отдельно в песочнице.
(define counter (let ((n 0)) (lambda () (set! n (+ n 1)) n)))
(list (counter) (counter))lispex run counter.lspxВывод
(1 2)Команда завершается с кодом 0 и не создаёт файлов.
let задаёт для n начальное значение 0. lambda создаёт процедуру, которая прибавляет к n единицу и возвращает новое значение. counter — имя этой процедуры.
Первый вызов меняет n на 1, второй — на 2. Процедура может использовать n и после завершения let. Процедура, сохраняющая такой доступ к именам из окружающего кода, называется замыканием.
Дайте каждому счётчику своё значение
Сохраните следующую программу в separate-counters.lspx.
(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))lispex run separate-counters.lspxВывод
(1 1 2 2)Команда завершается с кодом 0 и не создаёт файлов.
Каждый вызов make-counter снова выполняет let и создаёт отдельную n. Вызов counter-a меняет только его собственное значение. counter-b тоже начинает с 0, но увеличивает своё значение независимо.
Дайте двум счётчикам одно общее значение
Сохраните следующую программу в shared-counters.lspx.
(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))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 повторно.
Куда дальше
При запуске с начала эти программы дают одинаковые значения при одинаковом порядке вызовов и одинаковых условиях выполнения. Если состояние правила можно передать на вход, продолжите с руководством по правилам решений.