Х. Абельсон, Дж. Дж. Сассман, Дж. Сассман - Структура и интерпретация компьютерных программ (1108516), страница 53
Текст из файла (страница 53)
Этот аргумент должен быть либо символом generate, либо символом reset. Процедура работает так: (rand ’generate) порождает новое случайное число;((rand ’reset) hновое-значениеi) сбрасывает внутреннюю переменную состояния в указанное hновое-значениеi. Таким образом, сбрасывая значения, можно получать повторяющиесяпоследовательности. Эта возможность очень полезна при тестировании и отладке программ, использующих случайные числа.3.1.3. Издержки, связанные с введением присваиванияКак мы только что видели, операция set! позволяет моделировать объекты, обладающие внутренним состоянием. Однако за это преимущество приходится платить. Нашязык программирования нельзя больше описывать при помощи подстановочной модели8 В MIT Scheme есть такая процедура.
Если random на вход дается точное целое число (как в разделе 1.2.6),она возвращает точное целое число, но если ей дать десятичную дробь (как в этом примере), она и возвращаетдесятичную дробь.222Глава 3. Модульность, объекты и состояниеприменения процедур, которую мы ввели в разделе 1.1.5. Хуже того, не существуетпростой модели с «приятными» математическими свойствами, которая бы адекватно описывала работу с объектами и присваивание в языках программирования.Пока мы не применяем присваивание, два вычисления одной и той же процедуры содними и теми же аргументами всегда дают одинаковый результат.
Стало быть, можносчитать, что процедуры вычисляют математические функции. Соответственно, программирование, в котором присваивание не используется (как у нас в первых двух главахэтой книги), известно как функциональное программирование (functional programming).Чтобы понять, как присваивание усложняет ситуацию, рассмотрим упрощенную версию make-withdraw из раздела 3.1.1, которая не проверяет, достаточно ли на счетеденег:(define (make-simplified-withdraw balance)(lambda (amount)(set! balance (- balance amount))balance))(define W (make-simplified-withdraw 25))(W 20)5(W 10)-5Сравним эту процедуру со следующей процедурой make-decrementer, которая не использует set!:(define (make-decrementer balance)(lambda (amount)(- balance amount)))make-decrementer возвращает процедуру, которая вычитает свой аргумент из определенного числа balance, но при последовательных вызовах ее действие не накапливается, как при использовании make-simplified-withdraw:(define D (make-decrementer 25))(D 20)5(D 10)15Мы можем объяснить, как работает make-decrementer, при помощи подстановочноймодели.
Например, рассмотрим, как вычисляется выражение((make-decrementer 25) 20)Сначала мы упрощаем операторную часть комбинации, подставляя в теле make-decrementerвместо balance 25. Выражение сводится к3.1. Присваивание и внутреннее состояние объектов223((lambda (amount) (- 25 amount)) 20)Теперь мы применяем оператор к операнду, подставляя 20 вместо amount в телеlambda-выражения:(- 25 20)Окончательный результат равен 5.Посмотрим, однако, что произойдет, если мы попробуем применить подобный подстановочный анализ к make-simplified-withdraw:((make-simplified-withdraw 25) 20)Сначала мы упрощаем оператор, подставляя вместо balance 25 в теле makesimplified-withdraw. Таким образом, наше выражение сводится к9((lambda (amount) (set! balance (- 25 amount)) 25) 20)Теперь мы применяем оператор к операнду, подставляя в теле lambda-выражения 20вместо amount:(set! balance (- 25 20)) 25Если бы мы следовали подстановочной модели, нам пришлось бы сказать, что вычислениепроцедуры состоит в том, чтобы сначала присвоить переменной balance значение 5, азатем в качестве значения вернуть 25.
Но это дает неверный ответ. Чтобы получитьправильный ответ, нам пришлось бы как-то отличить первое вхождение balance (дотого, как сработает set!) от второго (после выполнения set!). Подстановочная модельна это не способна.Проблема здесь состоит в том, что подстановка предполагает, что символы в нашемязыке — просто имена для значений. Но как только мы вводим set! и представление,что значение переменной может изменяться, переменная уже не может быть всего лишьименем. Теперь переменная некоторым образом соответствует месту, в котором можетхраниться значение, и значение это может меняться.
В разделе 3.2 мы увидим, как внашей модели вычислений роль этого «места» играют окружения.Тождественность и изменениеПроблема, который здесь встает, глубже, чем просто поломка определенной моделивычислений. Как только мы вводим в наши вычислительные модели понятие изменения,многие другие понятия, которые до сих пор были ясны, становятся сомнительными.Рассмотрим вопрос, что значит, что две вещи суть «одно и то же».Допустим, мы два раза зовем make-decrementer с одним и тем же аргументом, иполучаем две процедуры:(define D1 (make-decrementer 25))(define D2 (make-decrementer 25))9 Мы не производим подстановку вхождения balance в выражение set!, поскольку hимяi в set! невычисляется.
Если бы мы провели подстановку, получилось бы (set! 25 (- 25 amount)), а это не имеетникакого смысла.224Глава 3. Модульность, объекты и состояниеЯвляются ли D1 и D2 одним и тем же объектом? Можно сказать, что да, посколькуD1 и D2 обладают одинаковым поведением — каждая из этих процедур вычитает свойаргумент из 25.
В сущности, в любом вычислении можно подставить D1 вместо D2, ирезультат не изменится.Напротив, рассмотрим два вызова make-simplified-withdraw:(define W1 (make-simplified-withdraw 25))(define W2 (make-simplified-withdraw 25))Являются ли W1 и W2 одним и тем же? Нет, конечно, потому что вызовы W1 и W2приводят к различным результатам, как показывает следующая последовательность вычислений:(W1 20)5(W1 20)-15(W2 20)5Хотя W1 и W2 «равны друг другу» в том смысле, что оба они созданы вычислением одного и того же выражения (make-simplified-withdraw 25), неверно, что в любомвыражении можно заменить W1 на W2, не повлияв при этом на результат его вычисления.Язык, соблюдающий правило, что в любом выражении «одинаковое можно подставить вместо одинакового», не меняя его значения, называется референциально прозрачным (referentially transparent).
Если мы включаем в свой компьютерный язык set!, егореференциальная прозрачность нарушается. Становится сложно определить, где можноупростить выражение, подставив вместо него равносильное. Следовательно, рассуждатьо программах, в которых используется присваивание, оказывается гораздо сложнее.С потерей референциальной прозрачности становится сложно формально описать понятие о том, что два объекта – один и тот же объект.
На самом деле, смысл выражения«то же самое» в реальном мире, который наши программы моделируют, сам по себенедостаточно ясен. В общем случае, мы можем проверить, являются ли два как будто бы одинаковых объекта одним и тем же, только изменяя один из них и наблюдая,изменился ли таким же образом и другой. Но как мы можем узнать, «изменился» лиобъект? Только рассмотрев один и тот же объект дважды и проверив, не различаетсяли некоторое его свойство между двумя наблюдениями. Таким образом, мы не можемопределить «изменение», не имея заранее понятия «идентичности», а идентичность мыне можем определить, не рассмотрев результаты изменений.В качестве примера того, как эти вопросы возникают в программировании, рассмотрим ситуацию, где у Петра и у Павла есть по банковскому счету в 100 долларов.
Здесьне все равно, смоделируем мы это через(define peter-acc (make-account 100))(define paul-acc (make-account 100))или3.1. Присваивание и внутреннее состояние объектов225(define peter-acc (make-account 100))(define paul-acc peter-acc)В первом случае, два счета различны. Действия, которые производит Петр, не меняютсчет Павла, и наоборот. Однако во втором случае мы сказали, что paul-acc — этота же самая вещь, что и peter-acc. Теперь у Петра и у Павла есть совместныйбанковский счет, и если Петр возьмет сколько-то с peter-acc, то у Павла на paul-accбудет меньше денег. При построении вычислительных моделей сходство между этимидвумя несовпадающими ситуациями может привести к путанице.
В частности, в случаес совместным счетом может особенно мешать то, что у одного объекта (банковскогосчета) есть два имени (peter-acc и paul-acc); если мы ищем в программе все места,где может меняться paul-acc, надо смотреть еще и где меняется peter-acc10 .В связи с этими замечаниями обратите внимание на то, что если бы Петр и Павелмогли только проверять свой платежный баланс, но не менять его, то вопрос «один ли уних счет?» не имел бы смысла.
В общем случае, если мы никогда не меняем объекты данных, то можно считать, что каждый объект представляет собой в точности совокупностьсвоих частей. Например, рациональное число определяется своим числителем и знаменателем. Однако при наличии изменений такой взгляд становится ошибочным, посколькутеперь у каждого объекта есть «индивидуальность», которая отличается от тех частей,из которых он состоит. Банковский счет останется «тем же самым» счетом, даже еслимы снимем с него часть денег; и наоборот, можно иметь два разных счета с одинаковымсостоянием. Такие сложности — следствие не нашего языка программирования, а нашеговосприятия банковского счета как объекта.