Х. Абельсон, Дж. Дж. Сассман, Дж. Сассман - Структура и интерпретация компьютерных программ (1108516), страница 56
Текст из файла (страница 56)
3.7. Результат вычисления (define W1 (make-withdraw 100)).Глава 3. Модульность, объекты и состояние236make-withdraw:...глобальноеокружение W1:E1balance: 100Баланс, который будетизменен операцией.set!.amount: 50параметры: amount(if (>= balance amount)(begin(set! (balance(- balance amount))balance"Недостаточно денег на счете"))Рис.
3.8. Окружения, создаваемые при применении процедурного объекта W1.Для начала мы конструируем кадр, в котором amount, формальный параметр W1, связывается со значением 50. Здесь крайне важно заметить, что у этого кадра в качествеобъемлющего окружения выступает не глобальное окружение, а E1, поскольку именнона него указывает процедурный объект W1.
В этом новом окружении мы вычисляем телопроцедуры:(if (>= balance amount)(begin (set! balance (- balance amount))balance)"Недостаточно денег на счете")Получается структура окружений, изображенная на рисунке 3.8. Вычисляемое выражение обращается к переменным amount и balance. Amount находится в первом кадреокружения, а balance мы найдем, проследовав по указателю на объемлющее окружение E1.Когда выполняется set!, связывание переменной balance в E1 изменяется. Послезавершения вызова W1 значение balance равно 50, а W1 по-прежнему указывает накадр, который содержит переменную balance. Кадр, содержащий amount (тот, в котором мы выполняли код, изменяющий balance), больше не нужен, поскольку создавшийего вызов процедуры закончен, и никаких указателей на этот кадр из других частейокружения нет. В следующий раз, когда мы позовем W1, создастся новый кадр, в котором будет связана переменная amount, и для которого объемлющим окружением сновабудет E1.
Мы видим, что E1 служит «местом», в котором хранится локальная переменная окружения для процедурного объекта W1. На рисунке 3.9 изображена ситуация3.2. Модель вычислений с окружениями237глобальное make-withdraw:...окружение W1:E1balance:50параметры: amountтело: ...Рис. 3.9. Окружения после вызова W1.после вызова W1.Рассмотрим, что произойдет, когда мы создадим другой объект для «снятия денег»,вызвав make-withdraw второй раз:(define W2 (make-withdraw 100))При этом получается структура окружений, изображенная на рисунке 3.10. Мы видим,что W2 — процедурный объект, то есть пара, содержащая код и окружение.
ОкружениеE2 для W2 было создано во время вызова make-withdraw. Оно содержит кадр со своимсобственным связыванием переменной balance. С другой стороны, код у W1 и W2 одини тот же: это код, определяемый lambda-выражением в теле make-withdraw15. Отсюдамы видим, почему W1 и W2 ведут себя как независимые объекты. Вызовы W1 работаютс переменной состояния balance, которая хранится в E1, а вызовы W2 с переменнойbalance, хранящейся в E2. Таким образом, изменения внутреннего состояния одногообъекта не действуют на другой.Упражнение 3.10.В процедуре make-withdraw локальная переменная balance создается в виде параметра makewithdraw.
Можно было бы создать локальную переменную и явно, используя let, а именно:(define (make-withdraw initial-amount)(let ((balance initial-amount))(lambda (amount)(if (>= balance amount)(begin (set! balance (- balance amount))balance)"Недостаточно денег на счете"))))15 Разделяют ли W1 и W2 общий физический код, хранимый в компьютере, или каждый из них хранитсобственную копию кода — это деталь реализации.
В интерпретаторе, который мы создадим в главе 4, кодбудет общим.Глава 3. Модульность, объекты и состояние238make-withdraw:...глобальноеW2:окружениеW1:E1balance: 50E2balance: 100параметры: amountтело: ...Рис. 3.10. Создание второго объекта при помощи (define W2 (make-withdraw100))Напомним, что в разделе 1.3.2 говорится, что let всего лишь синтаксический сахар для вызовапроцедуры:(let ((hперi hвырi))hтелоi)интерпретируется как альтернативный синтаксис для((lambda (hперi) hтелоi) hвырi)С помощью модели с окружениями проанализируйте альтернативную версию makewithraw. Нарисуйте картинки, подобные приведенным в этом разделе, для выражений(define W1 (make-withdraw 100))(W1 50)(define W2 (make-withdraw 100))Покажите, что две версии make-withdraw создают объекты с одинаковым поведением. Как различаются структуры окружений в двух версиях?3.2.4.
Внутренние определенияВ разделе 1.1.8 мы познакомились с идеей, что процедуры могут содержать внутренние определения, в результате чего возникает блочная структура, как, например, вследующей процедуре вычисления квадратного корня:(define (sqrt x)(define (good-enough? guess)(< (abs (- (square guess) x)) 0.001))3.2. Модель вычислений с окружениямиглобальноеокружение239sqrt:x:2good-enough?:improve:...sqrt-iter:...E1параметры: xтело: (define good-enough? ...)(define improve ...)(define sqrt-iter ...)(sqrt-iter 1.0)guess:1E2вызов sqrt-iterE3параметры: guessтело: (< (abs ...)...)guess:1вызов good-enough?Рис.
3.11. Процедура sqrt с внутренними определениями.(define (improve guess)(average guess (/ x guess)))(define (sqrt-iter guess)(if (good-enough? guess)guess(sqrt-iter (improve guess))))(sqrt-iter 1.0))Теперь с помощью модели с окружениями мы можем увидеть, почему эти внутренниеопределения работают так, как должны.
На рисунке 3.11 изображен момент во время вычисления выражения (sqrt 2), когда внутренняя процедура good-enough? вызванав первый раз со значением guess, равным 1.Рассмотрим структуру окружения. Символ sqrt в глобальном окружении связан спроцедурным объектом, ассоциированное окружение которого — глобальное окружение.Когда мы вызвали процедуру sqrt, появилось окружение E1, зависимое от глобального,в котором параметр x связан со значением 2. Затем мы вычислили тело sqrt внутриE1. Поскольку первое выражение в теле sqrt есть(define (good-enough? guess)(< (abs (- (square guess) x)) 0.001))вычисление этого выражения привело к определению процедуры good-enough? в окружении E1.
Выражаясь более точно, к первому кадру E1 был добавлен символ goodenough?, связанный с процедурным объектом, ассоциированным окружением которого является E1. Подобным образом в качестве процедур внутри E1 были определены240Глава 3. Модульность, объекты и состояниеimprove и sqrt-iter. Краткости ради на рис. 3.11 показан только процедурный объект, соответствующий good-enough?.После того, как были определены внутренние процедуры, мы вычислили выражение(sqrt-iter 1.0), по-прежнему в окружении E1. То есть, процедурный объект, связанный в E1 с именем sqrt-iter, был вызван с аргументом 1.
При этом появилосьокружение E2, в котором guess, параметр sqrt-iter, связан со значением 1. В своюочередь, sqrt-iter вызвала good-enough? со значением guess (из E2) в качествеаргумента. Получилось еще одно окружение, E3, в котором guess (параметр goodenough?) связан со значением 1. Несмотря на то, что и sqrt-iter, и good-enough?имеют по параметру с одинаковым именем guess, это две различные переменные, расположенные в разных кадрах. Кроме того, и E2, и E3 в качестве объемлющего окруженияимеют E1, поскольку как sqrt-iter, так и good-enough? в качестве окружения содержат указатель на E1.
Одним из следствий этого является то, что символ x в телеgood-enough? обозначает связывание x, в окружении E1, а точнее, то значение x, скоторым была вызвана исходная процедура sqrt.Таким образом, модель вычислений с окружениями объясняет две ключевых особенности, которые делают внутренние определения процедур полезным способом модуляризации программ:• Имена внутренних процедур не путаются с именами, внешними по отношению кохватывающей процедуре, поскольку локальные имена процедур будут связываться вкадре, который процедура создает при своем запуске, а не в глобальном окружении.• Внутренние процедуры могут обращаться к аргументам охватывающих процедур,просто используя имена параметров как свободные переменные. Это происходит потому,что тело внутренней процедуры выполняется в окружении, подчиненном окружению, гдевычисляется объемлющая процедура.Упражнение 3.11.В разделе 3.2.3 мы видели, как модель с окружениями описывает поведение процедур, обладающих внутренним состоянием.
Теперь мы рассмотрели, как работают локальные определения.Типичная процедура с передачей сообщений пользуется и тем, и другим. Рассмотрим процедурумоделирования банковского счета из раздела 3.1.1:(define (make-account balance)(define (withdraw amount)(if (>= balance amount)(begin (set! balance (- balance amount))balance)"Недостаточно денег на счете"))(define (deposit amount)(set! balance (+ balance amount))balance)(define (dispatch m)(cond ((eq? m ’withdraw) withdraw)((eq? m ’deposit) deposit)(else (error "Неизвестный вызов -- MAKE-ACCOUNT"m))))dispatch)3.3.
Моделирование при помощи изменяемых данных241Покажите, какая структура окружений создается последовательностью действий(define acc (make-account 50))((acc ’deposit) 40)90((acc ’withdraw) 60)30Где хранится внутреннее состояние acc? Предположим, что мы определяем еще один счет(define acc2 (make-account 100))Каким образом удается не смешивать внутренние состояния двух счетов? Какие части структурыокружений общие у acc и acc2?3.3.
Моделирование при помощи изменяемых данныхВ главе 2 составные данные использовались как средство построения вычислительных объектов, состоящих из нескольких частей, с целью моделирования объектов реального мира, обладающих несколькими свойствами. В этой главе мы ввели дисциплину абстракции данных, согласно которой структуры данных описываются в терминахконструкторов, которые создают объекты данных, и селекторов, которые обеспечиваютдоступ к частям составных объектов. Однако теперь мы знаем, что есть еще один аспектработы с данными, который остался незатронутым в главе 2.