Х. Абельсон, Дж. Дж. Сассман, Дж. Сассман - Структура и интерпретация компьютерных программ (1108516), страница 51
Текст из файла (страница 51)
Воспользуемся для этого процедурой withdraw, которая в качествеаргумента принимает сумму, которую требуется снять. Если на счету имеется достаточно средств, чтобы осуществить операцию, то withdraw возвращает баланс, остающийсяпосле снятия. В противном случае withdraw возвращает сообщение «Недостаточно денег на счете». Например, если вначале на счету содержится 100 долларов, мы получимследующую последовательность результатов:(withdraw 25)75(withdraw 25)50(withdraw 60)"Недостаточно денег на счете"(withdraw 15)35Обратите внимание, что выражение (withdraw 25), будучи вычислено дважды, дает различные результаты.
Это новый тип поведения для процедуры. До сих пор все нашипроцедуры можно было рассматривать как описания способов вычисления математических функций. Вызов процедуры вычислял значение функции для данных аргументов, идва вызова одной и той же процедуры с одинаковыми аргументами всегда приводили кодинаковому результату1 .При реализации withdraw мы используем переменную balance, которая показывает остаток денег на счете, и определяем withdraw в виде процедуры, которая обращается к этой переменной.
Процедура withdraw проверяет, что значение balance неменьше, чем значение аргумента amount. Если это так, withdraw уменьшает значение balance на amount и возвращает новое значение balance. В противном случаеона возвращает сообщение «Недостаточно денег на счете». Вот определения balance иwithdraw:(define balance 100)(define (withdraw amount)(if (>= balance amount)(begin (set! balance (- balance amount))balance)"Недостаточно денег на счете"))1 На самом деле это не совсем правда. Одно исключение — генератор случайных чисел из раздела 1.2.6.Второе связано с таблицами операций и типов, которые мы ввели в разделе 2.4.3, где значения двух вызововget с одними и теми же аргументами зависели от того, какие были в промежутке между ними вызовы put. Сдругой стороны, пока мы не ввели присваивание, мы лишены возможности самим создавать такие процедуры.214Глава 3. Модульность, объекты и состояниеЗначение переменной balance уменьшается, когда мы выполняем выражение(set! balance (- balance amount))Здесь используется особая форма set!, синтаксис которой выглядит так:(set! hимяi hновое-значениеi)Здесь hимяi — символ, а hновое-значениеi – произвольное выражение.
Set! заменяет значение hимениi на результат, полученный при вычислении hнового-значенияi. Вданном случае, мы изменяем balance так, что его новое значение будет результатомвычитания amount из предыдущего значения balance2 .Кроме того, withdraw использует особую форму begin, когда проверка if выдаетистину, и требуется вычислить два выражения: сначала уменьшить balance, а затемвернуть его значение. В общем случае вычисление выражения(begin hвыражение1i hвыражение2i ... hвыражениеk i)приводит к последовательному вычислению выражений от hвыражения1i до hвыраженияki,и значение последнего выражения hвыражениеk i возвращается в качестве значения всейформы begin3 .Хотя процедура withdraw и работает так, как мы того хотели, переменная balanceпредставляет собой проблему.
Balance, как она описана выше, является переменной,определенной в глобальном окружении, и любая процедура может прочитать или изменить ее значение. Намного лучше было бы, если бы balance можно было сделатьвнутренней переменной для withdraw, так, чтобы только withdraw имела доступ кней напрямую, а любая другая процедура — только посредством вызовов withdraw.Так можно будет более точно смоделировать представление о balance как о внутренней переменной состояния, с помощью которой withdraw следит за состоянием счета.Сделать balance внутренней по отношению к withdraw мы можем, переписав определение следующим образом:(define new-withdraw(let ((balance 100))(lambda (amount)(if (>= balance amount)(begin (set! balance (- balance amount))balance)"Недостаточно денег на счете"))))Здесь мы, используя let, создаем окружение с внутренней переменной balance, которой вначале присваивается значение 100. Внутри этого локального окружения мы при2 Значение выражения set! зависит от реализации.
Set! нужно использовать только ради эффекта, который оно оказывает, а не ради значения, которое оно возвращает.Имя set! отражает соглашение, принятое в Scheme: операциям, которые изменяют значения переменных(или структуры данных, как мы увидим в разделе 3.3) даются имена с восклицательным знаком на конце.
Этонапоминает соглашение называть предикаты именами, которые оканчиваются на вопросительный знак.3 Неявно мы уже использовали в своих программах begin, поскольку в Scheme тело процедуры может бытьпоследовательностью выражений. Кроме того, в каждом подвыражении cond следствие может состоять не изодного выражения, а из нескольких.3.1. Присваивание и внутреннее состояние объектов215помощи lambda определяем процедуру, которая берет в качестве аргумента amount идействует так же, как наша старая процедура withdraw.
Эта процедура — возвращаемая как результат выражения let, — и есть new-withdraw. Она ведет себя в точноститак же, как, как withdraw, но ее переменная balance недоступна для всех остальныхпроцедур4 .Set! в сочетании с локальными переменными — общая стратегия программирования,которую мы будем использовать для построения вычислительных объектов, обладающихвнутренним состоянием. К сожалению, при использовании этой стратегии возникаетсерьезная проблема: когда мы только вводили понятие процедуры, мы ввели также подстановочную модель вычислений (раздел 1.1.5) для того, чтобы объяснить, что означаетприменение процедуры к аргументам. Мы сказали, что оно должно интерпретироватьсякак вычисление тела процедуры, в котором формальные параметры заменяются на своизначения.
К сожалению, как только мы вводим в язык присваивание, подстановка перестает быть адекватной моделью применения процедуры. (Почему это так, мы поймем вразделе 3.1.3.) В результате, с технической точки зрения мы сейчас не умеем объяснить,почему процедура new-withdraw ведет себя именно так, как описано выше. Чтобыдействительно понять процедуры, подобные new-withdraw, нам придется разработатьновую модель применения процедуры. В разделе 3.2 мы введем такую модель, попутно объяснив set! и локальные переменные. Однако сначала мы рассмотрим некоторыевариации на тему, заданную new-withdraw.Следующая процедура, make-withdraw, создает «обработчики снятия денег со счетов». Формальный параметр balance, передаваемый в make-withdraw, указывает начальную сумму денег на счету5 .(define (make-withdraw balance)(lambda (amount)(if (>= balance amount)(begin (set! balance (- balance amount))balance)"Недостаточно денег на счете")))При помощи make-withdraw можно следующим образом создать два объекта W1 и W2:(define W1 (make-withdraw 100))(define W2 (make-withdraw 100))(W1 50)50(W2 70)304 По терминологии, принятой при описании языков программирования, переменная balance инкапсулируется (is encapsulated) внутри процедуры new-withdraw.
Инкапсуляция отражает общий принцип проектирования систем, известный как принцип сокрытия (the hiding principle): систему можно сделать более модульнойи надежной, если защищать ее части друг от друга; то есть, разрешать доступ к информации только тем частямсистемы, которым «необходимо это знать».5 В отличие от предыдущей процедуры new-withdraw, здесь нам необязательно использовать let, чтобысделать balance локальной переменной, поскольку формальные параметры и так локальны.
Это станет яснеепосле обсуждения модели вычисления с окружениями в разделе 3.2. (См. также упражнение 3.10.)216Глава 3. Модульность, объекты и состояние(W2 40)"Недостаточно денег на счете"(W1 40)10Обратите внимание, что W1 и W2 — полностью независимые объекты, каждый со своейлокальной переменной balance. Снятие денег с одного счета не влияет на другой.Мы можем создавать объекты, которые будут разрешать не только снятие денег, но иих занесение на счет, и таким образом можно смоделировать простые банковские счета.Вот процедура, которая возвращает объект-«банковский счет» с указанным начальнымбалансом:(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)Каждый вызов make-account создает окружение с локальной переменной состоянияbalance.
Внутри этого окружения make-account определяет процедуры depositи withdraw, которые обращаются к balance, а также дополнительную процедуруdispatch, которая принимает «сообщение» в качестве ввода, и возвращает одну издвух локальных процедур. Сама процедура dispatch возвращается как значение, которое представляет объект-банковский счет. Это не что иное, как стиль программированияс передачей сообщений (message passing), который мы видели в разделе 2.4.3, но толькоздесь мы его используем в сочетании с возможностью изменять локальные переменные.Make-account можно использовать следующим образом:(define acc (make-account 100))((acc ’withdraw) 50)50((acc ’withdraw) 60)"Недостаточно денег на счете"((acc ’deposit) 40)3.1.
Присваивание и внутреннее состояние объектов21790((acc ’withdraw) 60)30Каждый вызов acc возвращает локально определенную процедуру deposit или withdraw,которая затем применяется к указанной сумме. Точно так же, как это было с makewithdraw, второй вызов make-account(define acc2 (make-account 100))создает совершенно отдельный объект-счет, который поддерживает свою собственнуюпеременную balance.Упражнение 3.1.Накопитель (accumulator) — это процедура, которая вызывается с одним численным аргументоми собирает свои аргументы в сумму.
При каждом вызове накопитель возвращает сумму, которуюуспел накопить. Напишите процедуру make-accumulator, порождающую накопители, каждыйиз которых поддерживает свою отдельную сумму. Входной параметр make-accumulator долженуказывать начальное значение суммы; например,(define A (make-accumulator 5))(A 10)15(A 10)25Упражнение 3.2.При тестировании программ удобно иметь возможность подсчитывать, сколько раз за время вычислений была вызвана та или иная процедура. Напишите процедуру make-monitored, принимающую в качестве параметра процедуру f, которая сама по себе принимает один входнойпараметр. Результат, возвращаемый make-monitored — третья процедура, назовем ее mf, которая подсчитывает, сколько раз она была вызвана, при помощи внутреннего счетчика. Если навходе mf получает специальный символ how-many-calls?, она возвращает значение счетчика.Если же на вход подается специальный символ reset-count, mf обнуляет счетчик. Для любогодругого параметра mf возвращает результат вызова f с этим параметром и увеличивает счетчик.Например, можно было бы сделать отслеживаемую версию процедуры sqrt:(define s (make-monitored sqrt))(s 100)10(s ’how-many-calls?)1Упражнение 3.3.Измените процедуру make-account так, чтобы она создавала счета, защищенные паролем.А именно, make-account должна в качестве дополнительного аргумента принимать символ,например218Глава 3.