КулЛиб электронная библиотека 

Как проектировать программы [Маттиас Фелляйзен] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


Настройки текста:



RESCUER

Маттиас Фелляйзен
Роберт Брюс Финдлер
Мэтью Флэтт
Шрирам Кришнамурти

Как проектировать
программы

How to Design Programs
An Introduction to Programming and
Computing
Second Edition

Matthias Felleisen
Robert Bruce Findler
Matthew Flatt
Shriram Krishnamurthi

The MIT Press
Cambridge, Massachusetts
London, England

Как проектировать программы
Введение в программирование
и компьютерные вычисления

Маттиас Фелляйзен
Роберт Брюс Финдлер
Мэтью Флэтт
Шрирам Кришнамурти

Москва, 2022

УДК 004.2
ББК 32.97
Ф37

Фелляйзен М., Финдлер Р. Б., Флэтт М., Кришнамурти Ш.
Ф37 Как проектировать программы / пер. с англ. А. Н. Киселева; под ред.
П. Б. Иванова, А. Д. Чичигина, Ю. А. Сыровецкого, С. В. Бронникова. – М.:
ДМК Пресс, 2022. – 724 с.: ил.
ISBN 978-5-97060-926-2
Эта книга повествует о методах «хорошего программирования» – то есть о таком
подходе к созданию программного обеспечения, который опирается на системное
мышление, планирование и понимание задач разработчика на каждом этапе.
В числе рассматриваемых тем – фундаментальные понятия систематического
проектирования, типы данных, способы записи объемных данных, создание
и использование абстракций, тестирование программ и функций и др.
Издание адресовано профессионалам и энтузиастам программирования, не
имеющим прежнего опыта систематического проектирования программ, а также
преподавателям технических вузов, которые могут использовать представленный
материал в рамках учебного курса.

УДК 004.2
ББК 32.97

The rights to the russian launguage edition obtained thougth Alxander Korgzhenevski Agency
(Moscow). All rights reserved.
Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения
владельцев авторских прав.

ISBN 978-0-26253-480-2 (англ.)
ISBN 978-5-97060-926-2 (рус.)

© Massachusetts Institute of Technology, 2018
Illustrations © Torrey Butzer, 2000
© Перевод, оформление, издание, ДМК Пресс, 2022

Содержание
От редакторов..................................................................................................................... 10
От издательства.................................................................................................................. 11
Вступление.......................................................................................................................... 12
Пролог: как писать программы........................................................................................ 29

I

Д АННЫЕ ФИКСИРОВАННОГО РА ЗМЕРА .........................................55

1

Арифметика. ............................................................................................................... 56
1.1. Арифметика чисел................................................................................................ 57
1.2. Арифметика строк................................................................................................ 59
1.3. А теперь все смешаем........................................................................................... 61
1.4. Арифметика изображений................................................................................... 63
1.5. Арифметика логических значений...................................................................... 66
1.6. Смешанные операции с логическими значениями........................................... 67
1.7. Предикаты: знай свои данные.............................................................................. 69

2

Функции и программы. ............................................................................................. 72
2.1. Функции................................................................................................................. 72
2.2. Вычисления........................................................................................................... 76
2.3. Композиция функций........................................................................................... 80
2.4. Глобальные константы.......................................................................................... 83
2.5. Программы............................................................................................................ 85

3

Как проектировать программы................................................................................ 98
3.1. Проектирование функций.................................................................................... 99
3.2. Практические упражнения: функции.................................................................106
3.3. Знание предметной области...............................................................................106
3.4. От функций к программам..................................................................................107
3.5. О тестировании....................................................................................................108
3.6. Проектирование интерактивных программ......................................................110
3.7. Миры виртуальных питомцев.............................................................................120

4

Интервалы, перечисления и детализация.............................................................122
4.1. Программирование с условиями........................................................................122
4.2. Условные вычисления..........................................................................................124
4.3. Перечисления.......................................................................................................127
4.4. Интервалы............................................................................................................131
4.5. Детализация.........................................................................................................135
4.6. Проектирование с использованием детализации.............................................143
4.7. Миры с конечными состояниями........................................................................146

5

Добавляем структуру................................................................................................154
5.1. От позиций к структурам posn............................................................................154
5.2. Вычисления со структурами posn.......................................................................155
5.3. Программирование с posn...................................................................................156
5.4. Определение структурных типов........................................................................158
5.5. Вычисления со структурами................................................................................163
5.6. Программирование со структурами...................................................................167
5.7. Вселенная данных................................................................................................174

6

Содержание
5.8. Проектирование с использованием структур....................................................178
5.9. Структура в мире..................................................................................................181
5.10. Графический редактор.......................................................................................182
5.11. Больше виртуальных питомцев........................................................................184

6

Структуры и детализация.........................................................................................187
6.1. Проектирование с использованием детализации, снова..................................187
6.2. Смешивание миров..............................................................................................200
6.3. Ошибки ввода.......................................................................................................203
6.4. Проверка состояния мира...................................................................................207
6.5. Предикаты равенства...........................................................................................209

7

Итоги. ..........................................................................................................................211

Интермеццо 1. Язык для начинающих студентов........................................................212
Словарь BSL.................................................................................................................212
Грамматика BSL..........................................................................................................213
Значение в языке BSL.................................................................................................217
Значения и вычисления.............................................................................................220
Ошибки в BSL..............................................................................................................220
Логические выражения..............................................................................................223
Определения констант...............................................................................................224
Определения структур................................................................................................226
Тесты в BSL..................................................................................................................228
Сообщения об ошибках в BSL....................................................................................229

II Д АННЫЕ ПРОИЗВОЛЬНОГО РА ЗМЕРА . ..........................................237
8

Списки.........................................................................................................................238
8.1. Создание списков.................................................................................................238
8.2. Что такое '(), что такое cons.................................................................................243
8.3. Программирование со списками........................................................................245
8.4. Вычисления со списками.....................................................................................249

9

Проектирование с определениями данных, ссылающимися
на самих себя.............................................................................................................251
9.1. Практические упражнения: списки....................................................................258
9.2. Непустые списки..................................................................................................260
9.3. Натуральные числа..............................................................................................266
9.4. Русская матрешка.................................................................................................270
9.5. Списки в интерактивных программах...............................................................274
9.6. Замечания о списках и множествах....................................................................279

10 Еще о списках............................................................................................................284
10.1. Функции, создающие списки............................................................................284
10.2. Структуры в списках..........................................................................................287
10.3. Списки в списках, файлы...................................................................................291
10.4. И снова о графическом редакторе....................................................................300

11 Проектирование методом композиции.................................................................312
11.1. Функция list........................................................................................................312
11.2. Композиция функций........................................................................................314

Содержание

7

11.3. Повторяющиеся вспомогательные функции...................................................316
11.4. Обобщающие вспомогательные функции.......................................................323

12 Проекты: списки........................................................................................................333
12.1. Реальные данные: словари................................................................................333
12.2. Реальные данные: iTunes...................................................................................335
12.3. Игры со словами, иллюстрация приема композиции.....................................340
12.4. Игры со словами, суть проблемы......................................................................345
12.5. «Питон»...............................................................................................................347
12.6. Простой «Тетрис»...............................................................................................350
12.7. Полная игра «Космические захватчики»..........................................................353
12.8. Конечные автоматы...........................................................................................354

13 Итоги. ..........................................................................................................................362
Интермеццо 2. Quote, unquote........................................................................................364
Цитирование...............................................................................................................364
Квазицитирование и антицитирование...................................................................365
Объединение с антицитированием...........................................................................370

III АБСТРАКЦИИ .....................................................................................................375
14 Сходства повсюду.....................................................................................................376
14.1. Сходства в функциях..........................................................................................376
14.2. Отличающиеся сходства....................................................................................378
14.3. Сходства в определениях данных.....................................................................381
14.4. Функции – это значения....................................................................................384
14.5. Вычисления с функциями.................................................................................385

15 Проектирование абстракций...................................................................................389
15.1. Абстрагирование примеров..............................................................................389
15.2. Сходства в сигнатурах........................................................................................394
15.3. Единая точка управления .................................................................................399
15.4. Абстрагирование макетов.................................................................................400

16 Использование абстракций. ....................................................................................402
16.1. Имеющиеся абстракции....................................................................................403
16.2. Локальные определения....................................................................................405
16.3. Локальные определения добавляют выразительности...................................409
16.4. Вычисления с локальными определениями.....................................................411
16.5. Использование абстракций на примерах.........................................................415
16.6. Проектирование с использованием абстракций.............................................420
16.7. Практические упражнения: абстракция...........................................................422
16.8. Проекты: абстракция.........................................................................................423

17 Безымянные функции. .............................................................................................426
17.1. Определение функций с по­мощью лямбда-выражений.................................427
17.2. Вычисления с лямбда-выражениями................................................................429
17.3. Абстрагирование с по­мощью лямбда-выражений...........................................432
17.4. Определение спецификаций с по­мощью лямбда-выражений.......................435
17.5. Представление с по­мощью лямбда-выражений..............................................442

18 Итоги. ..........................................................................................................................447

8

Содержание

Интермеццо 3. Область видимости и абстракции. ......................................................448
Область видимости.....................................................................................................448
Циклы в языке ISL.......................................................................................................453
Сопоставление с образцом.........................................................................................461

IV ПЕРЕПЛЕТАЮЩИЕС Я Д АННЫЕ . .......................................................... 468
19 Поэзия S-выражений................................................................................................469
19.1. Деревья................................................................................................................469
19.2. Леса......................................................................................................................477
19.3. S-выражения.......................................................................................................479
19.4. Проектирование с использованием взаимосвязанных данных.....................485
19.5. Проект: BST.........................................................................................................487
19.6. Упрощение функций..........................................................................................491

20 Итеративное уточнение............................................................................................494
20.1. Анализ данных...................................................................................................494
20.2. Уточнение определений данных.......................................................................496
20.3. Уточнение функций...........................................................................................498

21 Уточнение интерпретатора......................................................................................501
21.1. Интерпретация выражений...............................................................................501
21.2. Интерпретация переменных.............................................................................504
21.3. Интерпретация функций...................................................................................507
21.4. Интерпретация всего и вся................................................................................509

22 Проект: обработка XML............................................................................................512
22.1. XML как S-выражения........................................................................................512
22.2. Отображение XML-перечислений.....................................................................518
22.3. Предметно-ориентированные языки...............................................................523
22.4. Чтение XML.........................................................................................................528

23 Одновременная обработка......................................................................................533
23.1. Одновременная обработка двух списков: случай 1.........................................533
23.2. Одновременная обработка двух списков: случай 2.........................................534
23.3. Одновременная обработка двух списков: случай 3.........................................537
23.4. Упрощение функций..........................................................................................541
23.5. Проектирование функций с двумя сложными аргументами..........................542
23.6. Практические упражнения: два аргумента......................................................544
23.7. Проект: база данных...........................................................................................548

24 Итоги. ..........................................................................................................................560
Интермеццо 4. Природа чисел........................................................................................561
Арифметика с числами фиксированного размера...................................................561
Переполнение.............................................................................................................567
Потеря значимости.....................................................................................................567
Числа в *SL...................................................................................................................568

V

ГЕНЕРАТИВНА Я РЕК УРСИЯ .....................................................................574

25 Нестандартная рекурсия..........................................................................................575
25.1. Рекурсия без структуры.....................................................................................575

Содержание

9

25.2. Рекурсия, игнорирующая структуру.................................................................579

26 Проектирование алгоритмов...................................................................................585
26.1. Адаптация рецепта проектирования................................................................585
26.2. Завершимость рекурсии....................................................................................587
26.3. Структурная и генеративная рекурсии.............................................................590
26.4. Выбор..................................................................................................................591

27 Вариации на тему......................................................................................................597
27.1. Фракталы, первое знакомство...........................................................................597
27.2. Бинарный поиск.................................................................................................600
27.3. Синтаксический анализ.....................................................................................606

28 Математические примеры.......................................................................................610
28.1. Метод Ньютона...................................................................................................610
28.2. Интегрирование.................................................................................................614
28.3. Проект: гауссово исключение...........................................................................621

29 Алгоритмы с возвратами..........................................................................................627
29.1. Обход графов......................................................................................................627
29.2. Проект: возврат..................................................................................................636

30 Итоги. ..........................................................................................................................643
Интермеццо 5. Стоимость вычислений..........................................................................644
Конкретное время, абстрактное время.....................................................................645
Определение термина «порядка»..............................................................................651
Почему программы используют предикаты и селекторы?......................................654

VI АКК УМУЛЯТОРЫ .............................................................................................658
31 Потеря знаний............................................................................................................659
31.1. Проблема структурной обработки....................................................................659
31.2. Проблема генеративной рекурсии....................................................................663

32 Проектирование функций с аккумулятором.........................................................668
32.1. Условия применения аккумулятора..................................................................668
32.2. Добавление аккумуляторов...............................................................................670
32.3. Преобразование простых функций в функции с аккумуляторами................672
32.4. Графический редактор с поддержкой мыши...................................................684

33 Дополнительные примеры использования аккумуляторов................................687
33.1. Аккумуляторы и деревья...................................................................................687
33.2. Представления данных с аккумуляторами......................................................693
33.3. Аккумуляторы как результаты..........................................................................699

34 Итоги. ..........................................................................................................................706
Эпилог: что дальше...........................................................................................................708
Предметный указатель.....................................................................................................714

От редакторов
Добрый день! Спасибо, что вы открыли эту книгу, даже если взяли
с полки магазина просто посмотреть. Нам приятно в любом случае.
Для нас удача и честь принять участие в переводе такой легендарной
книги, чтобы вы могли прочесть ее на родном языке.
Что же делает ее легендарной? Вы наверняка заметили, что это
перевод второго издания, которое вышло уже почти 5 лет назад,
а первое – более 20 лет назад. Оставаться востребованным столько
лет в стремительно меняющемся мире информационных технологий
и языков программирования – достижение само по себе.
Авторы книги – легенды мира компьютерных наук и информатики, авторы фундаментальных работ на протяжении последних 30 лет.
Они же являются одними из пионеров систематического, научного
подхода к преподаванию программирования и информатики. Как результат, данная книга является отражением глубокого понимания как
самого предмета, так и методики его преподавания, сформировавшегося у авторов за десятилетия практики.
Другой фактор долголетия этого учебника – то, что он живой. Книга
продолжает развиваться даже после публикации: в неё постоянно вносят правки и уточнения, исправляют ошибки, она используется в ка­
чест­ве основы для курсов в университетах и на онлайн-площадках.
В результате получился один из лучших в мире учебников по программированию для новичков, который увлекательно читать, который никогда не перегружает лишними деталями, всегда ясно указывает направление развития и ненавязчиво прививает правила «хорошего тона».
Авторы справедливо отмечают, что это учебник по чему-то большему, чем просто программирование. В первую очередь он учит тому,
что в последнее время принято называть «computational thinking»,
«алгоритмическим (или вычислительным) мышлением». Этим стилем мышления пользуются не только программисты, но и повара,
учителя, спортсмены, врачи и многие, многие другие.
Авторы книги скромничают, когда говорят, что книга ориентирована на начинающих. Профессионалы встретят в ней как отсылки
к фундаментальным понятиям и методам компьютерных наук, так
и эффективные методы анализа повседневных задач. Поэтому от
всего сердца советуем вам купить эту книгу! Мы от этого богаче не
станем, но верим, что вас она способна обогатить интеллектуально,
творчески и профессионально, даже если разработка программного
обеспечения – не ваша основная деятельность.
С уважением от редакторов,
Павел Борисович Иванов,
Александр Дмитриевич Чичигин,
Юрий Алексеевич Сыровецкий,
Сергей Викторович Бронников

От издательства
Отзывы и пожелания
Мы всегда рады отзывам наших читателей. Расскажите нам, что вы
ду­маете об этой книге, – что понравилось или, может быть, не понравилось. Отзывы важны для нас, чтобы выпускать книги, которые
будут для вас максимально полезны.
Вы можете написать отзыв на нашем сайте www.dmkpress.com,
зайдя на страницу книги и оставив комментарий в разделе «Отзывы и рецензии». Также можно послать письмо главному редактору по
адресу dmkpress@gmail.com; при этом укажите название книги в теме
письма.
Если вы являетесь экспертом в какой-либо области и заинтересованы в написании новой книги, заполните форму на нашем сайте по
адресу http://dmkpress.com/authors/publish_book/ или напишите в издательство по адресу dmkpress@gmail.com.

Список опечаток
Хотя мы приняли все возможные меры для того, чтобы обеспечить
высокое качество наших текстов, ошибки все равно случаются. Если
вы найдете ошибку в одной из наших книг, мы будем очень благодарны, если вы сообщите о ней главному редактору по адресу dmkpress@
gmail.com. Сделав это, вы избавите других читателей от недопонимания и поможете нам улучшить последующие издания этой книги.

Нарушение авторских прав
Пиратство в интернете по-прежнему остается насущной проблемой.
Издательства «ДМК Пресс» и The MIT Press очень серьезно относятся
к вопросам защиты авторских прав и лицензирования. Если вы столк­
нетесь в интернете с незаконной публикацией какой-либо из наших
книг, пожалуйста, пришлите нам ссылку на интернет-ресурс, чтобы
мы могли применить санкции.
Ссылку на подозрительные материалы можно прислать по адресу
элект­ронной почты dmkpress@gmail.com.
Мы высоко ценим любую помощь по защите наших авторов, благодаря которой мы можем предоставлять вам качественные мате­риалы.

Вступление
Многие современные профессии требуют умения программировать
в той или иной форме. Бухгалтеры программируют электронные таб­
лицы; музыканты программируют синтезаторы; писатели программируют текстовые процессоры; а веб-дизайнеры программируют
таб­лицы стилей. Когда мы писали эти слова для первого издания книги (1995–2000), читатели могли счесть их футуристическими, однако
к настоящему времени умение программировать стало обязательным, и появились многочисленные книги, онлайн-курсы и предметы
в общеобразовательной школе, которые удовлетворяют эту потребность и улучшают шансы людей на трудоустройство.
Типичный курс программирования учит подходу «Пробуй, пока не
заработает». Добившись нужного результата, учащийся восклицает:
«Работает!» – и идет дальше. К сожалению, эта фраза также является
самой короткой небылицей в информатике и многим людям стоила
многих часов их жизни. Эта книга, напротив, фокусируется на навыках хорошего программирования и адресована всем – и профессиональным программистам, и любителям.
Под «хорошим программированием» мы подразумеваем подход
к созданию программного обеспечения, который изначально опирается на системное мышление, планирование и понимание на каж­дом
этапе и на каждом шаге. Чтобы подчеркнуть это, мы говорим о системном проектировании программ и системно спроектированных
программах. Что особенно важно, последнее словосочетание ясно
выражает требование к желаемой функциональности. Хорошее программирование также удовлетворяет эстетическое чувство выполненного долга; хорошая программа по своей элегантности сравнима
с хорошими стихами или черно-белыми фотографиями ушедшей
эпохи. Проще говоря, программирование отличается от хорошего
программирования как наброски карандашом на салфетке, сделанные в закусочной, от картин маслом в музее.
Нет, эта книга не превратит вас в мастера живописи. Но мы не стали бы тратить пятнадцать лет на подготовку данного издания, если
бы не верили, что
каждый может разрабатывать программы
и
каждый может испытывать удовлетворение от творческого процесса.
Более того, мы утверждаем, что
проектирование программ – но не программирование –
в традиционном для Запада высшем образовании должно стоять
рядом с математикой и лингвистикой.
Студент, изучающий проектирование, который никогда больше не
коснется программ, все равно приобретет универсально полезные

Вступление

13

навыки решения задач, приобретет опыт творческой деятельности
и научится ценить новую форму эстетики. Остальная часть этого
вступления подробно объясняет, что мы имеем в виду под «системным проектированием», кому и чем это выгодно и как мы обучаем
всему этому.

Системное проектирование программ
Программа взаимодействует с людьми, которых называют пользователями, и другими программами, которые могут быть серверами
или клиентами. Соответственно, любая более или менее полная программа состоит из множества строительных блоков, одни из которых
обрабатывают ввод, другие производят вывод, а третьи соединяют
первые со вторыми. В качестве фундаментальных строительных блоков мы предпочитаем использовать функции, потому что все мы хорошо знакомы с функциями по курсу школьной математики и потому что простейшие программы являются именно такими
Мы черпали вдохновение
функция­ми. Главное – выяснить, какие функции необходи- в методе, предложенном
мы, как их соединить между собой и как их сконструиро- Майклом Джексоном
(Michael Jackson) для
вать из основных ингредиентов.
В этом контексте «системное проектирование программ» создания программ на
языке COBOL, а также
означает сочетание двух составляющих: рецептов проекти- в беседах с Дэниелом
рования и итеративного уточнения. Рецепты проектирова- Фридманом (Daniel
ния – это изобретение авторов, обеспечивающее возмож- Friedman) о рекурсии,
Робертом Харпером
ность итеративного уточнения.
(Robert Harper) о теории
Рецепты проектирования применимы как к целым типов и Дэниелом Джекпрограммам, так и к отдельным функциям. В этой кни- соном (Daniel Jackson)
ге есть всего два рецепта для целых программ: один для о проектировании пропрограмм с графическим пользовательским интерфейсом граммного обеспечения.
(Graphical User Interface, GUI) и другой для неинтерактивных программ. Рецепты проектирования функций, напротив, намного разнообразнее: для данных атомарных типов, таких как числа; для
перечислений разных видов данных; для данных, которые фиксированным образом объединяют другие данные; для конечных, но произвольно больших данных; и так далее.
Рецепты проектирования для функций объединяются Преподавателям.
общим процессом проектирования. В списке в рецепте 1 Попросите учащихся
скопировать рецепт 1
перечислены шесть основных шагов этого процесса. На- на картонную карточку.
звание каждого шага сообщает ожидаемый результат(ы), Когда у кого-то из них
а «команды» определяют ключевые действия. Центральную возникнет проблема,
роль в этом процессе играют примеры. Для представления попросите их предъ­
явить карточку и указать
данных, выбранного на шаге 1, примеры иллюстрируют, шаг, на котором они
как фактическая информация кодируется в данные и как застряли.
данные интерпретируются в информацию. В шаге 3 говорится, что человек, решающий задачу, должен проработать конкрет-

14

Вступление

ные сценарии, чтобы понять, что должна вычислить функция в каж­
дом конкретном примере. Это понимание используется на шаге 5,
когда наступает время определения функции. Наконец, шаг 6 требует,
чтобы примеры были преобразованы в автоматизированные тесты,
проверяющие правильность работы функции в некоторых случаях.
Запуск функции на реальных данных может выявить другие расхождения между ожиданиями и результатами.
Рецепт 1. Базовый рецепт проектирования функций
1. Анализ задачи и определение данных. Определите, какая
информация и как должна быть представлена в выбранном
языке программирования. Сформулируйте определения данных и проиллюстрируйте их примерами.
2. Сигнатура, описание назначения, заголовок. Укажите, какие данные функция принимает и выдает. Сформулируйте короткий ответ на вопрос: «что вычисляет функция?» Определите
заглушку с соответствующей сигнатурой.
3. Примеры использования функции. Представьте примеры,
иллюстрирующие назначение функции.
4. Создание макета функции. Используя определения данных,
напишите набросок функции.
5. Определение функции. Заполните недостающие части в макете функции. Используйте определение назначения и примеры.
6. Тестирование. Переформулируйте примеры в тесты и убедитесь, что функция успешно выполняет их все. Это поможет обнаружить вкравшиеся ошибки. Кроме того, тесты дополнят
примеры и помогут другим понять определение функции, если
в этом возникнет необходимость, а она всегда возникает в любой серьезной программе.
На каждом шаге процесса проектирования возникают
вопросы. На некоторых из них, например на шаге создания
примеров использования функции или на шаге создания
макета, вопросы могут затрагивать определение данных.
Ответы на них почти автоматически создают промежуточный продукт. Затраты на эти подготовительные шаги окупаются, когда приходит время сделать творческий шаг – завершить определение функции, потому что оказывают необходимую
помощь почти во всех случаях.
Необычность этого подхода заключается в создании новичками
промежуточного продукта. Когда новичок застрянет на каком-то
шаге, эксперт или преподаватель сможет проверить созданные промежуточные продукты, задать наводящие вопросы, характерные для
процесса проектирования, и тем самым помочь новичку преодолеть
затруднение. Этот аспект самообразования является коренным отличием проектирования программ от программирования.

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

Вступление

15

Итеративное уточнение решает проблему сложности и многогранности задач. Сделать все и сразу практически невозможно. Для
решения этой проблемы специалисты по информатике заимствовали
метод итеративного уточнения из естественных наук. Согласно методу итеративного уточнения рекомендуется сначала отбросить все несущественные детали и найти решение основной задачи. Затем шаг
уточнения добавляет одну из отброшенных деталей, и расширенная
задача решается повторно с максимальным использованием сущест­
вующего решения. Повторение этих шагов уточнения и поиска решения в конечном итоге приводит к полному решению.
В этом смысле программист является своего рода ученым. Ученые
создают приблизительные модели идеализированной версии мира,
чтобы получить возможность делать прогнозы. Пока прогнозы сбываются, модель используется как есть; когда прогнозы начинают отличаться от реальности, ученые пересматривают свои модели, стремясь
уменьшить расхождения. Точно так же, когда программист получает задание, он создает первую модель, превращает ее в код, оценивает с по­
мощью пользователей и итеративно уточняет модель, пока поведение
программы не будет достаточно точно соответствовать желаемому.
В этой книге мы раскрываем итеративное уточнение с двух сторон. Поскольку проектирование методом итеративного уточнения
становится особенно полезным при проектировании сложных программ, книга подробно знакомит с этой техникой в четвертой части,
когда рассматриваемые задачи достигают определенной степени
сложности. Кроме того, в первых трех частях итеративное уточнение
используется для формулирования все более сложных вариантов одной и той же задачи. То есть в одной главе мы берем базовую задачу,
решаем еще в одной главе, а в следующей главе ставим аналогичную
задачу, но с дополнительными деталями, отражающими новые, свежие, только что введенные понятия.

DrRa­cket и учебные языки
Чтобы научиться проектировать программы, нужно постоянно практиковаться. Как нельзя стать пианистом, не играя на пианино, так же
нельзя стать разработчиком программ, не создавая программ и не
доводя их до корректного поведения. По этой причине наша книга
сопровождается некоторой программной поддержкой: языком, на
котором можно писать программы, и средой разработки программ,
в которой программы редактируются как текстовые документы
и с по­мощью которой можно запускать программы.
Многие, встречавшиеся нам и говорившие, что хотели Преподавателям.
бы научиться программированию, спрашивали, какой язык На курсах, предназначенных для опытных
программирования им следует выучить. Учитывая реклам- разработчиков, можно
ную шумиху, развернутую вокруг некоторых языков про- использовать другой
граммирования, такой вопрос не вызывает удивления. Но подходящий язык.

16

Вступление

проблема в том, что он совершенно неуместен. Обучение программированию на языке, модном в настоящее время, часто приводит обучающихся к неудачам. Мода в этом мире очень недолговечна. Типичная
книга «Короткий курс программирования на X» не может научить
принципам, которые будут перенесены на следующий модный язык.
Хуже того, сам язык часто отвлекает от приобретения передаваемых
навыков на уровне формулирования решений и на уровне исправления ошибок.
Обучение проектированию программ, напротив, заключается преж­
де всего в изучении принципов и приобретении передаваемых навыков. Идеальный язык программирования должен поддерживать обе
эти цели, чего нельзя сказать ни об одном стандартном промышленном языке. Ключевая проблема в том, что новички совершают ошибки еще до того, как более или менее существенно овладевают языком,
однако языки программирования диагностируют эти ошибки, как
если бы программист уже знал все его тонкости. В результате сообщения об ошибках часто ставят новичков в тупик.
Наше решение: начать со знакомства с нашим собственПреподавателям.
ным специализированным языком обучения, который мы
Вы можете объяснить,
что BSL – это школьная назвали «язык для начинающих» (Beginning Student Lanалгебра с дополнитель- guage, BSL). По сути, это почти тот же «иностранный» язык,
ными формами данных который изучается в школьном курсе математики. Он вклюи множеством предопречает обозначения для определений функций, применения
деленных функций для
работы с ними. функций и условных выражений. Кроме того, он допускает
вложенность выражений. То есть этот язык настолько мал,
что диагностика ошибок в терминах всего языка доступна читателям,
у которых нет никаких знаний, кроме начального курса математики.
Учащийся, овладевший принципами структурного проектирования, может затем перейти к «языку для учащихся промежуточной
сложности» (Intermediate Student Language, ISL) и другим продвинутым диалектам с групповым названием *SL. В книге эти диалекты
используются для обучения принципам абстракции и рекурсии. Мы
твердо уверены, что использование такой последовательности обучающих языков позволяет читателям подготовить себя к созданию
программ на широком спектре профессиональных языков программирования (JavaScript, Python, Ruby, Java и др.).
ПРИМЕЧАНИЕ. Обучающие языки реализованы на Racket, языке
программирования для создания языков программирования. Racket
ускользнул из лаборатории в реальный мир и постепенно стал применяться для решения самых разных задач, от создания игр до реализации управления массивами телескопов. Обучающие языки заимствуют некоторые элементы из языка Racket, но эта книга не учит программированию на Racket. Однако учащийся, прочитавший эту книгу,
с легкостью сможет начать программировать на Racket. КОНЕЦ.
Выбирая среду программирования, мы оказываемся в такой же
плохой ситуации, как при выборе языка программирования. Среда

Вступление

программирования для профессионалов подобна кабине современного реактивного самолета. Она имеет множество элементов управления и дисплеев, непостижимых для любого, кто впервые запускает
такое программное приложение. Начинающим программистам нужен эквивалент двухместного одномоторного поршневого самолета,
на котором они могут практиковать базовые навыки. Поэтому мы создали среду программирования DrRa­cket для новичков.
DrRa­cket поддерживает очень увлекательное обучение с обратной
связью с использованием всего двух простых интерактивных панелей: области определений, содержащей определения функций, и области взаимодействия, которая позволяет программисту запросить
вычисление выражений, связанных с определениями. В этом контексте исследовать сценарии «что, если» так же легко, как в приложении для работы с электронными таблицами. Экспериментирование
можно начать при первом же контакте, используя обычные примеры
в стиле калькулятора и быстро переходя к вычислениям с изображениями, словами и другими формами данных.
Интерактивная среда разработки программ, такая как DrRa­cket,
упрощает процесс обучения. Во-первых, она позволяет начинающим программистам напрямую манипулировать данными. Поскольку нет никаких средств для чтения входной информации из файлов
или устройств хранения, новичкам не нужно тратить драгоценное
время на выяснение особенностей их использования. Во-вторых,
среда разработки четко отделяет данные и операции с ними от ввода и вывода информации из «реального мира». В настоящее время
это разделение считается настолько фундаментальным для системного проектирования программного обеспечения, что получило собственное название: архитектура модель-представление-контроллер
(Model-View-Controller, MVC). Работая в DrRa­cket, начинающие программисты естественным путем знакомятся с этой фундаментальной
идеей программной инженерии.

Применение навыков
Приобретенные в процессе обучения навыки проектирования программ находят системное применение в двух направлениях: в программировании вообще и в программировании электронных таблиц,
синтезаторов, таблиц стилей и даже текстовых процессоров в частности. Как показывают наши наблюдения, процесс проектирования,
представленный в рецепте 1, легко перенести практически на любой
язык программирования и можно использовать для разработки как
коротких, насчитывающих десяток строк, так и длинных программ,
состоящих из десятков тысяч строк кода. Конечно, необходимо какое-­
то время, чтобы осмыслить и адаптировать процесс проектирования
ко всему спектру языков и к разным масштабам программ; но как

17

18

Вступление

только он станет второй натурой, его применение становится естественным и начинает приносить выгоду.
Обучение проектированию программ также означает обретение
двух универсальных навыков. Проектирование программ, безусловно, дает те же аналитические навыки, что и математика, особенно
алгебра и геометрия. Но, в отличие от математики, работа с программами – это активный подход к обучению. Процесс создания
программного обеспечения включает немедленную обратную связь
и тем самым способствует исследованиям, экспериментам и само­
оценке. Результатом, как правило, являются интерактивные продукты, создание которых дает более мощное чувство удовлетворенности,
чем решение упражнений в учебниках.
Проектирование программ тренирует не только математические
навыки, но также навыки чтения и письма. Даже самые маленькие
задачи проектирования формулируются в текстовом виде. Без прочных навыков чтения и понимания прочитанного невозможно проектировать программы, которые решают более или менее сложные
задачи. И наоборот, методы проектирования программ заставляют
разработчика излагать свои мысли правильным и точным языком.
Фактически, усваивая рецепт проектирования, учащийся одновременно совершенствует свои навыки артикуляции.
Для иллюстрации взгляните еще раз на описание процесса проектирования в рецепте 1. В нем говорится, что проектировщик должен:
1) проанализировать постановку задачи, обычно обозначаемую
словом «задача»;
2) извлечь и абстрактно выразить ее суть;
3) проиллюстрировать суть примерами;
4) определить макет на основе этого анализа;
5) получить результаты и сопоставить их с ожиданиями;
6) доработать продукт с учетом неудачных проверок и тестов.
Каждый шаг требует анализа, описания, точности, сосредоточенности и внимания к деталям. Любой опытный предприниматель, инженер, журналист, юрист, ученый и любой другой профессионал сможет
подтвердить, насколько эти навыки важны в повседневной работе.
Практика проектирования программ – на бумаге и в DrRa­cket – это
приятный способ приобретения навыков.
Точно так же совершенствование проекта не ограничивается информатикой и созданием программ. Этим занимаются и архитекторы, и композиторы, и писатели, и другие профессионалы. Они начинают с идей в голове и каким-то образом формулируют их суть.
Они уточняют идеи на бумаге, пока продукт не будет максимально
точно отражать мысленное представление. Воплощая идеи на бумаге, они используют навыки, аналогичные рецептам проектирования:
рисование, письмо или игра на музыкальном инструменте, чтобы выразить определенные элементы стиля здания, описать характер че-

Вступление

ловека или сформулировать элементы мелодии. Их продуктивность
в итеративном процессе разработки обусловлена знанием и умением
применять базовые рецепты проектирования в своей сфере и правильно выбирать наиболее подходящий для текущей ситуации.

Структура книги
Цель этой книги – познакомить читателей, не имеющих практического опыта, с системным проектированием программ. Параллельно она
знакомит с символическим представлением вычислений – методом объяснения, как работает применение программы к данным. Проще говоря, этот метод обобщает сведения по арифметике и алгебре, которые
учащиеся получают в начальной и средней школах соответственно.
Но пусть вас это не пугает. В DrRa­cket имеется механизм пошаговых
вычислений, способный иллюстрировать такие вычисления по шагам.
Книга состоит из шести частей, разделенных пятью интермеццо,
и обрамляется прологом и эпилогом. Основные части посвящены
проектированию программ, а промежуточные интермеццо вводят
дополнительные понятия, касающиеся механики программирования
и вычислений.
Пролог – это краткое введение в простое программирование. В нем
объясняется, как реализовать простую анимацию на *SL. Прочитав
его, любой новичок почувствует воодушевление и подавленность одновременно. Поэтому в последнем примечании объясняется, почему
обычное программирование – это ошибочный путь, и как системный
и последовательный подход к проектированию программ устраняет
чувство страха, которое обычно испытывает каждый начинающий
программист.
За прологом следуют основные части книги.
zz Часть I описывает наиболее фундаментальные понятия си-

стемного проектирования на простых примерах. Основная
идея заключается в том, что проектировщики обычно имеют некоторое представление о том, какие данные программа
должна принимать и производить. По этой причине при системном подходе к проектированию необходимо извлечь как
можно больше подсказок из описания данных, поступающих
в программу и исходящих из нее. Для простоты эта часть начинается с атомарных данных – чисел, изображений и т. д., –
а затем постепенно вводит новые способы описания данных:
интервалы, перечисления, структуры и их комбинации.
zz Интермеццо 1 подробно описывает язык обучения: его словарь, грамматику и значение. Все вместе это обычно называют
синтаксисом и семантикой. Разработчики программ используют эту модель вычислений для прогнозирования действий их
творения после запуска или для анализа ошибок.

19

20

Вступление
zz Часть II дополняет часть I средствами описания наиболее ин-

zz
zz

zz

zz
zz

zz

тересных и полезных форм данных: составных данных произвольного размера. Программист может продолжать использовать типы данных из части I для представления информации,
но эти типы всегда имеют фиксированную глубину и ширину.
Эта часть демонстрирует, как небольшое обобщение позволяет
перейти к данным произвольного размера. Затем мы переключим свое внимание на системное проектирование программ,
обрабатывающих такие данные.
Интермеццо 2 вводит краткую и мощную нотацию для записи
больших объемов данных: цитирование и антицитирование.
Часть III наглядно демонстрирует сходство многих функций из
части II. Никакой язык программирования не должен заставлять программистов создавать фрагменты кода, настолько похожие друг на друга. И наоборот, во всяком хорошем языке программирования есть способы устранения подобного сходства.
Ученые-информатики называют этап устранения сходства абстрагированием, а его результат – абстракцией. Они знают, что
абстракции значительно повышают продуктивность программиста. По этой причине в данной части будут представлены рецепты создания и использования абстракций.
Интермеццо 3 преследует две цели. Во-первых, здесь вводится
понятие лексической области видимости, когда язык программирования связывает каждое вхождение имени с его определением, которое программист может найти, просматривая код.
Во-вторых, объясняется суть библиотеки с дополнительными
механизмами абстракции, включая так называемые циклы for.
Часть IV обобщает часть II и явно вводит идею итеративного
уточнения в словарь понятий проектирования.
Интермеццо 4 объясняет и иллюстрирует, почему десятичные
числа работают таким странным образом во всех языках программирования. Представленные здесь основные факты должен знать каждый начинающий программист.
Часть V добавляет новый принцип дизайна. Структурного проектирования и абстракции вполне достаточно для решения
большинства задач, с которыми сталкиваются программисты,
но иногда этого мало для создания «производительных» программ. То есть программам, созданным с применением принципов структурного проектирования, может потребоваться
слишком много времени или энергии для вычисления желаемых ответов. Поэтому ученые-информатики заменяют такие
программы, созданные с применением принципов структурного проектирования, программами, способными извлекать
выгоду из глубокого понимания предметной области. В этой
части книги показано, как спроектировать большой класс
именно таких программ.

Вступление
zz Интермеццо 5 использует примеры из части V для иллюстра-

ции представлений ученых-информатиков о производительности.
zz Часть VI добавляет последний трюк в инструментарий проектировщиков: аккумуляторы. Если говорить упрощенно, аккумулятор добавляет «память» в функции. Добавление памяти
значительно улучшает производительность функций из первых четырех частей книги, созданных с применением принципов структурного проектирования. Для специальных программ
из части V аккумуляторы могут даже гарантировать нахождение ответа.
zz Эпилог: что дальше – это одновременно оценка пройденного
и взгляд в будущее.

Читатели, занимающиеся самообразованием самостоятельно,
должны проработать всю книгу, от первой до последней страницы.
Под словом «проработать» мы подразумеваем, что они должны решить все упражнения или, по крайней мере, знать, как их решать.
Преподаватели тоже должны охватить как можно больше, начиная
с пролога и заканчивая эпилогом. Наш опыт преподавания показывает, что это выполнимо. Как правило, мы организуем наши курсы
так, чтобы слушатели в течение семестра писали большую и увлекательную программу. Однако мы понимаем, что могут быть обстоятельства, диктующие значительные сокращения, и вкусы некоторых
преподавателей требуют иных способов использования книги.
На рис. 1 изображена навигационная схема для тех, кто предпочитает получать знания выборочно. Эта схема представляет собой граф
зависимостей. Сплошная стрелка от одного элемента к другому предполагает обязательный порядок чтения; например, прежде чем перейти к части II, обязательно нужно изучить часть I. Пунктирные стрелки, напротив, обозначают предлагаемый маршрут; например, читать
пролог перед остальной частью книги необязательно.
Вот три возможных пути изучения книги, которые можно предложить, основываясь на этой схеме:
zz преподаватель старшей школы может пройти с учениками (на-

сколько это возможно) части I и II, включая небольшой проект;
zz преподаватель университета с квартальной системой обучения
может сосредоточиться на части I, части II, части III и части V,
а также интермеццо по *SL и области видимости;
zz преподаватель университета с семестровой системой обучения
может предпочесть как можно раньше охватить компромиссы
производительности в проектировании. В этом случае мы можем порекомендовать изучить части I и II, а затем раздел об
аккумуляторах в части VI, который не зависит от части V. Пос­
ле этого можно углубиться в интермеццо 5 и затем охватить
остальную часть книги.

21

22

Вступление
Пролог

I
*SL
II
Природа чисел
Цитирование

III

Область видимости
и абстракция

IV

V

VI

Стоимость вычислений

Эпилог

Рис. 1. Зависимости между частями книги и интермеццо

Повторение примеров и упражнений. Повествование в книге
снова и снова возвращается к определенным упражнениям и примерам. Например, виртуальные домашние животные встречаются
повсюду в части I и иногда даже в части II. Точно так же в обеих час­
тях, I и II, рассматриваются альтернативные подходы к реализации
интерактивного текстового редактора. В части V появляются графы,
которые перекочевывают в часть VI. Цель этих повторений – последовательное уточнение и закрепление изученного. Мы призываем преподавателей использовать в своей работе эти примеры и упражнения
или создать свои подобные последовательности.

Различия между изданиями
Второе издание «Как проектировать программы» имеет несколько
важных отличий от первого издания:
1) подчеркивает разницу между проектированием всей программы и отдельных функций, составляющих ее. В частности, в этом
издании основное внимание уделяется программам двух типов: управляемым событиями (с графическим интерфейсом
и сетевым) и неинтерактивным;
2) проектирование программы на этапе планирования осуществ­
ляется сверху вниз, а на следующем за ним этапе построения –

23

Вступление

снизу вверх. Мы явно показываем, как интерфейсы библиотек
определяют форму элементов программы. В частности, на самом первом этапе проектирования программы создается список желаемых функций. Да, идея списка желаний присутствует
и в первом издании, но во втором издании она рассматривается как явный элемент дизайна;
3) выполнимость пункта из списка желаний зависит от рецепта
проектирования функции, о чем рассказывается в шести основных частях книги;
4) ключевым элементом структурного проектирования Мы благодарим Кэти
является определение функций, являющихся компози- Фислер (Kathi Fisler)
цией других функций. Такая композиционная органи- за то, что обратила
наше внимание на этот
зация особенно полезна в мире неинтерактивных про- момент.
грамм. Как и порождающая рекурсия, для правильной
композиции требуется озарение и признание того факта, что последовательная обработка промежуточных результатов несколькими функциями упрощает общий процесс проектирования. Этот
подход тоже требует составить список желаний, но при формулировании этих желаний необходимо тщательно проработать определения промежуточных данных. Это издание книги включает
ряд явных упражнений по композиционному проектированию;
5) тестирование всегда было частью нашей философии проектирования, однако языки обучения и DrRa­cket начали обеспечивать достаточно полную его поддержку только в 2002 году, уже
после выхода первого издания. Данное новое издание в значительной степени полагается на эту поддержку тестирования;
6) из этого издания мы исключили тему проектирования императивных программ. Старые главы можно найти на нашем сайте1,
а их адаптированные версии войдут во второй том данной серии «How to Design Compo­nents»;
7) в этом издании изменены обучающие пакеты с примерами
и упражнениями. Предпочтительным стилем связывания этих
библиотек считается применение инструкции require, но вы все
еще можете добавлять их через меню в DrRa­cket;
8) наконец, во втором издании несколько изменились терминология и обозначения:
Второе издание
сигнатура
детализация
'()
#true
#false

Первое издание
контракт
объединение
empty
true
false

Последние три отличия значительно улучшают цитирование списков.
1

https://htdp.org/2003-09-26/Book/. – Прим. перев.

24

Вступление

Благодарности из первого издания
Особую благодарность мы хотим выразить четырем нашим коллегам:
Роберту Картрайту (Robert «Corky» Cartwright), который совместно
с первым автором (Маттиасом Фелляйзеном) разработал вводный курс
для университета имени Уильяма Марша Райса; Даниелю П. Фрид­ману
(Daniel P. Friedman), предложившему первому автору переписать
книгу «The Little LISPer» (также изданную в MIT Press) в 1984 году,
которая положила начало этому проекту; Джону Клементсу (John Clements), разработавшему, внедрившему и обслуживающему механизм
пошаговых вычислений для DrRa­cket; и Полу Стеклеру (Paul Steckler),
который поддерживал команду, внося свой вклад в разработку нашего набора инструментов программирования.
Участие в создании книги приняли многие другие наши друзья
и коллеги, которые использовали ее в своих курсах или давали подробные комментарии к рукописи. Мы благодарны вам за помощь
и терпение: Ян Барланд (Ian Barland), Джон Клементс (John Cle­ments),
Брюс Дуба (Bruce Duba), Майк Эрнст (Mike Ernst), Кэти Фислер (Kathi
Fisler), Даниель П. Фридман (Daniel P. Friedman), Джон Грейнер (John
Greiner), Джеральдин Морен (Géraldine Morin), Джон Стоун (John
Stone) и Вальдемар Тамез (Valdemar Tamez).
Десятки поколений студентов курса Comp 210 в университете Райса
использовали ранние версии текста и предложили различные улучшения. Кроме того, многочисленные участники нашей конференции
TeachScheme! использовали рукопись в своих курсах. Многие из них
прислали свои комментарии и предложения. Хотелось бы отметить
наиболее активных участников: г-жа Барбара Адлер (Ms. Barbara Adler), д-р Стивен Блох (Dr. Stephen Bloch), г-жа Карен Бурас (Ms. Karen
Buras), г-н Джек Клей (Mr. Jack Clay), д-р Ричард Клеменс (Dr. Richard
Clemens), г-н Кайл Джиллетт (Mr. Kyle Gillette), г-н Марвин Эрнандес
(Mr. Marvin Hernandez), г-н Майкл Хант (Mr. Michael Hunt), г-жа Карен Норт (Ms. Karen North), г-н Джейми Рэймонд (Mr. Jamie Raymond)
и г-н Роберт Рид (Mr. Robert Reid). Кристофер Фелляйзен (Christopher
Felleisen) участвовал в проработке нескольких первых частей книги вместе со своим отцом и помог получить представление о взглядах молодого студента. Хрвое Блажевич (Hrvoje Blazevic, в то время
яхтсмен, а ныне капитан танкера LPG/C Harriette), Джо Захарий (Joe
Zachary, университет штата Юта) и Даниель П. Фридман (Daniel P.
Friedman, университет штата Индиана) помогли найти и исправить
многочисленные опечатки в первом издании. Спасибо всем вам.
Наконец, Матиас выражает свою благодарность Хельге (Helga) за
ее многолетнее терпение и за окружение уютом ее рассеянных мужа
и отца. Робби выражает благодарность Синь-Хуэй Хуан (Hsing-Huei
Huang) – без ее поддержки и терпения он не смог бы работать так
плодотворно. Мэтью благодарит Вэнь Юань (Wen Yuan) за ее постоянную поддержку и непреходящую музыку. Шрирам выражает призна-

Вступление

тельность Кати Фислер (Kathi Fisler) за поддержку, терпение и бесконечные шутки, а также за ее участие в этом проекте.

Благодарности
Как и в 2001 году, мы благодарны Джону Клементсу (John Clements) за
разработку, проверку, внедрение и поддержку механизма пошаговых
вычислений для DrRa­cket. Он занимается им уже почти 20 лет, и его
движок стал незаменимым инструментом обучения.
За последние несколько лет некоторые наши коллеги передали нам
свои комментарии и предложения по улучшению. Мы с благодарностью отмечаем содержательные беседы и обмен мнениями с этими
людьми:
Кэти Фислер (Kathi Fisler, Вустерский политехнический институт и университет Брауна), Грегор Кичалес (Gregor Kiczales, университет Британской Колумбии), Прабхакар Рагде
(Prabhakar Ragde, университет Ватерлоо) и Норман Рэмси
(Norman Ramsey, университет Тафтса).
Тысячи преподавателей посетили наши семинары, проводившиеся
на протяжении многих лет, и многие из них давали нам ценные отзывы. Но особенно нам хотелось бы отметить Дэна Андерсона (Dan
Anderson), Стивена Блоха (Stephen Bloch), Джека Клея (Jack Clay), Надима Абдул Хамида (Nadeem Abdul Hamid) и Виера Пру (Viera Proulx),
внесших большой вклад в это издание.
Гийом Марсо (Guillaume Marceau), работая вместе с Кэти Фислер
(Kathi Fisler) и Шрирам, потратил много месяцев на изучение сообщений об ошибках в DrRa­cket и их устранение. Мы благодарны ему за
его прекрасную работу.
Селеста Холленбек (Celeste Hollenbeck) – самая удивительная наша
читательница. Она не уставала спрашивать снова и снова, пока не усваивала материал до конца. Она никогда не останавливалась на полпути, продвигая свои тезисы. Большое спасибо за ваши невероятные
усилия.
Мы также благодарим Эннаса Абдуссалама (Ennas Abdussalam),
Марка Олдрича (Mark Aldrich), Мехмета Акифа Аккуса (Mehmet Akif
Akkus), Анису Ануар (Anisa Anuar), Кристофа Бадура (Christoph Badura), Франко Барбайта (Franco Barbeite), Саада Башира (Saad Bashir),
Аарона Баумана (Aaron Bauman), Сюзанну Беккер (Suzanne Becker),
Майкла Бауша (Michael Bausch), Стивена Белкнапа (Steven Belknap),
Стивена Блоха (Stephen Bloch), Илью Боткина (Elijah Botkin), Джозефа Богарта (Joseph Bogart), Уильяма Брауна (William Brown), Томаса Кабрера (Tomas Cabrera), Сююкун Си (Xuyuqun C), Колина Кейна
(Colin Caine), Энтони Каррико (Anthony Carrico), Родольфо Карвалью
(Rodolfo Carvalho), Эстево Кастро (Estevo Castro), Марию Чакон (Maria Chacon), Стивена Чанга (Stephen Chang), Дэвида Чатмана (David

25

26

Вступление

Chatman), Берли Харитон (Burleigh Chariton), Тунг Ченг (Tung Cheng),
Нельсона Чиу (Nelson Chiu), Томаша Хрщоновича (Tomasz Chrzczonowicz), Джека Клея (Jack Clay), Ричарда Клейса (Richard Cleis), Джона
Клементса (John Clements), Скотта Краймбла (Scott Crymble), Пирса
Дарра (Pierce Darragh), Йонаса Декрекера (Jonas Decraecker), Ку Дунфанг (Qu Dongfang), Доминика Дейкхейзена (Dominique Dijkhuizen),
Марка Энгельберга (Mark Engelberg), Эндрю Фаллоуза (Andrew Fallows), Цзянькун Фань (Jiankun Fan), Кристофера Фелляйзена (Christopher Felleisen), Себастьяна Фелляйзена (Sebastian Felleisen), Владимира Гаджика (Vladimir Gajić), Синь Гао (Xin Gao), Адриана Германа
(Adrian German), Джека Гительсона (Jack Gitelson), Кайл Джиллетт
(Kyle Gillette), Джонатана Гордона (Jonathan Gordon), Скотта Грина
(Scott Greene), Бена Гринмана (Ben Greenman), Райана Голбека (Ryan
Golbeck), Джоша Грэмса (Josh Grams), Григориоса (Grigorios), Джейн
Грискти (Jane Griscti), Альберто Э. Ф. Герреро (Alberto E. F. Guerrero),
Тайлер Хаммонд (Tyler Hammond), Нана Халберга (Nan Halberg), Ли
Цзюнсон (Li Junsong), Надима Абдул Хамида (Nadeem Abdul Hamid),
Джереми Хэнлона (Jeremy Hanlon), Тони Хенка (Tony Henk), Крейга
Холбрука (Craig Holbrook), Коннора Хецлера (Connor Hetzler), Бенджамина Хоссейнзала (Benjamin Hosseinzahl), Уэйн Иба (Wayne Iba), Джона Джекмана (John Jackaman), Джордана Джонсона (Jordan Johnson),
Блейка Джонсона (Blake Johnson), Эрвина Юнга (Erwin Junge), Марка
Кауфманна (Marc Kaufmann), Коула Кендрика (Cole Kendrick), Грегора Кичалеса (Gregor Kiczales), Юджина Колбекера (Eugene Kohlbecker),
Ярослава Колосовского (Jaroslaw Kolosowski), Кейтлин Крамер (Caitlin Kramer), Романа Кунин (Roman Kunin), Джексона Лоулера (Jackson
Lawler), Девона Лепажа (Devon LePage), Бена Лернера (Ben Lerner), Шиченг Ли (Shicheng Li), Чен Элджей (Chen Lj), Эда Мафиса (Ed Maphis),
Юшенг Мэй (YuSheng Mei), Андреса Меза (Andres Meza), Саада Махмуда (Saad Mhmood), Елену Мачкасову (Elena Machkasova), Джея Мартина (Jay Martin), Александра Мартинеса (Alexander Martinez), Юрия
Машика (Yury Mashika), Джея Э. Маккарти (Jay McCarthy), Джеймса
Макдоннелла (James McDonell), Майка Макхью (Mike McHugh), Уэйда
Макрейнольдса (Wade McReynolds), Дэвида Мозеса (David Moses), Энн
Е. Москол (Ann E. Moskol), Скотта Ньюсона (Scott Newson), Штепана
Немеца (Štěpán Němec), Пола Оджанена (Paul Ojanen), проф. Роберта
Ордоньеса (Prof. Robert Ordóñez), Лоран Орсо (Laurent Orseau), Клауса
Остерманна (Klaus Ostermann), Аланну Паско (Alanna Pasco), Синана
Пехливаноглу (Sinan Pehlivanoglu), Эрика Паркера (Eric Parker), Дэвида Портера (David Porter), Ника Плицикаса (Nick Pleatsikas), Пратьюша Прамода (Prathyush Pramod), Алока Рая (Alok Rai), Нормана Рамси
(Norman Ramsey), Кришнана Равикумара (Krishnan Ravikumar), Джекоба Рубина (Jacob Rubin), Ильнара Салимзянова (Ilnar Salimzianov),
Луиса Санджуана (Luis Sanjuán), Брайана Шака (Brian Schack), Райана «Хевви» Шила (Ryan «Havvy» Scheel), Лизу Шойинг (Lisa Scheuing),
Вилли Шигеля (Willi Schiegel), Винита Шаха (Vinit Shah), Ника Шелли
(Nick Shelley), Эдварда Шена (Edward Shen), Тубо Ши (Tubo Shi), Хеен

Вступление

Шин (Hyeyoung Shin), Атхарва Шукла (Atharva Shukla), Мэтью Сингера
(Matthew Singer), Майкла Сигела (Michael Siegel), Стивена Сигела (Stephen Siegel), Милтона Сильву (Milton Silva), Картика Сингхала (Kartik
Singhal), Джо Сникериса (Joe Snikeris), Марка Смита (Marc Smith), Маттиса Смита (Matthijs Smith), Дэйва Смайли (Dave Smylie), Винсента
Сент-Амура (Vincent St-Amour), Рида Стивенса (Reed Stevens), Уильяма Стивенсона (William Stevenson), Кевина Салливана (Kevin Sullivan),
Асуму Такикава (Asumu Takikawa), Эрика Тантера (Éric Tanter), Сэма
Тобина-Хохштадта (Sam Tobin-Hochstadt), Таноса Цуанаса (Thanos
Tsouanas), Аарона Цая (Aaron Tsay), Маришку Тваалфховен (Mariska
Twaalfhoven), Бора Гонсалеса Усача (Bor Gonzalez Usach), Рикардо Руи
Валле-мена (Ricardo Ruy Valle-mena), Мануэля дель Валле (Manuel del
Valle), Дэвида Ван Хорна (David Van Horn), Ника Вона (Nick Vaughn),
Симеона Вельдстру (Simeon Veldstra), Андре Вентера (Andre Venter),
Яна Витека (Jan Vitek), Марко Виллотта (Marco Villotta), Митча Ванда (Mitch Wand), Юсюй (Эвен) Ван (Yuxu (Ewen) Wang), Майкла Виджайю (Michael Wijaya), Дж. Клиффорда Уильямса (G. Clifford Williams),
Эвана Уиттакера-Уокера (Ewan Whittaker-Walker), Джулию Влоховски (Julia Wlochowski), Рулофа Воббена (Roelof Wobben), Дж. Т. Райта
(J. T. Wright), Мардина Ядегара (Mardin Yadegar), Хуан Ичао (Huang
Yichao), Ю Ван Инь (Yuwang Yin), Эндрю Ципперера (Andrew Zipperer)
и Ари Цви (Ari Zvi) за комментарии к рукописи этого второго издания.
Верстка сайта htdp.org создана Мэтью Баттериком (Matthew Butterick), который также разработал стили оформления для нашей онлайн-документации.
Наконец, мы благодарны Аде Брунштейн (Ada Brunstein) и Мари
Луфкин Ли (Marie Lufkin Lee), нашим редакторам из MIT Press, которые дали нам разрешение на публикацию в интернете рукописей
второго издания «Как проектировать программы». Мы также благодарим Кристин Бриджит Сэвидж (Christine Bridget Savage) из Массачусетского технологического института и Джона Хоуи (John Hoey) из
Westchester Publishing Services за управление процессом публикации.
Джон Донохью (John Donohue), Дженнифер Робертсон (Jennifer Ro­
bert­son) и Марк Вудворт (Mark Woodworth) проделали большую работу по редактированию рукописи.

27

Пролог: как писать программы
Когда вы были маленьким ребенком, родители учили вас считать на
пальцах: «1 + 1 равно 2»; «1 + 2 равно 3» и т. д. Затем они спрашивали:
«А сколько будет 3 + 2?» – и вы считали пальцы одной руки. Они программировали, а вы вычисляли. В каком-то смысле это все, что нужно
для программирования и вычислений.
Загрузите DrRa­cket
Теперь пришло время поменяться ролями. Запус­ с веб-сайта проекта.
тите DrRa­cket. Перед вами откроется окно, как показано на рис. 21. Выберите пункт Choose language (Выбрать язык...)
в меню Language (Язык), после чего в открывшемся диалоге выберите пункт Teaching Languages (Учебные языки) и внутри этого пункта,
в списке под заголовком How to Design Programs (Как проектировать программы), выберите пункт Beginning Student (Начинающий
студент, то есть язык для начинающих студентов – BSL) и щелкните
на кнопке ОК. Теперь вы можете начать программировать, а DrRa­cket
будет вашим ребенком. Начните с простейших вычислений. Введите
(+ 1 1)

в верхней половине окна DrRa­cket, щелкните на кнопке RUN (Выполнить) – и в нижней половине появится число 2.

Рис. 2. Общий вид окна DrRa­cket
1

Редактор DrRa­cket имеет русифицированный интерфейс. Чтобы включить его, выберите пункт меню Help > Работать с русским интерфейсом DrRa­cket. После
этого откроется диалог, предупреждающий, что для смены языка интерфейса необходимо перезапустить редактор. Щелкните на кнопке Accept and Exit, или Применить и выйти, а затем снова запустите DrRa­cket. – Прим. перев.

30

Пролог: как писать программы

Как видите, программировать ничуть не сложно. Вы задаете вопросы, как если бы DrRa­cket был ребенком, а DrRa­cket выполняет
вычисления. Вы также можете попросить DrRa­cket обработать сразу
несколько запросов:
(+
(*
((/

2
3
4
6

2)
3)
2)
2)

После щелчка на кнопке RUN (Выполнить) и в нижней половине
окна появятся числа 4 9 2 3 – ожидаемые результаты.
Теперь приостановимся ненадолго и проясним некоторые термины.
zz Верхняя половина окна DrRa­cket называется областью опреде-

лений. В этой области создаются программы, а процесс их создания называется редактированием. Сразу после добавления
нового слова или изменения чего-либо в области определений
в верхнем левом углу появляется кнопка SAVE (Сохранить).
Пос­ле первого щелчка на кнопке SAVE (Сохранить) DrRa­cket
попросит указать имя файла, чтобы сохранить вашу программу.
После того как область определений будет связана с файлом,
последующие щелчки на кнопке SAVE (Сохранить) помогут вам
гарантировать своевременное сохранение содержимого области определений в этом файле.
zz Программы состоят из выражений. Вы уже не раз видели выражения на уроках математики. Выражение представляет собой
либо обычное число, либо что-то, что начинается с открывающей круглой скобки «(» и заканчивается парной ей закрывающей круглой скобкой «)». DrRa­cket распознает парные скобки
и закрашивает область между скобками.
zz После щелчка на кнопке RUN (Выполнить) DrRa­cket вычисляет
выражения в области определений и выводит полученные результаты в области взаимодействий. Затем DrRa­cket, ваш верный слуга, выводит приглашение к вводу (>) и ждет ваших команд. Этим приглашением DrRa­cket сигнализирует, что готов
к вводу дополнительных выражений, которые он вычислит
и выведет результат точно так же, как если бы выражение было
введено в области определений:
> (+ 1 1)
2

Введите выражение рядом с приглашением, нажмите клавишу
Return или Enter на клавиатуре и посмотрите, как DrRa­cket отреагирует на результат. Вы можете ввести столько выражений,
сколько пожелаете, например:
> (+ 2 2)
4

Пролог: как писать программы

31

> (* 3 3)
9
> (- 4 2)
2
> (/ 6 2)
3
> (sqr 3)
9
> (expt 2 3)
8
> (sin 0)
0
> (cos pi)
#i-1.0

Внимательно рассмотрите последний номер. Префиксом «#i» DrRa­
cket сообщает: «На самом деле я не знаю точного числа, поэтому получите то, что у меня есть, – неточное (inexact) число». В отличие от
вашего калькулятора или других систем программирования, DrRa­cket
честен. Когда точное число неизвестно, в ответ добавляется специальный префикс. Позже мы покажем настоящие странности, которые
творятся с «компьютерными числами», и тогда вы по достоинству
оцените предупреждения, которые выводит DrRa­cket.
Возможно, вам интересно узнать: может ли DrRa­cket складывать
больше двух чисел сразу? Да, может! Сделать это можно двумя разными способами:
> (+ 2 (+ 3 4))
9
> (+ 2 3 4)
9

Первый способ – использование вложенных арифметиче- Эта книга не научит
ских выражений. Он известен всем нам еще со школы. Вто- вас программированию
на языке Racket, даже
рой – арифметические выражения на языке BSL; это более притом что редактор
естественный способ, потому что в языке BSL операции называется DrRa­cket.
Прочитайте вступлеи числа всегда заключаются в круглые скобки.
В BSL всегда, когда требуется выполнить арифметическую ние, особенно раздел
cket и языки
операцию, выражение начинается с открывающей скобки, «DrRa­
обучения», где подробно
за которым следуют: символ операции, скажем +; числа, рассказывается
к которым нужно применить операцию (через пробел или о выборе языка.
даже через разрывы строк); и, наконец, закрывающая скобка. Элементы, следующие за операцией, называются операндами. При
использовании вложенных выражений эти выражения сами выступают в роли операндов во вмещающем выражении, поэтому
> (+ 2 (+ 3 4))
9

является вполне допустимой программой. Вложенные выражения
можно использовать в любом месте и в любых количествах:
> (+ 2 (+ (* 3 3) 4))
15

32

Пролог: как писать программы
> (+ 2 (+ (* 3 (/ 12 4)) 4))
15
> (+ (* 5 5) (+ (* 3 (/ 12 4)) 4))
38

И нет никаких ограничений на вложенность, кроме вашего терпения.
Естественно, выполняя вычисления, DrRa­cket использует правила,
которые известны вам из математики. Как и вы, он может определить результат сложения, только когда все операнды являются обычными числами. Если операнд представлен операторным выражением в скобках, начинающимся с открывающей скобки «(» и символа
операции, то DrRa­cket сначала вычислит результат этого вложенного
выражения. В отличие от вас, ему не приходится задумываться о том,
какое выражение вычислить первым, потому что это первое правило
есть единственное правило.
За удобства DrRa­cket приходится платить скрупулезным отношением к скобкам. Вы должны ввести все необходимые круглые скобки,
и при этом не должно быть лишних скобок. Например, ваш учитель
математики может терпимо относиться к присутствию лишних скобок, но это не относится к BSL. Выражение (+ (1) (2)) содержит лишние круглые скобки, и DrRa­cket однозначно сообщит вам об этом:
> (+ (1) (2))
function call:expected a function after the open parenthesis,
found a number

(вызов функции: после открывающей круглой скобки ожидается
функция, а обнаружено число).
Однако, привыкнув к особенностям языка BSL, вы увидите, что эта
цена не так уж высока. Во-первых, вы можете использовать операции
сразу с несколькими операндами, например:
> (+ 1 2 3 4 5 6 7 8 9 0)
45
> (* 1 2 3 4 5 6 7 8 9 0)
0

Если вы не знаете, что делает операция с несколькими
операндами, введите пример в области взаимодействия
и нажмите return; DrRa­cket позволяет экспериментировать
и узнавать, работает ли тот или иной прием, и как. Или обратитесь к документации HelpDesk. Во-вторых, читая программы, написанные другими, вам никогда не придется задаваться
вопросом, какие выражения вычисляются в первую очередь. Круглые
скобки и вложенность сразу скажут вам об этом.
В этом контексте «программировать» значит записывать понятные
арифметические выражения, а «вычислять» – определять их значение. С DrRa­cket вы легко освоите этот вид программирования и вычислений.

Как можно заметить,
в онлайн-версии книги
названия операций связаны с документацией
в HelpDesk.

Пролог: как писать программы

33

Арифметика, арифметика...
Если бы в программировании использовались только числа Шучу: математика –
и арифметические операции, то этот вид деятельности был увлекательнейший
бы таким же скучным, как уроки математики. К счастью, предмет, но нам она
пока не особенно нужна.
в программировании можно использовать не только числа,
но также текст, флаги истинности, изображения и многое другое.
Прежде всего вы должны запомнить, что текст в BSL – это любая
последовательность символов, введенных с клавиатуры, заключенная в двойные кавычки ("). Мы называем это строкой. То есть "hello world" – это типичная строка, и, «вычисляя» такие строки, DrRa­cket
просто выводит их в области взаимодействий, как число:
> "hello world"
"hello world"

Многие люди пишут свои первые программы, которые выводят
именно эту строку.
Вам также необходимо знать, что DrRa­cket поддерживает не только
арифметику чисел, но и арифметику строк. Вот два примера, иллюст­
рирующих эту форму арифметики:
> (string-append "hello" "world")
"helloworld"
> (string-append "hello " "world")
"hello world"

string-append, как и +, тоже является оператором; он создает новую
строку, объединяя все строки, следующие за ним. Как показывает
первое взаимодействие, string-append объединяет строки буквально,
не добавляя ничего между ними: ни пробелов, ни запятых, ничего.
Поэтому если вы хотите увидеть фразу "hello world", то должны сами
добавить пробел к одному из этих слов; именно это показывает второе взаимодействие. Конечно, самый естественный способ составить
фразу из двух слов – ввести
(string-append "hello" " " "world")

потому что string-append, так же как +, может обрабатывать любое количество операндов.
Со строками можно выполнять не только сложение. Вы Открыть HelpDesk
можете извлекать фрагменты из строк, переворачивать их, можно, нажав
F1 или выбрав
преобразовывать все буквы в верхний (или нижний) регистр, клавишу
соответствующий
удалять пробелы слева и справа и т. д. И что самое важное, пункт в контекстном
вам не нужно ничего запоминать. Чтобы узнать, какие опе- меню. Загляните
рации можно выполнять со строками, достаточно поискать в руководство по языку
BSL, в раздел с описав HelpDesk.
нием предопределенных
Заглянув в раздел с описанием простейших операций, до- операций, особенно
ступных в BSL, можно увидеть, что простейшие (иногда их операций со строками.

34

Пролог: как писать программы

называют предопределенными или встроенными) операции могут потреблять строки и производить числа:
> (+ (string-length "hello world") 20)
31
> (number->string 42)
"42"

Также есть операция, преобразующая строку в число:
> (string->number "42")
42

Если вы ожидали увидеть в результате строку «forty-two» («сорок
два») или что-то в этом роде, то извините: строковый калькулятор –
не совсем то, что вам нужно.
Тем не менее последнее выражение вызывает вопрос: что получится, если применить операцию string->number к строке, которая не
является изображением числа в кавычках? В этом случае операция
вернет результат другого типа:
> (string->number "hello world")
#false

Это не число и не строка; это логическое значение. В отличие от
чисел и строк, логические значения бывают только двух видов: #true
и #false. Первое обозначает истину, а второе – ложь. В DrRa­cket имеется несколько операций для объединения логических значений:
> (and #true #true)
#true
> (and #true #false)
#false
> (or #true #false)
#true
> (or #false #false)
#false
> (not #false)
#true

возвращающих результаты, которые соответствуют названиям операций. (Не знаете, что означают операции and, or и not? Все просто:
(and x y) вернет истину, если x и y истинны; (or x y) вернет истину, если
либо x, либо y, либо оба истинны; и (not x) вернет истину, только если
x ложно.)
Иногда бывает полезно «преобразовать» два числа в логическое
значение:
> (> 10 9)
#true
> (< -1 0)
#true
> (= 42 9)
#false

35

Пролог: как писать программы

Стоп! Попробуйте выполнить следующие три выражения: (> = 10
10), (number "11"))
(string=? "hello world" "good morning"))
(>= (+ (string-length "hello world") 60) 80))

Что получится в результате вычисления данного выражения? Как
вы это поняли? Вы сами догадались об этом? Или просто ввели выражение в области взаимодействий и нажали клавишу return? Если вы
поступили именно так, то как вы думаете, вы смогли бы определить
результат самостоятельно? В конце концов, если вы не научитесь
предсказывать результат, возвращаемый DrRa­cket для небольших выражений, вы не сможете доверять результатам вычислений Для вставки
более сложных задач.
изображений в DrRa­cket,
Прежде чем приступать к изучению «настоящего» про- например изображения
граммирования, давайте обсудим еще один вид данных, ко- ракеты, используйте
меню Insert (Вставка).
торый поможет оживить процесс: изображения. Если вста- Или скопируйте
вить изображение в область взаимодействий и нажать кла- и вставьте изображение
из вашего браузера.
вишу return, вот так:
>

в ответ DrRa­cket выведет изображение. В отличие от многих других
языков программирования, BSL понимает изображения и поддерживает арифметику с изображениями, по аналогии арифметики с числами и строками. Проще говоря, ваши программы могут вы- Добавьте выражение
полнять вычисления с изображениями, и вы можете опери- (require 2htdp/image)
ровать ими в области взаимодействий. Более того, програм- в область определений
мисты на BSL, как и программисты на других языках про- или выберите пункт
Add Teachpack (Добаграммирования, создают библиотеки, которые могут ока- вить учебный пакет)
заться полезными для других. Использование таких библио- в меню Language (Язык)
тек напоминает расширение словаря новыми словами. Мы и выберите пакет image
называем такие библиотеки учебными пакетами, потому что в списке Preinstalled
HtDP/2e Teachpack
они помогают в обучении.
(Предустановленные
Одна из важнейших библиотек – 2htdp/image – поддержи- учебные пакеты
вает операции определения ширины и высоты изображения: HtDP/2e).
(* (image-width

) (image-height

))

После добавления библиотеки в программу щелчок на кнопке RUN
(Выполнить) даст вам число 1176 – площадь изображения с размерами
28 на 42.

36

Пролог: как писать программы

Вам не обязательно искать изображения в Google, чтобы вставлять
их в программы DrRa­cket с по­мощью меню Insert (Вставка). Можете
поручить DrRa­cket создавать простые изображения с нуля:
> (circle 10 "solid" "red")
> (rectangle 30 20 "outline" "blue")

Когда результатом выражения является изображение, DrRa­cket
рисует его в области взаимодействия. Но в остальном программа
BSL работает с изображениями как с данными, подобными числам.
В частности, в BSL есть операции для объединения изображений так
же, как и операции для сложения чисел или добавления строк:
> (overlay (circle 5 "solid" "red")
(rectangle 20 20 "solid" "blue"))

Наложение этих изображений в обратном порядке дает в результате сплошной синий квадрат:
> (overlay (rectangle 20 20 "solid" "blue")
(circle 5 "solid" "red"))

Давайте остановимся и на мгновение задумаемся над последним
результатом.
Как видите, операция overlay больше похожа на string-append, чем
на +: она «складывает» изображения так же, как string-append «складывает» строки, а + вычисляет сумму чисел. Вот еще одна иллюстрация:
> (image-width (square 10 "solid" "red"))
10
> (image-width
(overlay (rectangle 20 20 "solid" "blue")
(circle 5 "solid" "red")))
20

Эти взаимодействия с DrRa­cket вообще ничего не рисуют; они прос­
то измеряют ширину получившегося изображения.
Следует упомянуть еще две операции: empty-scene и place-image.
Первая создает сцену, особый вид прямоугольника, а вторая помещает изображение в эту сцену:
Не совсем так. На самом
деле в полученном
изображении отсутствует сетка.
Мы наложили сетку
на пустую сцену, чтобы
вы могли видеть, где
именно находится
зеленая точка.

(place-image (circle 5 "solid" "green")
50 80
(empty-scene 100 100))

В результате получается:

Пролог: как писать программы

Как можно видеть на этом изображении, начало координат (или
(0,0)) находится в верхнем левом углу. В отличие от геометрии, ось Y
направлена вниз, а не вверх. В остальном изображение показывает
именно то, что можно было ожидать: зеленый диск с центром в точке
с координатами (50,80) в пустом прямоугольнике 100 на 100.
Подведем некоторые итоги. Под программированием подразумевается запись арифметических выражений, но при этом вы не ограничены одними только скучными числами. В языке BSL под арифметикой подразумевается арифметика чисел, строк, логических значений и даже изображений. Однако под вычислением по-прежнему
подразумевается определение значения выражения, разве что это
значение может быть строкой, числом, логическим значением или
изображением.
Теперь вы готовы писать программы, которые заставляют ракеты
летать.

Входы и выходы
Программы, которые мы писали до сих пор, довольно незатейливы.
Мы записывали выражение или несколько выражений, щелкали на
кнопке RUN (Выполнить) и просматривали получившиеся результаты. Если снова щелкнуть на кнопке RUN (Выполнить), DrRa­cket выведет точно такие же результаты. Фактически вы можете щелкать на
кнопке RUN (Выполнить) сколько угодно раз, и результаты от этого
не изменятся. Проще говоря, наши программы больше похожи на
вычисления на карманном калькуляторе, с той лишь разницей, что
DrRa­cket может выполнять вычисления с любыми видами данных,
а не только с числами.
Это и хорошо, и плохо. Хорошо, потому что программирование
и вычисления являются естественным обобщением калькулятора.
Плохо, потому что цель программирования – обрабатывать большое
количество данных и получать много разных результатов, выполняя
более или менее одинаковые вычисления. (Программы также должны
вычислять эти результаты быстро, по крайней мере быстрее, чем мы.)
То есть вам нужно еще многое узнать, прежде чем вы научитесь программировать. Но не волнуйтесь: обладая знаниями об арифметике
чисел, строк, логических значений и изображений, вы почти готовы
написать программу, которая создает фильмы, а не просто выводит
какое-нибудь простенькое сообщение, такое как «hello world». И этим
мы займемся дальше.
На всякий случай, если вы не знали этого, фильм – это последовательность изображений, которые быстро сменяют друг друга на экране. Если бы ваши учителя математики знали об «арифметике изображений», которую вы видели в предыдущем разделе, то наверняка
научили бы вас создавать фильмы вместо скучных числовых последовательностей. Вот еще одна такая таблица:

37

38

Пролог: как писать программы

x=
y=

1
1

2
4

3
9

4
16

5
25

6
36

7
49

8
64

9
81

10
?

Ваши учителя могли бы попросить вас вставить недостающее число в ячейку со знаком вопроса «?».
Как оказывается, снять фильм не сложнее, чем заполнить такую
таб­лицу чисел. Действительно, все дело в таких таблицах:
x=
y=

1

2

3

4
?

Говоря более конкретно, ваш учитель мог бы предложить вам нарисовать четвертое, пятое и 1273-е изображения, потому что фильм –
это просто длинная последовательность изображений, сменяющих
друг друга примерно 20 или 30 раз в секунду. То есть вам понадобится
от 1200 до 1800 таких изображений, чтобы из них сконструировать
фильм продолжительностью в одну минуту.
Вы также можете вспомнить, что ваши учителя могли просить не
только вставить четвертое или пятое число в некоторой последовательности, но и указать выражение, определяющее любой элемент
последовательности по заданному значению x. В числовом примере
учитель мог бы пожелать увидеть что-то вроде этого:
y = x · x.
Если в эту формулу вместо x подставить 1, 2, 3 и т. д., то в результате
получатся числа 1, 4, 9 и т. д., в точности как показано в таблице. Для
последовательности изображений то же самое можно выразить примерно так:
y = изображение, содержащее точку на x2 пикселей
ниже верхнего края.
Важно отметить, что эта формулировка обозначает не простое выражение, но функцию.
На первый взгляд функции похожи на выражения с символом y
слева, за которым следует знак = и выражение. Однако это не выражения, а функции, которые вы могли часто видеть в школе на уроках
математики. Но в DrRa­cket функции записываются немного иначе:
(define (y x) (* x x))

Слово define говорит: «считать y функцией», которая вычисляет значение подобно выражению. Однако значение функции зависит от
значения того, что называется входом. Этот факт мы выражаем с по­
мощью (y x). Поскольку входное значение заранее неизвестно, то для
его представления мы используем имя. Здесь, следуя математиче-

39

Пролог: как писать программы

ской традиции, мы использовали имя x для обозначения неизвестного входа; но довольно скоро мы будем использовать другие имена.
Эта вторая часть означает, что в функцию нужно передать одно
число – для x, – чтобы вычислить конкретное значение для y. В этом
случае DrRa­cket подставит полученное значение x в выражение, связанное с функцией, в данном примере это выражение (* x x). После
замены x значением, например 1, DrRa­cket сможет вычислить результат выражения, который также называется выходом функции.
Щелкните на кнопке RUN (Выполнить) и обратите внимание, что
ничего не произошло. В области взаимодействия не появилось ничего нового, как будто вы ничего и не вводили и в DrRa­cket ничего не
изменилось. Но на самом деле это не так. Вы фактически определили
функцию и сообщили DrRa­cket о ее существовании. Теперь редактор
готов использовать эту функцию. Введите
(y 1)

в области взаимодействий и убедитесь, что в ответ DrRa­cket
вывел число 1. В DrRa­cket выражение (y 1) называется применением функции. Попробуйте выполнить

В математике запись y(1)
тоже называется применением функции, просто
ваши учителя забыли вам
сказать об этом.

(y 2)

и убедитесь, что в ответ DrRa­cket вывел 4. Конечно, все эти выражения можно также ввести в области определений и щелкнуть на кнопке RUN (Выполнить):
(define (y x) (* x x))
(y 1)
(y 2)
(y 3)
(y 4)
(y 5)

В ответ DrRa­cket выведет: 1 4 9 16 25 – числа из таблицы. Теперь
определите недостающее число.
С нашей точки зрения функции дают весьма экономичный способ вычисления множества интересующих нас значений с по­мощью
одного выражения. В действительности программы – это функции;
и, освоив функции, вы будете знать о программировании почти все,
что нужно. Учитывая важность функций, давайте обобщим то, что мы
уже знаем о них.
zz Во-первых,
(define (ИмяФункции ИмяВхода) Тело)

– это определение функции. Об этом говорит ключевое слово define (определить). По сути, определение состоит из трех частей:
двух имен и выражения. Первое имя – это имя функции. Вы
будете использовать его, когда вам понадобится применить

40

Пролог: как писать программы

функцию. Второе имя, называемое параметром, – это вход
функции, который неизвестен до фактического применения
функции. Выражение с именем Тело вычисляет выход (результат) функции для определенного входа.
zz Во-вторых,
(ИмяФункции ВыражениеАргумента)

– это применение функции. Первая часть сообщает DrRa­cket,
какую функцию следует применить, а вторая часть – это вход,
к которому применяется функция. Если бы вы сейчас читали
руководство для Windows или Mac, в нем было бы написано, что
это выражение запускает приложение с именем ИмяФункции, которое получает на вход значение ВыражениеАргумента и обрабатывает его. Как всякое другое выражение, ВыражениеАргумента может
быть простым фрагментом данных или глубоко вложенным
выражением.
Функции могут принимать и возвращать не только числа, но и все
остальные виды данных. Давайте проверим это и создадим функцию,
имитирующую вторую таблицу, с изображениями цветной точки, подобно тому, как первая функция имитировала числовую таблицу. Поскольку в школе вам не рассказывали, как в выражениях создавать
изображения, начнем с простого. Помните пустую сцену? Мы кратко
упоминали о ней в конце предыдущего раздела. Если создать ее в области взаимодействий, как показано ниже:
> (empty-scene 100 60)

то DrRa­cket нарисует пустой прямоугольник, который называется сценой. В сцену можно добавлять изображения с по­мощью place-image:
> (place-image

50 23 (empty-scene 100 60))

Представьте, что ракета – это точка на рисунках, показанных в таб­
лице выше. Разница лишь в том, что видеть ракету интереснее.
Теперь вы должны заставить ракету опуститься, как точку в таблице выше. В предыдущем разделе вы узнали, как добиться этого эффекта: нужно увеличивать координату y, передаваемую в place-image:
> (place-image

50 20 (empty-scene 100 60))

> (place-image

50 30 (empty-scene 100 60))

Пролог: как писать программы

> (place-image

41

50 40 (empty-scene 100 60))

Теперь осталось только определить способ, который позволит
с легкостью создать множество таких сцен, и быстро отобразить по
порядку.
Листинг 1. Посадка ракеты (версия 1)
(define (picture-of-rocket height)
(place-image

50 height (empty-scene 100 60)))

Первую цель можно достичь с по­мощью функции в лис­ Язык BSL позволяет
тинге 1. Да, это определение функции. Вместо y ей дано использовать в именах
имя picture-of-rocket, которое ясно сообщает, что выводит любые символы, включая
«-» и «.».
функция: сцену с ракетой. Вместо x параметру в определении функции дано имя height, которое четко сообщает, что это число,
которое определяет высоту местоположения ракеты. Выражение с телом функции в точности повторяет выражения, с которыми мы только что экспериментировали, за исключением того, что в нем вместо
числа используется height. Теперь мы легко можем создать все необходимые изображения с по­мощью одной функции:
(picture-of-rocket
(picture-of-rocket
(picture-of-rocket
(picture-of-rocket

0)
10)
20)
30)

Введите эти выражения в области определений или в области взаимодействий, и вы получите ожидаемые сцены.
Для достижения второй цели вы должны познакомиться Не забудьте добавить
с одной элементарной операцией из библиотеки 2htdp/uni- библиотеку 2htdp/
universe в область
verse: animate. Щелк­ните на кнопке RUN (Выполнить) и вве- определений.
дите следующее выражение:
> (animate picture-of-rocket)

Обратите внимание, что выражение аргумента в этом примере –
функция. Не беспокойтесь об использовании функций в качестве аргументов; этот прием прекрасно работает с animate, но пока не пытайтесь сами определять такие функции, как animate.
Как только вы нажмете клавишу return, DrRa­cket вычислит выражение, но не отобразит ни результата, ни даже приглашения к вводу.
Вместо этого откроется другое окно – холст – и запустятся часы, тикающие 28 раз в секунду. С каждым тактом часов DrRa­cket будет применять picture-of-rocket к количеству тактов, прошедших с момента вызова animate. Результаты применения этой функции будут отобра-

42

Пролог: как писать программы

жаться на холсте и создавать эффект анимационного фильма. Моделирование продолжается до тех пор, пока вы не закроете окно. После
этого animate вернет количество обработанных тактов.
Но откуда берутся изображения в окне? Если в двух словах,
В упражнении 298
объясняется, как то: animate применяет функцию в своем операнде к числам 0,
организована функция 1, 2 и т. д. и отображает полученные изображения. Вот более
animate. подробное объяснение:
zz animate запускает часы и считает количество тактов;
zz часы идут со скоростью 28 тактов в секунду;
zz каждый раз, когда завершается очередной такт, animate приме-

няет функцию picture-of-rocket к порядковому номеру текущего такта; и
zz сцена, созданная в результате этого применения, отображается
на холсте.
Это означает, что ракета сначала появляется на высоте 0, затем 1,
потом 2 и т. д., то есть постепенно опускается вниз. Наша трехстрочная программа создает около 100 изображений примерно за 3,5 секунды, а быстрое их отображение создает эффект приземления ракеты.
А теперь обобщим все, что вы узнали в этом разделе. Функции –
это удобный способ обработки больших объемов данных за короткое
время. Вы можете запустить функцию вручную, передав несколько
разных входов, чтобы проверить правильность выходных результатов. Это называется тестированием функции. DrRa­cket может запус­
тить функцию для множества входов с по­мощью некоторых библио­
тек. Естественно, DrRa­cket может также запускать функции, когда
вы нажимаете клавиши на клавиатуре или манипулируете мышью.
Чтобы узнать, как это сделать, продолжайте читать. И независимо от
того, как запускается применение функции, помните, что программы
(простые) – это функции.

Множество способов вычисления
Если запустить (animate picture-of-rocket), спустя какое-то время ракета исчезнет под землей. Это выглядит странно. Ракеты в старых
фантастических фильмах не тонут в земле; они изящно приземляются на опоры, и на этом фильм должен заканчиваться.
Эта идея предполагает, что в зависимости от ситуации вычисления должны выполняться по-разному. В нашем примере программа
picture-of-rocket должна работать «как есть», пока ракета находится
в полете. Однако как только ракета коснется нижней части холста, ее
дальнейшее снижение должно остановиться.
Эта идея не должна быть для вас новой. Даже ваши учителя математики показывали вам функции, различающие разные ситуации:

43

Пролог: как писать программы

Эта функция sign различает три вида входных значений: которые
больше 0, равны 0 и меньше 0. В зависимости от входа результат
функции равен +1, 0 или –1.
Эту функцию легко определить в DrRa­cket, используя условное выражение cond:
(define (sign
(cond
[(> x 0)
[(= x 0)
[(< x 0)

x)
1]
0]
-1]))

После щелчка на кнопке RUN (Выполнить) вы сможете использовать функцию sign как любую другую функцию:

Откройте новую
вкладку в DrRa­cket и начните с чистого листа.

> (sign 10)
1
> (sign -5)
-1
> (sign 0)
0

В общем случае условное выражение имеет следующий
вид:
(cond
[ВыражениеУсловия1 ВыражениеРезультата1]
[ВыражениеУсловия2 ВыражениеРезультата2]
...
[ВыражениеУсловияN ВыражениеРезультатаN])

Сейчас самое время
познакомиться с кнопкой
STEP (Шаг). Введите
(sign -5) в области
определений (после
ввода определения функции sign) и щелкните на
кнопке STEP (Шаг).
Когда появится
новое окно, попробуйте
пощелкать на кнопках
со стрелками влево
и вправо.

То есть условное выражение cond состоит из некоторого
необходимого количества условных строк. Каждая строка
содержит два выражения: левое обычно называют условием,
а правое – результатом; иногда также используются термины вопрос и ответ. Чтобы вычислить выражение cond, DrRa­cket вычисляет первое выражение условия, ВыражениеУсловия1. Если оно возвращает #true, то DrRa­cket заменяет все выражение cond выражением
результата ВыражениеРезультата1, вычисляет его и получившееся значение возвращает как результат всего выражения cond. Если в результате вычисления ВыражениеУсловия1 получится #false, то DrRa­cket пропускает первую строку и переходит ко второй. Если все выражения
условий вернут #false, то DrRa­cket сообщит об ошибке.
Теперь, зная это, вы можете изменить ход процесса. Цель состоит
в том, чтобы не дать ракете опуститься ниже уровня земли в сцене
размером 100 на 60 пикселей. Поскольку функция picture-of-rocket
принимает высоту, на которой она должна изобразить ракету в сцене,

44

Пролог: как писать программы

кажется, что достаточно просто сравнить заданную высоту с максимально допустимой.
В листинге 2 приводится уточненное определение функции. В нем
определяется функция с именем picture-of-rocket.v2, чтобы мы могли различать две версии. Применение разных имен также позволяет
использовать обе функции в области взаимодействий и сравнивать
результаты.
Листинг 2. Посадка ракеты (версия 2)
(define (picture-of-rocket.v2 height)
(cond
[( height 60)
(place-image

50 60
(empty-scene 100 60))]))

Вот как работает оригинальная версия:
> (picture-of-rocket 5555)

А так – вторая:
> (picture-of-rocket.v2 5555)

Какое бы число вы не передали функции picture-of-rocket.v2, если
оно окажется больше 60, то вы получите ту же сцену. Если, к примеру,
выполнить такое выражение:
> (animate picture-of-rocket.v2)

то ракета опустится вниз и на половину своего корпуса уйдет под
землю, после чего остановится.
Стоп! Это именно то, что мы хотели увидеть?
Ракета, погрузившаяся наполовину под землю, смотрится некрасиво. Но вы знаете, как исправить этот огрех. Как вы уже видели,
язык BSL поддерживает арифметику изображений. Когда функция
place-image добавляет изображение в сцену, она ориентируется на его
центр, как если бы все изображение было представлено точкой, даже
если оно имеет реальную высоту и реальную ширину. Как вы знаете,
мы можем измерить высоту изображения с по­мощью image-height. Эта
функция пригодится нам и поможет остановить спуск ракеты в тот
момент, когда ее нижняя часть коснется земли.
Сложив два плюс два, нетрудно догадаться, что высота, на которой
ракета должна прекратить спуск, вычисляется так:

45

Пролог: как писать программы
(- 60 (/ (image-height

) 2))

Вы можете убедиться в этом, поиграв с самой программой или поэкспериментировав в области взаимодействий.
Вот первая попытка:
(place-image

50 (- 60 (image-height
(empty-scene 100 60))

))

Теперь замените третий аргумент в примере выше выражением
(- 60 (/ (image-height

) 2))

Стоп! Поэкспериментируйте сами и оцените полученные результаты. Какой из них вам больше нравится?
Листинг 3. Посадка ракеты (версия 3)
(define (picture-of-rocket.v3 height)
(cond
[( height (- 60 (/ (image-height
(place-image

) 2)))

) 2)))

50 (- 60 (/ (image-height
(empty-scene 100 60))]))

) 2))

Размышляя и экспериментируя, вы в конечном итоге дойдете до
программы в листинге 3. Если задано какое-то число, представляющее высоту местоположения ракеты, то сначала проверяется, достиг
ли нижний край ракеты на земле. Если не достиг, то местоположение
ракеты в сцене меняется, как и раньше. Иначе изображение ракеты
позиционируется так, чтобы ее нижняя часть касалась земли.

Одна программа, множество определений
Теперь предположим, что ваши друзья посмотрели анимацию и им не
понравился размер холста. Они попросили дать им версию, в которой
сцена имеет размер 200×400. Эта простая просьба заставит вас заменить 100 на 400 в пяти местах программы и 60 на 200 еще в двух местах,
не говоря уже о числе 50, обозначающем «середину холста».
А теперь попробуйте проделать это, чтобы понять, насколько сложно выполнить данную просьбу для простенькой пятистрочной программы. Читая дальше, имейте в виду, что в мире не редкость программы, состоящие из 50 000, 500 000 или даже 5 000 000 или более
строк программного кода.
В идеальной программе подобные просьбы, такие как изменение
размеров холста, не должны требовать вносить такое большое коли-

46

Пролог: как писать программы

чество изменений. В BSL этого легко добиться с по­мощью define. Эта
инструкция способна определять не только функции, но также конс­
танты, присваивая имена некоторым значениям. Вот как выглядит
определение константы в общем виде:
(define Имя Выражение)

То есть вы можете добавить в программу такое определение:
(define HEIGHT 60)

а в программе использовать HEIGHT везде, где прежде использовалось
число 60. Смысл такого определения очевиден. Каждый раз, встретив
имя HEIGHT во время вычислений, DrRa­cket будет заменять его числом 60.
Теперь взгляните на код в листинге 7, который реализует это прос­
тое изменение, а также присваивает имя изображению ракеты. Скопируйте эту программу в DrRa­cket, щелкните на кнопке RUN (Выполнить) и введите следующее выражение в области взаимодействий:
> (animate picture-of-rocket.v4)

Убедитесь, что программа работает так же, как и раньше.
Программа в листинге 4 включает четыре определения: одно определение функции и три определения констант. Числа 100 и 60 встречаются в программе всего один раз – в определениях констант WIDTH
и HEIGHT. Вы также могли заметить, что обновленная программа использует имя h вместо height для параметра функции picture-of-ro­
cket.v4. Строго говоря, в этом изменении нет особой необходимости,
потому что DrRa­cket не спутает height и HEIGHT, но мы сделали это, чтобы не сбить с толку вас.
Вычисляя выражение (animate picture-of-rocket.v4), DrRa­cket заменяет HEIGHT на 60, WIDTH на 100 и ROCKET на изображение ракеты каждый
раз, когда встречает эти имена. Чтобы испытать радость настоящих
программистов, замените число 60 в определении HEIGHT на 400 и щелк­
ните на кнопке RUN (Выполнить). Вы увидите приземляющую­ся ракету в сцене размером 100 на 400 пикселей. Чтобы увеличить высоту
сцены, потребовалось всего одно небольшое изменение!
Листинг 4. Посадка ракеты (версия 4)
(define (picture-of-rocket.v4 h)
(cond
[( h (- HEIGHT (/ (image-height ROCKET) 2)))
(place-image ROCKET
50 (- HEIGHT (/ (image-height ROCKET) 2))
(empty-scene WIDTH HEIGHT))]))
(define WIDTH 100)
(define HEIGHT 60)
(define ROCKET

)

Пролог: как писать программы

Выражаясь современным языком, вы только что выполнили свой
первый рефакторинг программы. Каждый раз, реорганизуя свою
программу, чтобы подготовиться к возможным просьбам изменить
что-нибудь в ней, вы выполняете рефакторинг программы. Добавьте
этот пункт в свое резюме. Он смотрится неплохо, и вашему будущему
работодателю, вероятно, понравится читать такие модные словечки,
даже если это не делает вас хорошим программистом. Однако хороший программист никогда не смирится с наличием в программе трех
одинаковых выражений:
(- HEIGHT (/ (image-height ROCKET) 2))

Каждый раз, когда ваши друзья и коллеги будут читать эту программу, им придется приостанавливаться, чтобы понять, что вычисляет
это выражение – расстояние между верхним краем холста и цент­
ральной точкой ракеты, покоящейся на земле. Каждый раз, вычисляя
эти выражения, DrRa­cket должен выполнить три шага: (1) определить
высоту изображения; (2) разделить ее на 2 и (3) вычесть результат из
HEIGHT. И каждый раз будет получаться одно и то же число.
Это наблюдение требует от нас добавить еще одно определение:
(define ROCKET-CENTER-TO-TOP
(- HEIGHT (/ (image-height ROCKET) 2)))

Теперь подставьте ROCKET-CENTER-TO-TOP вместо выражения (- HEIGHT
(/ (image-height ROCKET) 2)) в остальной части программы. Возможно,
вас волнует вопрос: где разместить это определение – выше или ниже
определения HEIGHT? Или в общем случае: имеет ли значение порядок следования определений? Ответ заключается в следующем: для
определений констант порядок имеет значение, а для определений
функций – нет. Встретив определение константы, DrRa­cket вычисляет выражение в определении, а затем связывает имя константы с полученным результатом. Например, следующая последовательность
определений:
(define HEIGHT (* 2 CENTER))
(define CENTER 100)

вызовет сообщение об ошибке «CENTER is used before its definition»
(константа CENTER используется до ее определения), когда DrRa­cket
встретит определение константы HEIGHT.
Если переупорядочить определения:
(define CENTER 100)
(define HEIGHT (* 2 CENTER))

они будут вычислены без ошибок. Здесь DrRa­cket сначала свяжет имя
CENTER с числом 100, а затем вычислит выражение (* 2 CENTER) и получит в результате число 200, которое благополучно свяжет с именем
HEIGHT.

47

48

Пролог: как писать программы

Программа также
может содержать
однострочные комментарии, начинающиеся
с точки с запятой (;).
DrRa­cket игнорирует
такие комментарии,
но люди, читающие
программы, не должны
этого делать, потому
что комментарии предназначены для людей.
Это «канал обратной
связи» между автором
программы и всеми ее
читателями в будущем
для передачи информации о программе.

Порядок определений констант имеет значение, но совершенно не важно, где поместить определения констант
относительно определений функций. Если ваша программа включает множество определений функций, их порядок тоже не имеет значения, хотя лучше сначала ввести все
определения констант, а затем определения функций в порядке убывания важности. Начав писать свои программы
с множеством определений, вы поймете, почему этот порядок важен.
После устранения всех повторяющихся выражений вы получите программу, показанную в листинге 5. Она состоит из
одного определения функции и пяти определений констант.
Кроме положения центра ракеты, эти константы определяют также размеры самого изображения и сцены.

Листинг 5. Посадка ракеты (версия 5)
; константы
(define WIDTH 100)
(define HEIGHT 60)
(define MTSCN (empty-scene WIDTH HEIGHT))
(define ROCKET )
(define ROCKET-CENTER-TO-TOP
(- HEIGHT (/ (image-height ROCKET) 2)))
; функции
(define (picture-of-rocket.v5 h)
(cond
[( h ROCKET-CENTER-TO-TOP)
(place-image ROCKET 50 ROCKET-CENTER-TO-TOP MTSCN)]))

Прежде чем продолжить чтение, подумайте о следующих изменениях в вашей программе:
zz Как бы вы изменили программу, чтобы создать сцену размером
200×400?
zz Как бы вы изменили программу, чтобы она демонстрировала
приземление зеленого НЛО (неопознанного летающего объекта)? Нарисовать НЛО легко:
(overlay (circle 10 "solid" "green")
(rectangle 40 4 "solid" "green"))

zz Как бы вы изменили программу, чтобы фон сцены окрасить

в синий цвет?

zz Как бы вы изменили программу, чтобы ракета приземлялась

на плоскую каменную площадку, которая на 10 пикселей выше
уровня земли? Не забудьте также добавить эту площадку в сцену.
Практика – лучший способ изучения. Поэтому не останавливайтесь
и просто сделайте это.

49

Пролог: как писать программы

Магические числа. Взгляните еще раз на функцию picture-of-rocket.v5. Мы убрали из определения функции все повторяющиеся выражения и все числа, кроме одного – числа 50. В мире программирования такие числа называют магическими числами, и большинство
программистов не любят их. По прошествии времени легко забыть,
какую роль играет число и можно ли его изменить. Лучше всего для
таких чисел определить отдельные константы.
В данном случае мы знаем, что 50 – это выбранная нами координаты x для ракеты. Несмотря на то что число 50 не похоже на выражение, в действительности оно является повторяющимся выражением.
Таким образом, у нас есть две причины исключить 50 из определения
функции, и мы предлагаем вам сделать это самостоятельно.

Еще одно определение
Напомним, что animate фактически применяет переданные
ей функции к количеству тактов часов, прошедших с момента первого вызова. То есть аргументом для picture-of-rocket
является не высота, а время. В наших предыдущих определениях picture-of-rocket использовалось неправильное имя
для аргумента функции; вместо h (сокращенно от «height» –
высота) следует использовать t (сокращенно от «time» –
время):

Будьте внимательны!
В этом разделе
используются некоторые знания из физики.
Если вас пугает физика,
пропустите этот
раздел при первом чтении; программиро­вание
не требует знаний
физики.

(define (picture-of-rocket t)
(cond
[( t ROCKET-CENTER-TO-TOP)
(place-image ROCKET
50 ROCKET-CENTER-TO-TOP
MTSCN)]))

Это небольшое изменение в определении сразу же проясняет, что
эта программа использует время, как если бы оно было расстоянием.
А это нехорошо.
Даже если вы пропускали уроки физики в школе, вы наверняка
знаете, что время – это не расстояние. Так что наша программа работала по чистой случайности. Но не волнуйтесь, этот недостаток легко
исправить. Для этого нужно немного знать ракетостроение, которое
такие люди, как мы, называют физикой.
Физика?!? Возможно, вы уже забыли, чему вас учили на уроках физики. Или даже пропускали их, потому что были слишком молоды и вет­
рены. Но не волнуйтесь. Такое случается даже с лучшими программистами, потому что им приходится помогать людям, занимающимся
музыкой, экономикой, фотографией, медициной и многими другими
дисциплинами. Очевидно, что никакой программист не может знать
всего этого. Поэтому они или ищут необходимые знания, или разгова-

50

Пролог: как писать программы

ривают со специалистами. И если вы поговорите с физиком, то узнаете, что пройденное расстояние пропорционально времени:
d = v · t.
То есть объект, движущийся со скоростью v, за t секунд переместится на d километров (метров, пикселей и т. п.).
Конечно, учитель должен показать вам правильное определение
функции:
d(t) = v · t,
потому что оно сразу говорит, что значение d зависит от t, а v является
константой. Программисты обычно делают еще один шаг и заменяют
однобуквенные сокращения осмысленными именами:
(define V 3)
(define (distance t)
(* V t))

Этот фрагмент программы включает два определения: функцию
distance, которая вычисляет расстояние, пройденное объектом, который движется с постоянной скоростью, и константу V, описывающую
скорость.
Вы можете задаться вопросом: почему для скорости V здесь определено значение 3? Какой-то особой причины нет, просто мы посчитали, что 3 пикселя за такт – это хорошая скорость. Вы можете не согласиться с нами. Поэкспериментируйте с этим числом и посмотрите, что из этого получится.
Листинг 6. Посадка ракеты (версия 6)
; свойства "мира" и садящейся ракеты
(define WIDTH 100)
(define HEIGHT 60)
(define V 3)
(define X 50)
; константы, связанные с графикой
(define MTSCN (empty-scene WIDTH HEIGHT))
(define ROCKET )
(define ROCKET-CENTER-TO-TOP
(- HEIGHT (/ (image-height ROCKET) 2)))
; функции
(define (picture-of-rocket.v6 t)
(cond
[( (distance t) ROCKET-CENTER-TO-TOP)
(place-image ROCKET X ROCKET-CENTER-TO-TOP MTSCN)]))
(define (distance t)
(* V t))

Пролог: как писать программы

Теперь можно еще раз исправить picture-of-rocket. Вместо сравнения t с высотой функция будет использовать выражение (distance
t), вычисляющее высоту местоположения ракеты. Окончательная
программа показана в листинге 6. Она включает определения двух
функций: picture-of-rocket.v6 и distance. Остальные определения
констант делают определения функций легко читаемыми и изменяемыми. Как обычно, эту программу можно запустить с по­мощью
animate:
> (animate picture-of-rocket.v6)

По сравнению с предыдущими версиями, эта версия picture-of-rocket показывает, что программа может состоять из нескольких определений функций, ссылающихся друг на друга. Кроме того, даже в первой версии использовались функции + и / – просто вы думали, что они
встроены в BSL.
Когда вы станете настоящим программистом, то обнаружите, что
программы состоят из множества определений функций и множест­
ва определений констант. Вы также увидите, что функции все время
ссылаются друг на друга. И ваша задача – организовать их так, чтобы
вы могли легко читать эти определения даже спустя несколько месяцев после завершения работы над ними. В конце концов, вы или ктото другой может пожелать внести изменения в эти программы, и если
вы не сможете понять организацию программы, вам будет сложно
выполнить даже самую простую задачу.

Теперь вы – программист
Это утверждение может стать для вас неожиданностью, но это правда. Теперь вы знаете всю механику языка BSL. Вы знаете, что в программировании используется арифметика чисел, строк, изображений
и любых других данных, поддерживаемых вашими языками программирования. Вы знаете, что программы состоят из определений
функций и констант. Вы знаете, как мы говорили выше, что все дело
в правильной организации этих определений. И последнее, но не менее важное: вы знаете, что DrRa­cket и учебные пакеты поддерживают
множество других функций, а документация в HelpDesk описывает
эти функции.
Вы можете подумать, что еще недостаточно знаете, чтобы писать
программы, реагирующие на нажатия клавиш, щелчки мыши и т. д.
И это правда. Кроме animate, библиотека 2htdp/universe содержит
множество других функций, которые подключают ваши программы
к клавиатуре, мыши, часам и другим механизмам в вашем компьютере. Более того, с ее помощью можно даже писать программы, способные связать ваш компьютер с любым другим компьютером, где бы
тот не находился. Так что это не проблема.

51

52

Пролог: как писать программы

Проще говоря, вы познакомились почти со всеми механизмами составления программ. Если вдобавок к этому вы познакомитесь со всеми доступными функциями, то сможете писать программы, играть
в интересные компьютерные игры, запускать анимацию или отслеживать бизнес-аккаунты. Вопрос в том, действительно ли это означает, что вы программист. Как вы думаете?
Не спешите перевернуть страницу. Подумайте!

Пролог: как писать программы

Нет!
Рассматривая полки в разделе «Программирование» в книжном магазине, можно увидеть множество книг, которые обещают превратить вас в программиста. Однако теперь, опробовав несколько первых примеров, вы, вероятно, понимаете, что это невозможно.
Механические навыки программирования – умение писать выражения, понятные компьютеру, знакомство с доступными функциями
и библиотеками и прочие подобные знания и умения – мало помогают в реальном программировании. Если бы это было не так, то вы
точно так же могли бы выучить иностранный язык, запомнив тысячу
слов из словаря и несколько правил из учебника грамматики.
Умение программировать – это гораздо больше, чем простое знание языка. Особенно важно помнить, что программисты создают
программы, которые другие люди смогут читать в будущем. Хорошая
программа отражает постановку задачи и ее важные понятия. Она
должна включать краткое описание самой себя и сопровождаться
примерами, иллюстрирующими это описание и связывающими программу со стоящей перед ней задачей. Наличие примеров гарантирует, что будущий читатель поймет, почему и как работает ваш код.
Проще говоря, умение программировать – это системный подход
к решению задач и передача этой системы в коде. Самое замечательное, что такой подход делает программирование доступным для
всех – он служит сразу двум хозяевам.
Остальная часть книги посвящена всему перечисленному; в ней
очень мало внимания уделяется механике DrRa­cket, BSL или библио­
тек. Книга показывает, как хорошие программисты размышляют
о задачах. И вы узнаете, что такой способ решения задач применим
и к другим жизненным ситуациям, таким как работа врачей, журналистов, юристов и инженеров.
И кстати, в остальной части книги используется другой тон, более
подходящий для серьезного обсуждения, чем в этом прологе.
Примечание о том, чего вы не найдете в этой книге. Вводные
книги по программированию, как правило, содержат много сведений
о любимых авторами прикладных дисциплинах: головоломках, математике, физике, музыке и т. д. И это естественно, потому что программирование используется во всех этих областях, но такие сведения одновременно отвлекают от сути программирования. Поэтому
мы постарались свести к минимуму использование знаний из других
областей и сосредоточиться на решении вычислительных задач.

53

I Д АННЫЕ
ФИКСИРОВАННОГО
РА ЗМЕРА

Всякий язык программирования включает язык данных и язык операций с данными. Первый всегда определяет некоторые формы атомарных данных для представления разнообразной информации,
и программист должен уметь составлять элементарные данные для
описания более сложных композиций. Второй язык определяет некоторые базовые операции с атомарными данными, и задача программиста состоит в том, чтобы научиться объединять эти операции
в программы, выполняющие желаемые вычисления. Для описания
комбинации этих двух частей языка программирования мы используем слово арифметика, потому что оно обобщает то, что вы узнали
во время учебы в школе.
Эта первая часть книги знакомит с арифметикой языка программирования BSL, с которым мы познакомились в прологе. От арифметики рукой подать до ваших первых простых программ, которые
в математике называют функциями. Но прежде чем вы это осознаете,
процесс написания программ будет казаться вам запутанным, и вы
будете искать способ упорядочить свои мысли. Мы приравниваем
«организацию мыслей» к проектированию, и эта первая часть книги
знакомит вас с систематическим подходом к проектированию программ.

1. Арифметика
Быстро пролистайте эту первую главу
и переходите ко второй.
А когда встретите
незнакомую вам
«арифметику»,
возвращайтесь сюда.

В прологе вы узнали, как записать выражение, знакомое вам с первого класса, следуя правилам языка BSL:
zz ввести «(»,
zz ввести имя операции op,

zz ввести аргументы, разделяя их пробелами, и
zz ввести «)».

Просто ради напоминания, вот простое выражение:
(+ 1 2)

Здесь используется операция + сложения, за которой следуют два
аргумента – обычные числа. А вот другой пример:
(+ 1 (+ 1 (+ 1 1) 2) 3 4 5)

Отметим два важных момента в этом втором примере. Во-первых,
операции могут принимать больше двух аргументов. Во-вторых, аргументы необязательно должны быть числами; они могут быть выражениями.
Вычисляются выражения просто. Сначала BSL вычисляет все аргументы операции, а затем передает полученные значения операции,
которая возвращает результат. Таким образом:
Мы используем ==,
чтобы сказать: «равно,
согласно законам
вычислений».

(+ 1 2)
==
3

и
(+ 1 (+ 1 (+ 1 1) 2) 3 (+ 2 2) 5)
==
(+ 1 (+ 1 2 2) 3 4 5)
==
(+ 1 5 3 4 5)
==
18

Эти вычисления должны быть вам знакомы, потому что подобные
вычисления вы производили на уроках математики. Кто-то мог бы
записать порядок вычислений иначе, если его не учили выстраивать
вычисления в правильной последовательности. Как бы то ни было,
BSL выполняет вычисления точно так же, как и вы, и этот факт должен
принести вам облегчение. Он гарантирует, что вы понимаете работу
элементарных операций с элементарными данными, поэтому есть
некоторая надежда, что вы сможете предсказать результаты, вычисляемые вашими программами. Вообще говоря, для программиста

Арифметика

57

важно знать, как выполняет вычисления выбранный им язык, потому
что иначе поведение программы может нанести ущерб людям, которые их используют.
В оставшейся части этой главы представлены четыре фор- В следующем томе
мы атомарных данных в языке BSL: числа, строки, изобра- «How to Design Components» мы расскажем,
жения и логические значения. Мы используем слово «ато- как проектировать
марный», следуя за аналогией с физикой. Вы не сможете атомарные данные.
заглянуть внутрь атомарных фрагментов данных, но у вас
есть функции, позволяющие объединить несколько фрагментов атомарных данных, извлечь их «свойства», так же в терминах атомарных данных, и т. д. В следующих разделах представлены некоторые
из этих функций, еще называемых примитивными, или предопределенными, операциями. Полный перечень функций, доступных в языке
BSL, вы найдете в документации, поставляемой с DrRa­cket.

1.1. Арифметика чисел
Услышав слово «арифметика», многие начинают думать о «числах»
и «операциях с числами». К «операциям с числами» можно отнести:
сложение двух чисел для получения третьего, вычитание одного числа из другого, определение наибольшего общего делителя двух чисел
и многие другие. Если не воспринимать слово «арифметика» слишком буквально, то в этот список можно также включить вычисление
синуса угла, округление действительного числа до ближайшего целого и т. д.
Язык BSL поддерживает числа и арифметику с ними. Как обсуждалось в прологе, арифметические операции, такие как +, используются
следующим образом:
(+ 3 4)

то есть в форме префиксной записи. Вот некоторые из операций с числами, которые поддерживает наш язык: +, -, *, /, abs, add1, ceiling, denominator, exact->inexact, expt, floor, gcd, log, max, numerator, quotient, random,
remainder, sqr и tan. Мы прошлись по всему алфавиту, чтобы показать
разнообразие операций. Загляните в документацию, чтобы узнать,
что они вычисляют и заодно – сколько вообще подобных операций
поддерживается.
Если вам понадобится операция с числами, знакомая вам по урокам математики, то, скорее всего, BSL поддерживает ее. Угадайте ее
название и поэкспериментируйте в области взаимодействий. Представим, что вам нужно вычислить синус некоторого угла. Вы могли бы
попробовать сделать это:
> (sin 0)
0

58

Глава 1

а потом долго и счастливо пользоваться своим открытием.
Но можно заглянуть в HelpDesk. Там вы обнаружите, что
кроме операций язык BSL также распознает имена некоторых широко используемых чисел, например pi и е.
Что еще можно сказать о числах? Программы на BSL могут использовать натуральные, целые, рациональные, действительные и комплексные числа. Мы уверены, что вы слышали обо
всех этих видах чисел, кроме последнего. Комплексные числа могли
упоминаться на уроках математики в старших классах средней школы. Если нет, то не волнуйтесь; несмотря на несомненную пользу
комплексных чисел, новичкам необязательно знать о них.
По-настоящему важное различие касается точности чисел. На данный момент важно понимать, что BSL различает точные и неточные
числа. Когда в вычислениях участвуют точные числа, BSL старается сохранить точность. Например, (/ 4 6) дает точную дробь 2/3, которую
DrRa­cket может отобразить в виде правильной, неправильной или
десятичной дроби. Поэкспериментируйте с компьютерной мышкой
и найдите пункт в меню, который заменяет дробь десятичной дробью.
Некоторые числовые операции BSL не могут дать точного результата. Например, операция sqrt над числом 2 дает иррациональное число,
которое нельзя описать конечным числом цифр. Поскольку компьютеры имеют ограничения в представлении данных, язык BSL вынужден учитывать эти ограничения и поэтому выводит приближенный
результат операции: 1.4142135623730951. Как упоминалось в прологе,
об этой неточности начинающих программистов предупреждает префикс #i. Однако большинство языков программирования предпочитают жертвовать точностью молча, лишь немногие сообщают о ней в документации и еще меньше предупреждают об этом программистов.

Возможно, вы знакомы
с числом е. Это
действительное число,
примерно 2,718, которое
называется
«постоянной Эйлера».

ПРИМЕЧАНИЕ О ЧИСЛАХ. Слово «число» относится к большому разнообразию чисел, включая натуральные, целые, рациональные, действительные и даже комплексные числа. В большинстве случаев слово «число» можно приравнять к «числовой прямой», известной вам
из начальной школы, хотя иногда такое сравнение не особенно точное. Для большей точности в выражении своих мыслей мы используем подходящие слова: целое, действительное и т. д. Мы можем даже
уточнять эти понятия, используя такие стандартные термины, как
положительное целое, неотрицательное число, отрицательное число
и т. д. КОНЕЦ.
Упражнение 1. Добавьте следующие определения для x и y в области определений в DrRa­cket:
(define x 3)
(define y 4)

Теперь представьте, что x и y – это координаты точки. Напишите
выражение, которое вычисляет расстояние от этой точки до начала
координат, то есть до точки с координатами (0,0).

Арифметика

Правильный результат для этих значений – число 5, но ваше выражение должно давать правильный результат даже после изменения
определений x и y.
На всякий случай, если вы не изучали геометрию или забыли формулу, напомним, что расстояние от точки (x,y) до начала координат
вычисляется как:

В конце концов, мы учим вас проектировать программы, а не готовим из вас геометров.
Лучший способ прийти к желаемому выражению – щелкнуть на
кнопке RUN (Выполнить) и поэкспериментировать в области взаимодействий. После щелчка на кнопке RUN (Выполнить) DrRa­cket определит текущие значения x и y, и вы сможете использовать их в своих
экспериментах с выражениями:
> x
3
> y
4
> (+ x 10)
13
> (* x y)
12

Получив выражение, которое дает правильный результат, скопируйте его из области взаимодействий в область определений.
Чтобы убедиться, что выражение работает правильно, замените
число 5 на 12 в определении x и число 4 на число 5 в определении y,
а затем щелкните на кнопке RUN (Выполнить). В результате должно
получиться число 13.
Ваш учитель математики сказал бы, что вы вычислили значение
по формуле расстояния. Чтобы использовать формулу с другими
входными значениями, нужно открыть DrRa­cket, отредактировать
определения x и y, подставив желаемые координаты, и щелкнуть на
кнопке RUN (Выполнить). Но такой способ повторного использования формулы расстояния слишком громоздкий и неудобный. Вскоре
мы покажем вам, как определять функции, которые упрощают повторное использование формул. А здесь мы просто использовали это
упражнение, чтобы привлечь внимание к идее функций и подготовить вас к программированию с их помощью. 

1.2. Арифметика строк
Существует распространенное предубеждение относительно внут­
реннего устройства компьютеров: многие считают, что все дело в битах и байтах – какими бы они ни были – и, возможно, в числах, пото-

59

60

Глава 1

му что все знают, что компьютеры предназначены для вычислений.
С одной стороны, это верно, и инженеры-электронщики должны использовать именно такое представление, но начинающие программисты и все остальные никогда не должны делать этого.
Языки программирования предназначены для выполнения вычислений с информацией, а информация может иметь любую форму.
Например, программа может работать с цветами, именами, деловыми письмами или бытовой перепиской между людьми. Даже если бы
мы могли кодировать такую информацию как числа, то это было бы
совершенно неправильно. Только представьте, что вам придется запомнить огромные таблицы с числовыми обозначениями, например
0 означает «красный», а 1 означает «привет» и т. д.
Вместо этого большинство языков программирования поддерживают, по крайней мере, один вид данных для представления такой
символьной информации. На данный момент мы используем строки
BSL. Вообще говоря, строка (String) – это последовательность символов, которые можно вводить с клавиатуры, плюс некоторые другие,
которые мы пока не будем трогать, заключенная в двойные кавычки.
В прологе мы видели несколько строк на языке BSL: "hello", "world",
"blue", "red" и др. Первые две – это слова, которые могут употребляться в разговоре или в письме; остальные – названия цветов, которые
мы, возможно, захотим использовать.
ПРИМЕЧАНИЕ. Мы используем термин 1String (односимвольная
строка) для обозначения символов, вводимых с клавиатуры и составляющих строку. Например, "red" состоит из трех таких 1String: "r", "e",
"d". В действительности 1String – это нечто большее, но сейчас будем
представлять данные этого типа как строки, состоящие из одного
символа. КОНЕЦ.
В языке BSL есть только одна операция, принимающая и возвращающая исключительно строки: string-append, которая, как мы видели в прологе, объединяет две строки в одну. Операцию string-append
можно считать операцией сложения строк, похожей на +, только, в отличие от последней, принимающей два (или более) числа и возвращающей новое число, первая принимает две или более строк и возвращает новую строку:
> (string-append "what a " "lovely " "day" " 4 BSL")
"what a lovely day 4 BSL"

Исходные числа не меняются, когда складываются операцией
+, и исходные строки не меняются, когда объединяются операцией string-append. Если вам понадобится вычислять такие выражения
в уме, то просто помните, что при сложении строк используются очевидные законы, аналогичные законам для +:
(+ 1 1) == 2 (string-append "a" "b") == "ab"
(+ 1 2) == 3 (string-append "ab" "c") == "abc"
(+ 2 2) == 4 (string-append "a" " ") == "a "
...
...

Арифметика

Упражнение 2. Добавьте следующие две строки в область определений:
(define prefix "hello")
(define suffix "world")

Затем используйте элементарные операции со строками, чтобы
создать выражение, которое объединяет prefix и suffix и вставляет "_"
между ними. Получившаяся в результате программа должна после запуска выводить "hello_world" в области взаимодействий.
См. упражнение 1, где показано, как создавать выражения в DrRa­
cket. 

1.3. А теперь все смешаем
Все остальные операции со строками (в языке BSL) принимают или возвращают данные, не являющиеся строками. Вот несколько примеров:
zz string-length принимает строку и возвращает число;
zz string-ith принимает строку s и число i и возвращает 1String

(символ), находящийся в i-й позиции в строке s (счет начинается с 0);
zz number->string принимает число и возвращает строку.

Также обратите внимание на substring и узнайте, что она делает.
Если документация в HelpDesk покажется вам путаной, поэкспериментируйте с функциями в области взаимодействий. Передайте им
подходящие аргументы и выясните, что они вычисляют. Также попробуйте передать неподходящие аргументы, чтобы узнать, как на
это реагирует BSL:
> (string-length 42)
string-length:expects a string, given 42

(string-length: ожидалась строка, а получено число 42).
Как видите, в таких случаях BSL сообщает об ошибке. В первой час­
ти сообщения («string-length») указывается название операции, в которой обнаружилась ошибка, а во второй половине описывается сама
ошибка. В данном конкретном примере BSL сообщает, что stringlength должна применяться к строке, а мы передали ей число 42.
Операции можно вкладывать друг в друга, если следить за тем,
чтобы передавались подходящие данные. Вернемся к выражению
из пролога:
(+ (string-length "hello world") 20)

Внутреннее выражение применяет string-length к "hello world" – нашей любимой строке. Внешнее выражение + получает результат вложенного выражения и число 20.

61

62

Глава 1

Давайте пройдем это выражение по шагам и определим его ре­
зультат:
(+ (string-length "hello world") 20)
==
(+ 11 20)
==
31

Как видите, вычисления с такими вложенными выражениями, обрабатывающими данные разных типов, ничем не отличаются от вычислений с числовыми выражениями. Вот еще один пример:
(+ (string-length (number->string 42)) 2)
==
(+ (string-length "42") 2)
==
(+ 2 2)
==
4

Прежде чем продолжить, попробуйте создать несколько вложенных выражений, которые неправильно смешивают данные, например:
(+ (string-length 42) 1)

Запустите их в DrRa­cket. Прочитайте сообщение об ошибке, а также
обратите внимание, какие области подсвечиваются в области определений.
Упражнение 3. Добавьте следующие две строчки кода в область
определений:
(define str "helloworld")
(define i 5)

Затем, используя операции со строками, создайте выражение, которое добавляет символ "_" в строку str в позицию i. В результате
должна получиться строка длиннее исходной; ожидаемый результат:
"hello_world".
Под термином позиция подразумевается символ, находящийся на
i-м месте слева от начала, но программисты начинают счет с 0, по­
этому 5-я буква в этом примере – "w", потому что 0-я буква – "h". Подсказка. Столкнувшись с подобной «проблемой отсчета», выпишите
символы строки и подпишите ниже их номера, начав с 0, это облегчит
подсчет:
(define str "helloworld")
(define ind "0123456789")
(define i 5)

См. упражнение 1, где показано, как создавать выражения в DrRa­
cket. 

Арифметика

63

Упражнение 4. Используйте те определения, что и в упражнении 3, и создайте выражение, удаляющее из str символ в i-й позиции.
Очевидно, что это выражение создаст строку короче исходной. Какие
значения i допустимы? 

1.4. Арифметика изображений
Изображение – это прямоугольный фрагмент визуальных Открыв новую вкладку,
данных, например фотография или геометрическая фигура не забудьте подключить
библиотеку 2htdp/image.
и ее рамка. Изображения можно вставлять в DrRa­cket везде,
где можно вставлять выражения, потому что изображения
являются значениями, такими же как числа и строки.
Ваши программы могут манипулировать изображениями с по­
мощью элементарных операций трех видов. Операции первого вида
создают элементарные изображения:
zz circle создает изображение круга из радиуса, строку, определяzz

zz
zz
zz
zz

ющую необходимость заливки, и строку с названием цвета;
ellipse создает эллипс и принимает два диаметра, строку,
определяющую необходимость заливки, и строку с названием
цвета;
line создает отрезок по двум точкам и строке с названием
цвета;
rectangle создает прямоугольник и принимает ширину, высоту,
строку режима и строку с названием цвета;
text создает текстовое изображение и принимает строку с текстом, размер шрифта и строку с названием цвета;
triangle создает равносторонний треугольник, направленный
вверх, и принимает размер, строку режима и строку с названием цвета.

Названия этих операций однозначно определяют создаваемое
изображение. Вам только нужно запомнить строки режима "solid" (со
сплошной заливкой цветом) и "outline" (только контур) и строки цветов, такие как "orange" (оранжевый), "black" (черный) и т. д.
Поэкспериментируйте с этими операциями в окне взаимодействий:
> (circle 10 "solid" "green")
> (rectangle 10 20 "solid" "blue")
> (star 12 "solid" "gray")

А теперь взгляните еще раз на примеры выше! В последнем примере используется не упомянутая выше операция. Загляните в докумен-

64

Глава 1

тацию (https://docs.racket-lang.org/teachpack/2htdpimage.html) и узнай­
те, сколько еще таких операций имеется в библиотеке 2htdp/image.
Поэкспериментируйте с этими операциями.
Операции второго вида возвращают свойства изображений:
zz image-width определяет ширину изображения в пикселях;
zz image-height определяет высоту изображения.

Они извлекают эти значения непосредственно из изображений,
например:
> (image-width (circle 10 "solid" "red"))
20
> (image-height (rectangle 10 20 "solid" "blue"))
20

А теперь остановитесь и объясните, что вернет DrRa­cket, если ввес­
ти следующее выражение:
(+ (image-width (circle 10 "solid" "red"))
(image-height (rectangle 10 20 "solid" "blue")))

Для правильного понимания третьего вида операций с изображениями необходимо познакомиться с одной новой идеей: точкой привязки. Изображение – это не единственный пиксель, оно состоит из
множества пикселей. Каждое изображение чем-то похоже на фотографию, то есть на прямоугольник, заполненный пикселями. Один из
этих пикселей считается точкой привязки. При использовании операций, объединяющих два изображения, объединение осуществляется относительно точек привязки, если явно не указать какую-либо
другую точку:
zz overlay накладывает все изображения, перечисленные в опера-

ции, друг на друга, используя центр в качестве точки привязки;
zz overlay/xy подобна операции overlay, но принимает два числа –
x и y – между двумя аргументами с изображениями. Она сдвигает второе изображение на x пикселей вправо и на y пикселей
вниз относительно верхнего левого угла первого изображения;
отрицательное значение x вызывает сдвиг второго изображения влево, а отрицательное значение y – вверх;
zz overlay/align подобна операции overlay, но принимает две строки, которые смещают точки привязки указанных изображений.
Всего существует девять разных позиций; поэкспериментируйте с ними!
Библиотека 2htdp/image содержит множество других элементарных
функций для объединения изображений. Когда захотите познакомиться с ними поближе, вам придется прочитать документацию с их
описанием. А пока мы представим еще три операции, которые могут
пригодиться для создания анимированных сцен и изображений для
игр:

65

Арифметика
zz empty-scene создает прямоугольник заданной ширины и высоты;
zz place-image помещает изображение в сцену в указанное место.

Если изображение не помещается в сцену, оно будет соответствующим образом обрезано;

zz scene+line принимает сцену, четыре числа и цвет и рисует ли-

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

Законы арифметики изображений аналогичны законам арифметики чисел; см. табл. 1, где приводится несколько примеров и сравнение
с арифметикой чисел. Повторю еще раз, что ни одна операция не изменяет и не уничтожает исходное изображение. Так же как +, эти операции просто создают новые изображения, которые определенным
образом объединяют исходные данные.
Таблица 1. Правила создания изображений
Арифметика чисел
(+ 1 1) == 2

Арифметика изображений
(overlay (square 4 "solid" "orange")
(circle 6 "solid" "yellow"))
==

(+ 1 2) == 3

(underlay (circle 6 "solid" "yellow")
(square 4 "solid" "orange"))
==

(+ 2 2) == 4

(place-image (circle 6 "solid" "yellow")
10 10
(empty-scene 20 20))
==

...

...

Упражнение 5. Воспользуйтесь библиотекой 2htdp/image и создайте изображение простой лодки или дерева. Предусмотрите простую
возможность изменения размеров изображения. 
Упражнение 6. Добавьте следующую строку в область Скопируйте и вставьте
изображение в DrRa­cket.
определений:

(define cat

)

Создайте выражение, подсчитывающее пиксели в изображении. 

66

Глава 1

1.5. Арифметика логических значений
Прежде чем мы сможем проектировать программы, нам нужно познакомиться с последним видом элементарных данных: логическими
(boolean) значениями. Существует только два вида логических значений: #true и #false. Программы используют логические значения для
представления решений или состояния переключателей.
Вычисления с логическими значениями тоже очень просты. В частности, программы на BSL используют в основном три операции: or,
and и not. Эти операции похожи на сложение, умножение и изменение знака чисел. Конечно, поскольку существует всего два логических
значения, мы имеем возможность продемонстрировать работу этих
функций во всех возможных ситуациях:
zz or проверяет, есть ли #true среди заданных логических значе-

ний:

> (or #true #true)
#true
> (or #true #false)
#true
> (or #false #true)
#true
> (or #false #false)
#false

zz and проверяет, равны ли все указанные логические значения

значению #true:
> (and
#true
> (and
#false
> (and
#false
> (and
#false

#true #true)
#true #false)
#false #true)
#false #false)

zz not всегда выбирает другое логическое значение, отличающееся

от заданного:
> (not #true)
#false

Неудивительно, что операции or и and могут принимать больше
двух выражений. Наконец, операции or и and имеют еще ряд отличительных особенностей, но для их объяснения необходимо вернуться
к вложенным выражениям.
Формулировку этого
Упражнение 7. Логические выражения могут выражать
упражнения предложил
Надим Хамид некоторые повседневные проблемы. Предположим, вам нуж(Nadeem Hamid). но решить, подходит ли сегодняшний день для посещения

Арифметика

торгового центра. Вы ходите в торговый центр либо когда пасмурно,
либо по пятницам (потому что именно по пятницам в магазинах проводятся распродажи).
Теперь попробуйте принять решение, используя новые знания
о логических значениях. Сначала добавьте эти две строки в область
определений DrRa­cket:
(define sunny #true)
(define friday #false)

Теперь создайте выражение, которое проверит, что sunny имеет
значение #false или friday имеет значение #true. В данном случае результат должен получиться равным #false. (Почему?)
См. упражнение 1, где показано, как создавать выражения в DrRa­
cket. Сколько всего разных комбинаций из значений sunny и friday может быть? 

1.6. Смешанные операции с логическими
значениями
Одно из важных применений логических значений – помощь в вычислениях с другими видами данных. В прологе уже говорилось, что
в программах на BSL можно давать значениям имена с по­мощью
определений. Например, программа может начинаться с определения
(define x 2)

и затем вычислять обратную величину:
(define inverse-of-x (/ 1 x))

И все будет хорошо, пока мы не отредактируем программу и не изменим значение x на 0.
В таких ситуациях нам могут помочь логические значения, в частности условные вычисления. Сначала с по­мощью элементарной
функции = можно проверить равенство двух (или более) чисел. Если
они равны, то операция = вернет #true, иначе – #false. Затем использовать разновидность выражения на BSL, о которой мы пока не упоминали: выражение if. В нем используется слово «if», как если бы это
была элементарная функция, но это не так. За словом «if» следуют три
выражения, разделенных пробельными символами (включая табуляцию, разрывы строк и т. д.). Естественно, все выражение заключено
в круглые скобки, например:
(if (= x 0) 0 (/ 1 x))

Это выражение if содержит три подвыражения: (= x 0), 0 и (/ 1 x).
Вычисление этого выражения происходит в два этапа:

67

68

Глава 1

1. Первое подвыражение вычисляется всегда. Его результат должен быть логическим значением.
2. Если первое подвыражение возвращает #true, то вычисляется
второе подвыражение; в противном случае – третье. Результат
Щелкнув правой кнопкой второго этапа вычислений становится результатом всего
мыши на результате, выражения if.
вы сможете выбрать
другую форму его
представления.

Введите определение x, показанное выше, и поэкспериментируйте с выражением if в области взаимодействий:

> (if (= x 0) 0 (/ 1 x))
0.5

Опираясь на законы арифметики, вы можете сами предугадать результат:
(if (= x 0) 0 (/ 1 x))
== ; поскольку x имеет значение 2
(if (= 2 0) 0 (/ 1 2))
== ; 2 не равно 0, подвыражение (= 2 0) даст #false
(if #false 0 (/ 1 x))
(/ 1 2)
== ; после нормализации в десятичное представление получается
0.5

Другими словами, DrRa­cket знает, что x обозначает 2, а оно не равно
0. Поэтому (= x 0) вернет #false, и функция if выберет третье подвыражение для этапа вычислений.
А сейчас представьте, что вы исправили определение x так, что теперь оно выглядит следующим образом:
(define x 0)

Какое значение теперь вернет наше условное выражение?
(if (= x 0) 0 (/ 1 x))

Почему? Напишите на листке бумаги последовательность вычислений, как она видится вам.
Кроме =, в BSL имеется множество других элементарных операций
сравнения. Объясните, что делают следующие четыре операции сравнения в отношении чисел: =.
Строки нельзя сравнивать с по­мощью = и родственных ей операций. Вместо этого надо использовать string=?, string=?.
Совершенно очевидно, что string=? проверяет равенство двух заданных строк, но две другие операции требуют пояснений. Загляните
в документацию с их описанием. Или экспериментальным путем
определите общие закономерности, а затем проверьте свои выводы,
заглянув в документацию.
У кого-то может возникнуть вопрос: зачем вообще сравнивать строки друг с другом? Представьте программу, которая управляет светофо-

69

Арифметика

рами. Она может использовать строки "green", "yellow" и "red" для обозначения цветов. Программа может содержать такой фрагмент:
(define current-color ...)

(define next-color
(if (string=? "green" current-color) "yellow" ...))

Точки в определении
current-color, конечно
же, не являются частью
программы. Замените
их строкой с названием
цвета.

Легко представить, что этот фрагмент связан с вычислениями, которые определяют, какую лампочку нужно включить, а какую выключить.
В следующих нескольких главах мы рассмотрим более эффективные способы выражения условных вычислений, чем if, и, что особенно важно, системные способы их проектирования.
Упражнение 8. Добавьте следующую строку в область опреде­
лений:

(define cat

)

Создайте условное выражение, которое определяет, какая сторона изображения больше – ширина или высота. Изображение должно быть помечено как "tall" (высокое), если его высота больше или
равна ширине; иначе метка должна быть строкой "wide" (широкое).
См. упражнение 1, где показано, как создавать выражения в DrRa­cket.
В ходе экспериментов замените изображение кота прямоугольником
по вашему выбору и убедитесь, что ваше выражение возвращает правильный ответ.
После этого попробуйте изменить выражение так, чтобы оно определяло, является ли изображение "tall" (высоким), "wide" (широким)
или "square" (квадратным). 

1.7. Предикаты: знай свои данные
Вспомните выражение (string-length 42) и его результат. На самом
деле это выражение не дает результата, оно сообщает об ошибке.
DrRa­cket выводит сообщения об ошибках красным цветом в области взаимодействий и выделяет ошибочные выражения (в области
определений). Этот способ выделения ошибок особенно полезен,
когда ошибочное выражение глубоко вложено в какое-то другое выражение:
(* (+ (string-length 42) 1) pi)

70

Глава 1

Поэкспериментируйте с этим выражением, введя его в область взаи­
модействий и в область определений (а затем щелкните на кнопке
RUN (Выполнить)).
Конечно, никому не хочется, чтобы в его программе были подобные выражения, сигнализирующие об ошибках. И обычно мало кто
допускает такие очевидные ошибки, как использование числа 42
вмес­то строки. Однако довольно часто программы имеют дело с переменными, которые могут хранить число или строку:
(define in ...)
(string-length in)

Переменная, такая как in, может играть роль заменителя любого значения, включая число, и использоваться в выражении stringlength.
Один из способов предотвратить подобные случайности – использовать предикат, то есть функцию, которая принимает значение
и определяет, принадлежит ли оно какому-либо классу данных. Например, предикат number? определяет, является ли данное значение
числом:
> (number?
#true
> (number?
#true
> (number?
#false
> (number?
#false

4)
pi)
#true)
"fortytwo")

Как видите, предикаты возвращают логические значения. Поэтому, комбинируя предикаты с условными выражениями, можно предотвратить неправильное использование выражений:
(define in ...)
(if (string? in) (string-length in) ...)
Введите выражение (sqrt -1)
в области взаимодействий и нажмите клавишу Enter. Посмотрите, что
получилось в результате. Вы должны
увидеть так называемое комплексное
число, с которым рано или поздно
сталкивается каждый. Ваш учитель
математики мог говорить вам, что
нельзя вычислить квадратный корень
из отрицательного числа, но правда
в том, что математики и некоторые
программисты уверены в обратном.
Не волнуйтесь: понимание особенностей комплексных чисел не является
обязательным требованием для
проектировщиков программ.

Для всех классов данных, с которыми мы познакомились в этой главе, имеются соответствующие предикаты. Поэкспериментируйте с number?,
string?, image? и boolean?, чтобы понять, как они работают.
В дополнение к предикатам, которые различают
разные формы данных, языки программирования
также имеют предикаты, различающие разные
типы чисел. В BSL числа классифицируются по
строению и по точности. Под строением понимаются знакомые всем числа: целые, рациональные,
действительные и комплексные, но многие языки
программирования, включая BSL, также используют конечные приближения хорошо известных

Арифметика

констант, что приводит к несколько неожиданным результатам с предикатом rational?:
> (rational? pi)
#true

Что касается точности, то мы уже упоминали это понятие. А теперь
поэкспериментируйте с предикатами exact? и inexact?, чтобы убедиться, что они выполняют проверки, о которых говорят их имена
(точное число и неточное число соответственно). Позже мы обсудим
природу чисел более подробно.
Упражнение 9. Добавьте следующую строку в область определений в DrRa­cket:
(define in ...)

Затем создайте выражение, преобразующее значение in в положительное число. Для строки в переменной in выражение должно вычислять длину строки; для изображения – площадь; для числа оно
должно уменьшать это число на 1, если оно не равно 0 или неотрицательное; для #true должно возвращаться значение 10, а для #false –
значение 20. Подсказка: прочитайте (еще раз) описание условного выражения cond в разделе «Пролог: как писать программы».
См. упражнение 1, где показано, как создавать выражения в DrRa­
cket. 
Упражнение 10. Теперь отдохните, поешьте, поспите и переходите к следующей главе. 

71

2. Функции и программы
«Арифметика» в программировании – это только половина дела; другая половина – «алгебра». В данном случае слово «алгебра» относится
к школьному предмету алгебры так же, как понятие «арифметика» из
предыдущей главы относится к школьному предмету арифметики.
В частности, алгебра в программировании включает такие понятия,
как переменная, определение функции, применение функции и композиция функций. Эта глава повторно познакомит вас с этими понятиями в увлекательной и доступной форме.

2.1. Функции
Программы – это функции. Так же как функции, программы принимают входные данные и возвращают результат. В отличие от функций, с которыми вы, возможно, хорошо знакомы, программы работают с разными данными: числами, строками, изображениями и их
комбинациями. Кроме того, программы могут реагировать на события в реальном мире, а их результаты – влиять на реальный мир.
Например, программа для работы с электронными таблицами может
реагировать на нажатия клавиш бухгалтером и заполнять некоторые
ячейки числами, или программа-календарь может запускать программу ежемесячного расчета заработной платы в последний день
каждого месяца. Наконец, программа не может использовать все
свои входные данные сразу, вместо этого она обрабатывает их поэтапно.
Определения. Многие языки программирования скрывают связь
между программами и функциями, но BSL, наоборот, выдвигает ее на
передний план. Каждая программа на языке BSL состоит из нескольких определений, за которыми обычно следует выражение, использующее эти определения. Есть два вида определений:
zz определения констант в форме (define Переменная Выражение), кото-

рые мы видели в предыдущей главе;
zz определения функций, которые бывают разных видов; один из
них мы использовали в прологе.
Подобно выражениям, определения функций в BSL имеют едино­
образную форму:
(define (ИмяФункции Переменная ... Переменная)
Выражение)

То есть, чтобы определить функцию, нужно ввести:
zz «(define (»,
zz имя функции,

Функции и программы
zz необходимые переменные, перечислив их через пробел, и за-

вершить список переменных закрывающей круглой скобкой
«)»,
zz затем выражение и закрывающую скобку «)».
Вот несколько коротких примеров:
zz (define (f x) 1)
zz (define (g x y) (+ 1 1))
zz (define (h x y z) (+ (* 2 2) 3))

Прежде чем объяснить, почему эти примеры не имеют практической ценности, мы должны рассказать, что означают определения
функций. Если выражаться простым языком, то определение функции вводит новую операцию с данными, то есть добавляет новую
операцию в наш словарь, содержащий элементарные операции, которые всегда доступны. Подобно элементарной функции, определяемая
нами функция принимает входные данные. Количество переменных
определяет количество входных данных, также называемых аргументами или параметрами. Таким образом, f – это функция с одним
аргументом, такие функции иногда называют унарными функциями. Функция g – это функция с двумя аргументами, такие функции
иногда называют бинарными, а h – это функция с тремя аргументами,
или тернарная функция. Выражение, которое часто называют телом
функции, определяет результат.
Примеры демонстрируют бесполезные функции, потому что выражения внутри них не включают переменные. Поскольку переменные
относятся к входным параметрам, отсутствие упоминания их в выражениях означает, что выходные данные функции не зависят от их
входных данных и, следовательно, всегда одинаковы. Нам не нужно
писать функции или программы, если результат всегда один и тот же.
Переменные – это не данные; они представляют данные. Например, определение константы, такое как
(define x 3)

говорит, что x всегда имеет значение 3. Переменные в заголовке функции, то есть следующие за именем функции, являются заглушками
и представляют пока неизвестные входные данные. Ссылки на переменные в теле функции – это способ использовать эти данные во
время применения функции, когда значения переменных становятся
известными.
Рассмотрим следующий фрагмент определения:
(define (ff a) ...)

Заголовок этой функции – (ff a) – означает, что ff принимает одно
входное значение, а переменная a является представителем этого
значения. Определяя функцию, мы не знаем, какие значения будут

73

74

Глава 2

иметь входные данные. На самом деле весь смысл определения функции состоит в том, чтобы дать возможность использовать функцию
много раз для множества разных входных данных.
Тело функции ссылается на ее параметры. Ссылка на параметр
функции на самом деле является ссылкой на входные данные функции. Если завершить определение ff следующим образом:
(define (ff a)
(* 10 a))

то тем самым мы скажем, что результатом функции является число,
в десять раз большее входного числа. Вероятно, этой функции будут
передаваться числа, потому что нет смысла умножать на 10 изображения, логические значения или строки.
На данный момент нам осталось прояснить единственный вопрос – как функция получает входные данные. С этой целью обратимся к понятию применения функции.
Применение. Применение функции заставляет определения функций работать и выглядит так же, как применение любой предопределенной операции:
zz введите «(»;
zz введите имя функции, например f;
zz добавьте столько аргументов, сколько может принять функция,

перечислив их через пробел;
zz и добавьте «)» в конце.

Теперь, чтобы закрепить новые знания, поэкспериментируйте
с функциями в области взаимодействий, как мы это делали с элементарными операциями, и убедитесь, что понимаете, как они работают.
Например, следующие три эксперимента подтверждают, что функция
f, которую мы определили выше, возвращает одно и то же значение
при применении к любым данным:
> (f 1)
1
> (f "hello world")
1
> (f #true)
1

Что вернет выражение (f (circle 3 "solid" "red"))?
Как видите, поведение функции f не меняется, даже если
ее применить к изображению. Но вот что произойдет, если
попытаться применить функцию к слишком малому или
слишком большому количеству аргументов:

Не забудьте добавить
выражение (require
2htdp/image) в области
определений.

> (f)
f:expects 1 argument, found none (f: ожидается 1 аргумент, не найдено ни одного)
> (f 1 2 3 4 5)
f:expects only 1 argument, found 5 (f: ожидается только 1 аргумент, найдено 5)

Функции и программы

DrRa­cket сигнализирует об ошибке сообщением, которое похоже на
те, что выводятся при применении элементарной операции к неправильному количеству аргументов:
> (+)
+:expects at least 2 arguments, found none (+: ожидается по меньшей мере 2 аргумента, не
найдено ни одного)

Функции можно применять не только в приглашении к вводу в области взаимодействий. Их также можно применять внутри вложенных выражений:
> (+ (ff 3) 2)
32
> (* (ff 4) (+ (ff 3) 2))
1280
> (ff (ff 1))
100

Упражнение 11. Определите функцию, которая принимает два
числа, x и y, и вычисляет расстояние от точки (x, y) до начала коор­
динат.
В упражнении 1 мы разработали правую часть этой функции для
конкретных значений x и y. Теперь добавьте заголовок. 
Упражнение 12. Определите функцию cvolume, которая принимает
длину ребра куба и вычисляет его объем. Если у вас есть время, определите также функцию csurface, вычисляющую площадь поверхности
куба.
Подсказка. Куб – это трехмерная объемная фигура, ограниченная шестью квадратами – гранями. Площадь поверхности куба легко
определить, зная площадь одного квадрата, которая равна квадрату
длины его стороны. Объем куба – это произведение длины ребра на
площадь одной грани. (Почему?) 
Упражнение 13. Определите функцию string-first, которая извлекает первый символ (1String) из непустой строки. 
Упражнение 14. Определите функцию string-last, которая извлекает последний символ (1String) из непустой строки. 
Упражнение 15. Определите функцию ==>. Она должна принимать
два логических значения (пусть это будут параметры с именами sunny
и friday) и возвращать #true, если sunny имеет ложное значение или
friday – истинное. Примечание. В алгебре логики эта логическая операция называется импликацией и обозначается как sunny => friday. 
Упражнение 16. Определите функцию image-area, которая подсчитывает количество пикселей в заданном изображении. Идеи, как это
можно реализовать, см. в упражнении 6. 
Упражнение 17. Определите функцию image-classify, которая
принимает изображение и возвращает "tall" (высокое), если высота
изображения больше ширины; "wide" (широкое), если высота изображения меньше ширины, и "square" (квадратное), если ширина равна
высоте. Идеи, как это можно реализовать, см. в упражнении 8. 

75

76

Глава 2

Упражнение 18. Определите функцию string-join, которая принимает две строки и объединяет их в одну строку, добавляя символ "_"
между ними. Идеи, как это можно реализовать, см. в упражнении 2. 
Упражнение 19. Определите функцию string-insert, которая принимает строку str и число i и вставляет "_" в i-ю позицию строки str.
Предполагается, что i – это число в диапазоне от 0 до длины заданной строки (включительно). Идеи, как это можно реализовать, см.
в упражнении 3. Подумайте, как string-insert может справиться со
вставкой "". 
Упражнение 20. Определите функцию string-delete, которая принимает строку str и число i и удаляет из str символ в i-й позиции.
Предполагается, что i – это число в диапазоне от 0 до длины данной
строки (не включая). Идеи, как это можно реализовать, см. в упражнении 4. Сможет ли string-delete работать с пустыми строками? 

2.2. Вычисления
Определение и применение функций всегда идут рука об руку. Если
вы хотите проектировать программы, то должны понимать эту взаимосвязь, потому что вам придется в уме представлять, как DrRa­cket
выполняет ваши программы, и, соответственно, замечать, что идет
не так, когда что-то идет не так, а рано или поздно что-то обязательно пойдет не так.
Возможно, вы уже сталкивались с этой идеей на уроках алгебры,
тем не менее мы попробуем объяснить ее по-своему. Итак, поехали.
Применение функции происходит в три этапа: DrRa­cket вычисляет
значения выражений аргументов; проверяет равенство числа аргументов и числа параметров функции; и если это условие соблюдается, то вычисляет значение тела функции, заменяя все параметры
значениями соответствующих аргументов. Это последнее значение
является значением выражения применения функции. Данное объяснение выглядит довольно сложным, поэтому обратимся к примерам.
Вот пример применения функции f:
(f
==
(f
==
1

(+ 1 1))
; DrRa­cket знает, что (+ 1 1) == 2
2)
; DrRa­cket заменяет все вхождения x числом 2

Последнее уравнение выглядит странным, потому что x не используется в теле f. В результате замена вхождения x на 2 в теле функции
дает в результате 1 – число, которое является телом функции.
Для ff вычисления выполняются несколько иначе:
(ff (+ 1 1))
== ; DrRa­cket знает, что (+ 1 1) == 2
(ff 2)

Функции и программы
== ; DrRa­cket заменяет все вхождения x в теле ff числом 2
(* 10 2)
== ; а затем DrRa­cket выполняет самую обычную арифметическую операцию
20

Самое замечательное, что, объединяя эти законы вычислений с законами арифметики, можно успешно предсказать результат любой
программы на BSL:
(+
==
(+
==
(+
==
(+
==
32

(ff (+ 1 2)) 2)
; DrRa­cket знает, что (+ 1 2) == 3
(ff 3) 2)
; DrRa­cket заменяет все вхождения x в теле ff числом 3
(* 10 3) 2)
; теперь DrRa­cket применяет законы арифметики
30 2)

Естественно, полученный результат можно использовать в других
вычислениях:
(* (ff 4) (+ (ff 3) 2))
== ; DrRa­cket заменяет все вхождения x в теле ff числом 4
(* (* 10 4) (+ (ff 3) 2))
== ; DrRa­cket знает, что (* 10 4) == 40
(* 40 (+ (ff 3) 2))
== ; теперь используется результат, вычисленный выше
(* 40 32)
==
1280 ; потому что это просто математика

В целом можно сказать, что DrRa­cket прекрасно владеет алгеброй,
знает все законы арифметики и отлично справляется с заменой. Более того, DrRa­cket может не только определять значения выражений,
но также может показать, как это делается, то есть может показать
пошаговое решение алгебраической задачи, которая должна определить значение выражения.
Еще раз взгляните на кнопки в окне DrRa­cket. Одна из них выглядит как кнопка «перейти к следующему треку» на аудиоплеере. Если
щелкнуть на этой кнопке, то появится окно движка пошаговых вычислений, в котором можно проверить порядок выполнения программы в области определений.
Введите в область определений определение функции ff. Там же,
в области определений, ниже определения функции, добавьте выражение (ff (+ 1 1)). Теперь щелкните на кнопке STEP (Шаг). Появится
окно движка пошаговых вычислений; на рис. 3 показано, как выглядит это окно в версии DrRa­cket 6.2. Далее можно использовать стрелки вперед и назад, чтобы увидеть все этапы вычислений, выполняемые для определения значения выражения. Обратите внимание, что
движок пошаговых вычислений выполняет те же вычисления, что
и мы.

77

78

Глава 2

Да, вы могли бы использовать DrRa­cket для решения домашних
заданий по алгебре! Поэкспериментируйте с разными вариантами,
которые предлагает движок.

Рис. 3. Движок пошаговых вычислений в DrRa­cket

Упражнение 21. Используйте движок пошаговых вычислений
в DrRa­cket, чтобы увидеть, как вычисляется выражение (ff (ff 1)).
Также попробуйте выражение (+ (ff 1) (ff 1)). Использует ли движок пошаговых вычислений в DrRa­cket результаты вычислений по­
вторно? 
Читая обсуждение всех этих вычислений, включающих скучные
функции и числа, вы можете подумать, что снова изучаете курс алгебры. К счастью, этот подход распространяется на все программы,
включая самые интересные, в данной книге.
Для начала рассмотрим функции, обрабатывающие строки. Вспомним некоторые законы арифметики строк:
(string-append "hello" " " "world") == "hello world"
(string-append "bye" ", " "world") == "bye, world"
...

Теперь допустим, что мы определили функцию, которая создает
начало письма:
(define (opening first-name last-name)
(string-append "Dear " first-name ","))

Применив эту функцию к двум строкам, вы получаете начало
письма:
> (opening "Matthew" "Fisler")
"Dear Matthew,"

Однако самое важное – как законы вычислений определяют этот
результат и как, используя их, можно предвидеть, что сделает DrRa­
cket:
(opening "Matthew" "Fisler")
== ; DrRa­cket подставляет "Matthew" вместо first-name
(string-append "Dear " "Matthew" ",")
==
"Dear Matthew,"

79

Функции и программы

Поскольку last-name не встречается в определении функции opening, подстановка "Fisler" не имеет никакого эф­
фекта.
Далее в книге мы познакомимся с другими формами
данных. Для объяснения выполнения операций с данными
всегда используются законы, подобные тем, которые действуют в арифметике в этой книге.
Упражнение 22. Используйте движок пошаговых вычислений в DrRa­cket, чтобы увидеть, как выполняется следующий фрагмент программы:

Рано или поздно вы
столкнетесь с императивными операциями,
которые не объединяют и не извлекают
значения, а изменяют
их. Чтобы производить
вычисления с по­мощью
таких операций, вам
понадобится изучить
дополнительные законы,
помимо законов арифметики и подстановки.

(define (distance-to-origin x y)
(sqrt (+ (sqr x) (sqr y))))
(distance-to-origin 3 4)

Соответствует ли ход вычислений вашему пониманию? 
Упражнение 23. Первый символ (1String) в строке "hello world" –
буква "h". Как следующая функция вычисляет этот результат?
(define (string-first s)
(substring s 0 1))

Подтвердите свои рассуждения с по­мощью движка пошаговых вычислений. 
Упражнение 24. Вот определение функции ==>:
(define (==> x y)
(or (not x) y))

С помощью движка пошаговых вычислений определите значение
выражения (==> #true #false). 
Упражнение 25. Взгляните на следующую попытку решить упражнение 17:
(define (image-classify img)
(cond
[(>= (image-height img) (image-width img)) "tall"]
[(= (image-height img) (image-width img)) "square"]
[( (letter "Matthew" "Fisler" "Felleisen")
"Dear Matthew,\n\nWe have discovered that ...\n"
> (letter "Kathi" "Felleisen" "Findler")
"Dear Kathi,\n\nWe have discovered that ...\n"

ПРИМЕЧАНИЕ. Результатом обоих выражений является длинная
строка, содержащая "\n" – символ, представляющий перевод на но-

81

Функции и программы

вую строку при печати. Теперь добавьте в программу инструкцию (require 2htdp/batchio), подключающую библиотеку с функцией write-file, которая позволит вам вывести эту
строку в консоль:

Пока считайте 'stdout
простой строкой.

> (write-file 'stdout (letter "Matt" "Fiss" "Fell"))
Dear Matt,
We have discovered that all people with the
last name Fiss have won our lottery. So,
Matt, hurry and pick up your prize.
Sincerely,
Fell
'stdout

В разделе 2.5 мы подробно разберем такие пакетные программы.
КОНЕЦ.
В общем случае, когда решаемая программой проблема делится на
несколько отдельных задач, каждая такая задача должна решаться отдельной функцией, а общая проблема – главной функцией, объединяющей все вместе. Выразим эту идею простым правилом:
Определяйте отдельную функцию для каждой задачи.
Следуя этому правилу, вы получаете достаточно короткие и прос­
тые для понимания функции. Когда вы научитесь разрабатывать
функции, вы поймете, что маленькие функции намного проще заставить работать правильно, чем большие. Более того, если вам когда-нибудь понадобится изменить часть программы из-за изменения
формулировки проблемы, то вам будет гораздо проще найти соответствующие части кода, если он организован как набор небольших
функций, а не как большой монолитный блок.
Вот иллюстрация действия этого правила на примере.
Проблема. Владелец монопольного кинотеатра в маленьком городке имеет полную свободу устанавливать цены на билеты. Чем
выше цена, тем меньше людей могут позволить себе купить билеты. Чем ниже цена, тем больше затраты на организацию показов из-за высокой посещаемости. Немного поэкспериментировав, владелец кинотеатра установил зависимость между ценой
билета и средней посещаемостью.
При цене 5 долларов за билет на показ приходят 120 человек.
На каждые 10 центов изменения стоимости билета средняя посещаемость меняется на 15 человек. То есть если поднять стои­
мость билета до 5,10 доллара, то на сеансы будет приходить
в среднем 105 человек; если опустить цену до 4,90 доллара, то
средняя посещаемость увеличится до 135 человек. Давайте переведем это в математическую формулу:

82

Глава 2

Прежде чем продолжить, объясните, почему в этом уравнении
стоит знак минус.
К сожалению, увеличение посещаемости удорожает обслуживание сеансов. Каждый сеанс обходится владельцу по фиксированной цене в 180 долларов плюс переменные затраты в размере 0,04 доллара на одного зрителя.
Владелец хотел бы знать точное соотношение между при­
былью и ценой билета, чтобы максимизировать прибыль.
Итак, задача ясна, но как ее решить – пока не понятно. На данный
момент мы можем лишь сказать, что у нас имеется несколько величин, зависящих друг от друга.
Сталкиваясь с такой ситуацией, желательно выявить различные зависимости, одну за другой:
1. В постановке задачи указано, что количество зрителей зависит
от цены билета. Очевидно, вычисление этого числа является
отдельной задачей и, следовательно, заслуживает отдельного
определения функции:
(define (attendees ticket-price)
(- 120 (* (- ticket-price 5.0) (/ 15 0.1))))

2. Выручка зависит исключительно от продажи билетов, то есть
определяется произведением цены билета на количество зрителей:
(define (revenue ticket-price)
(* ticket-price (attendees ticket-price)))

3. Затраты на проведение сеанса состоят из двух частей: фиксированной (180 долларов) и переменной, зависящей от количества зрителей. Учитывая, что количество зрителей является
функцией от цены билета, то функция вычисления затрат на
организацию сеанса должна также принимать цену билета,
чтобы повторно использовать функцию attendees:
(define (cost ticket-price)
(+ 180 (* 0.04 (attendees ticket-price))))

4. Наконец, прибыль – это разность между выручкой и затратами
при заданной цене билета:
(define (profit ticket-price)
(- (revenue ticket-price)
(cost ticket-price)))

Функции и программы

Определение функции profit явно отражает неформальное описание проблемы.
Эти четыре функции – все, что нужно для вычисления прибыли,
и теперь мы можем использовать функцию profit, чтобы найти оптимальную цену билета.
Упражнение 27. Наше решение проблемы содержит несколько
констант, используемых в функциях. Как отмечалось в разделе «Одна
программа, множество определений» в прологе, таким константам
лучше давать имена, чтобы будущие читатели кода понимали суть
этих чисел. Определите все константы в области определений DrRa­
cket и замените все магические числа именами констант. 
Упражнение 28. Определите потенциальную прибыль для следующих цен на билеты: 1 доллар, 2 доллара, 3 доллара, 4 доллара и 5 долларов. При какой цене прибыль кинотеатра получится больше всего?
Определите лучшую цену билета до цента. 
Вот альтернативная версия той же программы в виде одной функции:
(define (profit price)
(- (* (+ 120
(* (/ 15 0.1)
(- 5.0 price)))
price)
(+ 180
(* 0.04
(+ 120
(* (/ 15 0.1)
(- 5.0 price)))))))

Введите это определение в DrRa­cket и убедитесь, что оно дает те
же результаты для цен для 1, 2, 3, 4 и 5 долларов, что и исходная версия. Одного взгляда на эту монолитную версию достаточно, чтобы
убедиться, насколько сложнее понять эту функцию, чем четыре пред­
шест­вующие.
Упражнение 29. Изучив затраты на сеанс, владелец обнаружил несколько способов снизить их. В результате оптимизации он избавился от постоянной составляющей затрат; осталась только переменная
составляющая в размере 1,50 доллара на зрителя.
Измените обе программы, чтобы отразить эту находку, а затем протестируйте их еще раз с ценами на билеты 3, 4 и 5 долларов и сравните результаты. 

2.4. Глобальные константы
Как уже говорилось в прологе, такие функции, как profit, выигрывают
от использования глобальных констант. Любой язык программирования позволяет программистам определять константы. В BSL такие
определения оформляются так:

83

84

Глава 2
zz введите «(define»,
zz добавьте имя константы,
zz затем пробел и выражение
zz и добавьте «)» в конце.

Константа – это, по сути, глобальная переменная, а ее определение
называется определением константы. Выражение в определении константы мы обычно называем правой частью определения.
Определения констант задают имена для любых форм данных: чисел, изображений, строк и т. д. Вот несколько простых примеров:
; текущая стоимость билета на сеанс:
(define CURRENT-PRICE 5)
; удобно использовать для вычисления площади круга:
(define ALMOST-PI 3.14)
; пустая строка:
(define NL "\n")
; пустая сцена:
(define MT (empty-scene 100 100))

Два первых определения объявляют числовые константы, два последних – константы со строкой и изображением. По соглашению
в именах глобальных констант мы используем только прописные
буквы. Это позволяет легко определять такие переменные при бег­
лом просмотре программы.
На эти глобальные переменные могут ссылаться все функции
в программе. Ссылка на переменную подобна непосредственному
использованию соответствующего выражения. Преимущество использования именованных переменных вместо данных заключается
в том, что изменение определения константы отражается на всех вычислениях, в которых она участвует. Например, мы можем добавить
цифры в определение ALMOST-PI или увеличить размер пустой сцены:
(define ALMOST-PI 3.14159)
; пустая сцена:
(define MT (empty-scene 200 800))

В большинстве наших примеров определений в правой части используются константы-литералы, а в последнем – выражение. В действительности программист может использовать любые выражения
в определениях констант, если они вычислимы в точке определения.
Предположим, программе требуется обрабатывать изображения
строго определенного размера и выполнять операции с его центром:
(define WIDTH 100)
(define HEIGHT 200)
(define MID-WIDTH (/ WIDTH 2))
(define MID-HEIGHT (/ HEIGHT 2))

Функции и программы

Мы можем определить две константы с литералами в правой части
и две вычисляемые константы, то есть переменные, значения которых являются не просто литералами, но результатами вычисления
выражений.
И снова обозначим это как правило:
Для каждой константы, используемой в формулировке задачи,
определите константу в программе.
Упражнение 30. Определите константы в программе оптимизации
цен на билеты в кинотеатре, преобразовав изменение посещае­мости
в зависимости от изменения цены (15 человек на каждые 10 центов)
в константу. 

2.5. Программы
Теперь вы готовы к созданию простых программ. С точки зрения написания кода, программа – это просто набор определений функций
и констант. Обычно одна функция выделяется как «главная», и эта
главная функция ссылается на другие функции. Однако с точки зрения выполнения программы делятся на две большие категории:
zz пакетные программы получают сразу все свои входные данные

и вычисляют результат. Главная функция такой программы является композицией из вспомогательных функций, которые
могут ссылаться на другие вспомогательные функции и т. д.
Когда мы запускаем пакетную программу, операционная система вызывает главную функцию, передает ей входные данные и ждет вывода программы;
zz интерактивные программы получают лишь часть своих входных данных, выполняют вычисления и производят некоторые
выходные данные, затем получают дополнительные данные
и т. д. Появление дополнительных данных мы называем событием, а интерактивные программы – программами, которые
управляются событиями. Главная функция такой программы,
управляемой событиями, использует выражение, чтобы описать, какие функции вызывать для тех или иных типов событий. Эти функции называются обработчиками событий.
Когда мы запускаем интерактивную программу, главная
функция сообщает это описание операционной системе. Когда происходят события ввода, операционная система вызывает
соответствующий обработчик событий. Точно так же операционная система знает из описания, когда и как представлять результаты выполнения этих обработчиков.
В этой книге основное внимание уделяется программам, взаимодействующим с пользователями через графический пользовательский
интерфейс (ГПИ) (англ. graphical user interface, GUI); существуют так-

85

86

Глава 2

же другие виды интерактивных программ, и вы обязательно познакомитесь с ними, продолжив изучать информатику.
Пакетные программы. Как уже упоминалось, пакетная программа сразу получает все свои входные данные и, опираясь на них, вычисляет результат. Ее главная функция ожидает получить некоторые
аргументы, передает их вспомогательным функциям, получает обратно их результаты и объединяет эти результаты в свой окончательный ответ.
После создания программы мы, естественно, приступаем к их использованию. В DrRa­cket пакетные программы запускаются в области взаимодействий, что позволяет наблюдать за их работой.
Еще более полезные программы могут извлекать входные данные
из какого-то файла и выводить результаты в другой файл. В действительности название «пакетная программа» восходит к временам развития вычислительной техники, когда программа читала файл (или
несколько файлов) из пакета перфокарт и помещала результат в другой файл(ы) и также в пакет перфокарт. По идее, пакетная программа
целиком и сразу читает входной файл(ы) и выводит все результаты
в другой файл(ы).
Подобные пакетные программы на основе файлов можно создавать с использованием библиотеки 2htdp/batch-io, которая добавляет
в наш словарь две функции (кроме прочих):
Прежде чем вводить
следующие выражения,
сохраните содержимое
области определений
в файл.

zz read-file, читает содержимое файла и представляет его

как строку;

zz write-file, создает файл и записывает в него указанную

строку.

Эти функции читают и записывают строки в файлы:

> (write-file "sample.dat" "212")
"sample.dat"
> (read-file "sample.dat")
"212"

После первой итерации файл с именем "sample.dat" будет содержать
212
Имена stdout и stdin
являются сокращениями от «standard
output» (стандартное
устройство вывода, или
просто стандартный
вывод) и «standard input»
(стандартное устройство ввода, или просто
стандартный ввод)
соответственно.

Результатом write-file является подтверждение, что строка помещена в файл. Если файл уже существует, его содержимое будет затерто заданной строкой; в противном случае
функция создаст файл и запишет в него данную строку. Второе взаимодействие (read-file "sample.dat") прочитает содержимое файла "sample.dat" и вернет строку "212".
По прагматическим соображениям write-file может принимать в первом аргументе особый токен 'stdout. В этом
случае вывод будет осуществляться на экран, в области взаи­
модействий, например:

Функции и программы

87

> (write-file 'stdout "212\n")
212
'stdout

Аналогично функция read-file может принимать токен 'stdin вмес­
то имени файла и читать данные, вводимые с клавиатуры.
Давайте рассмотрим простой пример создания пакетной Мы не требуем запопрограммы. Предположим, мы хотим создать программу, минать факты, но начто вы будете
которая преобразует температуру, измеренную на градус- деемся,
помнить, где их найти.
нике со шкалой Фаренгейта, в температуру в градусах Цель- Вы знаете, где найти
сия. Не волнуйтесь, мы не собираемся проверять ваши зна- формулу конвертирования температур?
ния по физике; вот формула преобразования:

Естественно, f в этой формуле – это температура по Фаренгейту,
а C – температура по Цельсию. Этой формулы вполне достаточно для
учебника алгебры, но математик или программист должен написать
C(f ) в левой части уравнения, чтобы напомнить читателям, что f – это
заданное значение, а C вычисляется из f.
Эта формула легко переводится на язык BSL:
(define (C f)
(* 5/9 (- f 32)))

Напомним, что 5/9 – это число, точнее рациональная дробь, и что C
зависит от f, что и выражает обозначение функции.
Запускается эта пакетная программа в области взаимодействий
как обычно:
> (C 32)
0
> (C 212)
100
> (C -40)
-40

Но давайте предположим, что мы решили использовать эту функцию в программе, которая читает температуру по Фаренгейту из файла, преобразует ее в температуру по Цельсию и выводит результат
в другой файл.
У нас уже есть формула преобразования на языке BSL, осталось
только написать главную функцию, объединяющую C с имеющимися
элементарными функциями:
(define (convert in out)
(write-file out
(string-append
(number->string
(C
(string->number

88

Глава 2
(read-file in))))
"\n")))

Мы дали главной функции имя convert. Она принимает имена двух
файлов: in – файл со значением температуры по Фаренгейту и out –
куда должен быть записан результат с температурой по Цельсию.
Комбинируя пять функций, convert вычисляет результат. Давайте
внимательно рассмотрим тело convert:
1) (read-file in) извлекает содержимое указанного файла и возвращает его в виде строки;
2) string->number преобразует строку в число;
3) C интерпретирует число как температуру по Фаренгейту и преобразует его в температуру по Цельсию;
4) number->string получает число с температурой по Цельсию
и преобразует его в строку;
5) (write-file out ...) записывает строку с результатом в файл
с именем, указанным в параметре out.
Этот длинный список шагов может показаться ошеломляющим,
даже притом что в нем и не упоминается ссылка на функцию string-append. Приостановитесь и попробуйте сами объяснить, что делает
(string-append ... "\n")

Когда мы изучали арифметику программирования, средняя композиция функций включала две, а иногда три функции. Но имейте
в виду, что программы решают реальные задачи, тогда как упражнения по алгебре просто иллюстрируют идею композиции функций.
Теперь можно поэкспериментировать с convert. Для наФайл "sample.dat"
можно также создать чала используем write-file, чтобы создать файл с исходным
с по­мощью текстового
значением для convert:
редактора.

> (write-file "sample.dat" "212")
"sample.dat"
> (convert "sample.dat" 'stdout)
100
'stdout
> (convert "sample.dat" "out.dat")
"out.dat"
> (read-file "out.dat")
"100"

В первом эксперименте мы использовали stdout, чтобы видеть результат работы convert в области взаимодействий. Во втором эксперименте мы указали имя выходного файла "out.dat". Как и ожидалось,
вызов convert вернул строку с именем этого файла, которую возвращает write-file; мы уже видели этот эффект, когда использовали writefile для записи температуры по Фаренгейту. Затем мы прочитали содержимое выходного файла с по­мощью read-file, но то же самое можно сделать с по­мощью текстового редактора.

Функции и программы

Помимо простого запуска пакетной программы, полезно также
выполнить вычисления в пошаговом режиме. Проверьте еще раз наличие файла "sample.dat" и что в нем находится единственное число,
а затем щелкните на кнопке STEP (Шаг) в DrRa­cket. После этого откроется другое окно, в котором можно увидеть, как протекает процесс вычислений, запускаемый вызовом главной функции пакетной
программы. Сделав это, вы увидите, что процесс в точности совпадает с описанием выше.
Упражнение 31. Вспомните программу letter из раздела 2.3. Вот
как можно запустить эту программу и заставить ее выводить результат в область взаимодействий:
> (write-file
'stdout
(letter "Matthew" "Fisler" "Felleisen"))
Dear Matthew,
We have discovered that all people with the
last name Fisler have won our lottery. So,
Matthew, hurry and pick up your prize.
Sincerely,
Felleisen
'stdout

Конечно, одно из основных достоинств программ заключается в том,
что они могут производить разные результаты для разных входных данных. Запустите letter с тремя входными строками по вашему выбору.
Вот пример пакетной программы, составляющей заготовки писем,
которая читает имена из трех разных файлов и сохраняет письмо
в четвертом файле:
(define (main in-fst in-lst in-signature out)
(write-file out
(letter (read-file in-fst)
(read-file in-lst)
(read-file in-signature))))

Функция main принимает четыре строки: первые три – имена входных файлов, а последняя – имя выходного файла. Она читает по одной строке из первых трех файлов, передает эти строки в letter и полученный результат записывает в файл с именем в out – четвертом
параметре функции main.
Создайте соответствующие файлы, запустите главную функцию
main и проверьте, выводит ли она ожидаемое письмо в заданный
файл. 
Интерактивные программы. Когда-то пакетные программы являлись основой использования компьютеров в производстве, но в настоящее время люди в основном работают с интерактивными программами. Обычно они взаимодействуют с настольными приложениями посредством мыши и клавиатуры. Кроме того, интерактивные

89

90

Глава 2

программы могут реагировать на события, генерируемые компьютером, такие как такты системных часов или поступление сообщения
с другого компьютера.
Упражнение 32. Большинство людей запускают программы не
только на настольных компьютерах, но также на сотовых телефонах,
планшетах и бортовых компьютерах своих автомобилей. Очень скоро
люди будут использовать носимые компьютеры, встроенные в очки,
одежду и спортивное снаряжение. В более отдаленном будущем могут появиться встроенные биокомпьютеры, вживляемые в организм
человека и напрямую взаимодействующие с функциями тела. Попробуйте представить десяток различных форм событий, с которыми
придется иметь дело программам на таких компьютерах. 
Цель этого раздела – познакомить с механикой написания интерактивных программ на BSL. Поскольку многие примеры в этой
книге описывают интерактивные программы, мы будем представлять новые идеи постепенно и осторожно. Начав заниматься проектами интерактивных программ, вы сможете вернуться к этому разделу и прочитать его вновь; второе или третье чтение часто помогает
прояснить особенно сложные аспекты механики.
Чистый компьютер без ничего – бесполезное физическое оборудование. К нему можно прикоснуться, и только. Компьютер становится
полезным после установки программного обеспечения, то есть набора
программ. Обычно в первую очередь на компьютер устанавливается операционная система. Ее задача – управлять компьютером, в том
числе и подключенными устройствами, такими как монитор, клавиа­
тура, мышь, динамики и т. д. Вот как примерно это работает: когда
пользователь нажимает клавишу на клавиатуре, операционная система вызывает функцию, обрабатывающую нажатия клавиш. Мы говорим, что нажатие клавиши является событием клавиатуры, а функция – обработчиком события. Аналогично операционная система
вызывает обработчик тактов системных часов, событий мыши и т. д.
После того как обработчик событий выполнит свою работу, операционной системе может потребоваться изменить изображение на экране, издать звуковой сигнал, распечатать документ или выполнить
какое-то другое действие. Для выполнения этих задач она вызывает
функции, преобразующие данные в звуки, изображения, операции
с принтером и т. д.
Естественно, у разных программ разные потребности. Одна программа может интерпретировать нажатия клавиш как сигналы
управления ядерным реактором; другая передает их текстовому процессору. Чтобы компьютеры могли выполнять эти радикально разные
задачи, разные программы устанавливают разные обработчики событий. То есть программа, управляющая запуском ракеты, использует одни функции для обработки тактов системных часов, а программное обеспечение управления духовкой – другие.
При проектировании интерактивной программы необходимо
иметь способ обозначить одну функцию как отвечающую за обработ-

Функции и программы

ку событий клавиатуры, другую функцию – за обработку событий тактов системных часов, третью – за представление некоторых данных
в виде изображения и т. д. Задача главной функции в интерактивной
программе – сообщить эти обозначения операционной системе –
программной платформе, на которой действует программа.
DrRa­cket – это небольшая операционная система, а BSL – один из ее
языков программирования. Последний поставляется с библиотекой
2htdp/universe, включающей механизм big-bang, посредством которого
программа может сообщить операционной системе, какая функция
и какое событие обрабатывает. Кроме того, big-bang следит за состоя­
нием программы. Для этого в нем есть одно обязательное подвыражение, значение которого становится начальным состоянием программы. В остальном можно считать, что big-bang состоит из одного
обязательного предложения и множества дополнительных предложений. Обязательное предложение to-draw сообщает DrRa­cket, как
отобразить состояние программы, включая начальное. Все остальные необязательные предложения сообщают операционной системе,
что та или иная функция обрабатывает определенное событие. Под
обработкой событий в BSL подразумевается, что функция получает
состояние программы и описание события и генерирует следующее
состояние программы. Поэтому для описания состояния программы
мы часто используем слова текущее состояние программы.
ТЕРМИНОЛОГИЯ. В некотором смысле выражение big-bang описывает, как программа взаимодействует с небольшим сегментом мира.
Этот мир может быть игрой, в которую играет пользователь программы; анимацией, которую смотрит пользователь; или текстовым
редактором, в котором пользователь ведет некоторые заметки. Поэтому исследователи языка программирования часто говорят, что
big-bang – это описание маленького мира, включающее его начальное
состояние, порядок преобразования состояний, порядок отображения состояний и как big-bang будет определять другие атрибуты текущего состояния. Учитывая это, мы также часто говорим о состоянии
мира и даже называем программы big-bang мировыми программами.
КОНЕЦ.
Давайте исследуем эту идею шаг за шагом, начав со следующего
определения:
(define (number->square s)
(square s "solid" "red"))

Функция принимает положительное число и отображает красный
квадрат этого размера. Щелкните на кнопке RUN (Выполнить) и поэкспериментируйте с этой функцией, например:
> (number->square 5)
> (number->square 10)

91

92

Глава 2
> (number->square 20)

Он ведет себя как пакетная программа, принимая число и создавая
изображение, которое DrRa­cket показывает на экране.
Теперь попробуйте выполнить следующее выражение big-bang в области взаимодействий:
> (big-bang 100 [to-draw number->square])

Появится отдельное окно с красным квадратом 100×100 пикселей.
Кроме того, в области взаимодействий DrRa­cket не отобразит приглашение к вводу, как если бы программа продолжала работать, и это
действительно так. Чтобы остановить программу, нажмите кнопку
STOP (Остановить) или просто закройте дополнительное окно:
> (big-bang 100 [to-draw number->square])
100

Когда DrRa­cket останавливает вычисление выражения big-bang, он
возвращает текущее состояние, которым в данном случае является
начальное состояние: 100.
Вот более интересное выражение big-bang:
> (big-bang 100
[to-draw number->square]
[on-tick sub1]
[stop-when zero?])

Это выражение big-bang добавляет два необязательных предложения к предыдущему: предложение on-tick сообщает DrRa­cket, как обрабатывать такты системных часов, а предложение stop-when сообщает, когда остановить программу. Это выражение читается так, с учетом начального состояния 100:
1. С каждым тактом часов вычесть 1 из текущего состояния.
2. Затем проверить истинность выражения zero? для нового состояния, и если оно истинно, то остановить программу.
3. Каждый раз, когда обработчик события возвращает значение,
использовать number->square для отображения состояния в форме квадрата.
Теперь нажмите клавишу return и посмотрите, что произойдет.
Спустя какое-то время выполнение выражений прекратится, и DrRa­
cket выведет число 0.
Выражение big-bang отслеживает текущее состояние. Первоначально это состояние было равно 100. Каждый раз, когда отмеряется очередной такт часов, вызывается обработчик тактов, и текущее состоя­
ние обновляется. То есть состояние big-bang меняется следующим образом:
100, 99, 98, ..., 2, 1, 0

Функции и программы

Когда значение состояния станет равным 0, вычисления прекращаются. Для любого другого состояния от 100 до 1 выражение big-bang
преобразует состояние в изображение, используя number->square, как
указано в предложении to-draw, и в результате в дополнительном
окне отображается красный квадрат, который постепенно уменьшается в размерах в течение 100 тактов часов.
Давайте добавим предложение с обработчиком событий клавиатуры. Прежде всего определим функцию, которая принимает текущее
состояние и строку, описывающую событие клавиатуры, и возвращает новое состояние:
(define (reset s ke)
100)

Эта функция игнорирует свои аргументы и просто возвращает
100 – начальное состояние выражения big-bang, которое мы хотим изменить.
Затем добавим в выражение big-bang предложение on-key:
> (big-bang 100
[to-draw number->square]
[on-tick sub1]
[stop-when zero?]
[on-key reset])

Стоп! Попробуйте объяснить, что произойдет, если ввести это выражение в область взаимодействий, нажать клавишу return, сосчитать до 10 и нажать клавишу a.
Вы увидите, что красный квадрат сначала уменьшается в размерах
на один пиксель с каждым тактом часов. Однако как только вы a, красный квадрат снова увеличится до полного размера, потому что нажатие клавиши a вызывает функцию reset, которая возвращает число
100. Это число станет новым состоянием big-bang, и number->square превратит его в полноразмерный красный квадрат.
Чтобы понять, как вычисляются выражения big-bang, рассмотрим
следующую схематическую версию:
(big-bang cw0
[on-tick tock]
[on-key ke-h]
[on-mouse me-h]
[to-draw render]
[stop-when end?]
...)

Это выражение big-bang определяет три обработчика событий –
tock, ke-h и me-h – и использует предложение stop-when.
Вычисление данного выражения big-bang начинается с текущего
состояния cw0, которое обычно является выражением. DrRa­cket, наша
операционная система, устанавливает значение cw0 как текущее состояние и вызывает render для преобразования текущего состояния
в изображение, которое затем отображается в отдельном окне. На

93

94

Глава 2

самом деле render – это единственное средство, с по­мощью которого
выражение big-bang представляет данные.
Вот как обрабатываются события:
zz с каждым тактом часов DrRa­cket применяет tock к текущему со-

стоянию big-bang и получает результат; big-bang рассматривает
этот результат как следующее текущее состояние;
zz при каждом нажатии клавиши DrRa­cket применяет ke-h к текущему состоянию выражения big-bang и к строке, представляющей клавишу; например, нажатие клавиши а будет представлено строкой "а", а нажатие клавиши со стрелкой влево – строкой
"left". Значение, возвращаемое обработчиком ke-h, big-bang
рассматривает как следующее текущее состояние;
zz каждый раз, когда указатель мыши входит в окно, покидает его,
перемещается по окну или выполняется щелчок мышью, DrRa­
cket применяет me-h к текущему состоянию выражения big-bang,
координатам x и y – события и строке, представляющей событие
мыши; например, нажатие кнопки мыши будет представлено
строкой "button-down". Значение, возвращаемое обработчиком
me-h, big-bang рассматривает как следующее текущее состояние.
Все события обрабатываются по порядку; если кажется, что два события произошли одновременно, то DrRa­cket выступит в роли арбит­
ра и выстроит их в определенном порядке.
После обработки события big-bang использует end? и render для проверки и отображения текущего состояния:
zz (end? Cw) возвращает логическое значение; если это #true, то big-

bang немедленно останавливает вычисления, в противном случае продолжает их;
zz предполагается, что (render cw) создаст изображение, и big-bang
отображает это изображение в отдельном окне.
Таблица 2. Принцип действия выражения big-bang
Текущее состояние
Событие

cw0
e0

cw1
e1

...
...

Такт часов

(tock cw0)

(tock cw1)

...

Нажатие клавиши

(ke-h cw0 e0)

(ke-h cw1 e1)

...

Событие мыши

(me-h cw0 e0 ...) (me-h cw1 e1 ...)

...

Отображение состояния

(render cw0)

...

(render cw1)

Таблица 2 кратко описывает этот процесс. В первой строке перечислены имена текущих состояний. Во второй строке – событий,
возникающих в DrRa­cket: e0, e1 и т. д. Каждое событие ei может быть
тактом часов, нажатием клавиши или событием мыши. В следующих
трех строках описывается, как обрабатываются события:

Функции и программы
zz если e0 – это такт часов, то big-bang вычисляет (tock cw0) и полу-

чает cw1;
zz если e0 – это событие клавиатуры, то big-bang вычисляет (ke-h
cw0 e0) и получает cw1. Обработчик должен применяться к самому событию, потому что часто программы по-разному реагируют на нажатия разных клавиш;
zz если e0 – это событие мыши, то big-bang вычисляет (me-h cw0 e0
...) и получает cw1. Этот вызов показан в схематичной форме,
потому что событие мыши e0 сопровождается несколькими элементами данных – его природой и координатами, – и троеточием мы просто указываем на это;
zz наконец, render превращает текущее состояние в изображение,
как указывает последняя строка. DrRa­cket отображает такие
изображения в отдельном окне.
Столбец под заголовком cw1 показывает, как генерируется следую­
щее текущее состояние cw2 в зависимости от характера события e1.
Давайте применим эту таблицу для интерпретации конкретной
последовательностью событий: пользователь нажимает клавишу a,
затем истекает очередной такт часов, и, наконец, пользователь щелкает мышью, вызывая событие «button down» в позиции (90, 100).
В обозначениях Racket:
1) cw1 является результатом (ke-h cw0 "a");
2) cw2 – результатом (tock cw1);
3) cw3 – результатом (me-h cw2 90 100 "button down").
Мы можем выразить эти три шага в виде последовательности трех
определений:
(define cw1 (ke-h cw0 "a"))
(define cw2 (tock cw1))
(define cw3 (me-h cw2 "button-down" 90 100))

Стоп! Как big-bang отображает каждое из этих трех состояний?
Теперь рассмотрим случай обработки последовательности из трех
тактов часов. В этом случае:
1) cw1 является результатом (tock cw0);
2) cw2 является результатом (tock cw1);
3) cw3 является результатом (tock cw2).
Или если сформулировать на языке BSL:
(define cw1 (tock cw0))
(define cw2 (tock cw1))
(define cw3 (tock cw2))

Фактически то же самое можно выразить в виде единственного выражения:

95

96

Глава 2
(tock (tock (tock cw0)))

Оно определяет состояние, которое вычисляет big-bang после трех
тактов часов. Стоп! Переформулируйте первую последовательность
событий в виде единственного выражения.
Проще говоря, последовательность событий определяет, в каком
порядке big-bang пересекает приведенную выше табл. 2 возможных
состояний, чтобы достичь текущего состояния в каждый момент времени. Конечно, само выражение big-bang никак не изменяет текущее
состояние; оно просто хранит его и при необходимости передает обработчикам событий и другим функциям.
Отсюда легко определить первую интерактивную программу.
Взгляните на листинг 8. Программа состоит из двух определений констант, за которыми следуют три определения функций: main, запускающей интерактивную программу big-bang; place-dot-at, преобразующей текущее состояние в изображение; и stop, игнорирующей свои
входные данные и возвращающей 0.
Листинг 8. Первая интерактивная программа
(define BACKGROUND (empty-scene 100 100))
(define DOT (circle 3 "solid" "red"))
(define (main y)
(big-bang y
[on-tick sub1]
[stop-when zero?]
[to-draw place-dot-at]
[on-key stop]))
(define (place-dot-at y)
(place-image DOT 50 y BACKGROUND))
(define (stop y ke)
0)

После щелчка на кнопке RUN (Выполнить) мы можем попросить
DrRa­cket применить эти функции-обработчики. Вот один из способов
проверить их работу:
> (place-dot-at 89)

> (stop 89 "q")
0

Стоп! Попробуйте теперь объяснить, как main среагирует на нажатие клавиши.
Один из способов узнать, насколько верно ваше предположение, –
запустить функцию main с некоторым разумным числом:
> (main 90)

Функции и программы

Передохните!
К настоящему времени вы могли почувствовать ошеломление от
этих первых двух глав. Они познакомили вас со множеством новых
понятий, включая новый язык, его словарь, его идеи, его идиомы,
инструмент для записи новых слов в этот словарь и способ запуска
программ. Столкнувшись с этим изобилием информации, у многих
из вас может возникнуть вопрос: как создать программу, когда имеется сформулированная постановка задачи. Чтобы ответить на этот
важный вопрос, в следующей главе мы сделаем шаг назад и подробно
рассмотрим систематический подход к проектированию программ.
Так что сделайте передышку и продолжайте, когда будете готовы.

97

3. Как проектировать программы
Как показали первые главы в этой книге, обучение программированию требует овладения многими понятиями. С одной стороны, нужен
язык программирования, определяющий систему обозначений для
описания вычислений. Языки, используемые для формулирования
программ, – это искусственные конструкции, однако процесс освое­
ния языка программирования имеет много общего с освоением ес­
тест­венного языка. Оба имеют словарный запас, грамматику и представление о «фразах».
С другой стороны, очень важно научиться переходить от постановки задачи к программе. Мы должны определить, что уместно в постановке задачи, а что можно игнорировать. Мы обязаны выяснить,
какие данные должна получать программа, какие данные производить и как входные данные соотносятся с выходными. Мы должны
знать или выяснять, поддерживает ли выбранный язык и его библиотеки базовые операции с данными, которые наша программа должна
обрабатывать. В противном случае нам, возможно, придется разработать вспомогательные функции, реализующие эти операции. Наконец, создав программу, мы должны проверить, действительно ли
она выполняет предполагаемые вычисления. При этом могут быть
выявлены всевозможные ошибки, которые необходимо исследовать
и исправить.
Все это звучит довольно сложно, и у кого-то из вас может возникнуть вопрос: почему бы просто не начать экспериментировать тут
и там, оставляя в покое те участки, результаты работы которых выглядят верными? Такой подход к программированию часто называют «гаражным программированием», он широко распространен и во
многих случаях действительно приводит к успеху; иногда с этого
начинается развитие новой компании. Однако никакая компания
не может продавать такие «гаражные программы», потому что разобраться в них смогут только ее авторы.
Хорошая программа поставляется с коротким описанием, объясняющим, что делает программа, какие данные принимает и производит. В идеале такое описание дает некоторую уверенность, позволяя
убедиться, что программа действительно работает. Связь идеальной
программы с постановкой задачи очевидна, поэтому небольшое изменение в постановке задачи легко трансформировать в небольшое
изменение программы. Инженеры-программисты называют такие
программы «программным продуктом».
Вся эта дополнительная работа необходима, поСлово «другие» также
подразумевает самих тому что программисты создают программы не
программистов, для себя. Программисты пишут программы для
которые обычно чтения другими программистами, и, так уж слозабывают все мысли,
жилось, иногда люди запускают эти программы,
которые они вкладывали
в создаваемые чтобы выполнить работу. Большинство программ
программы. представляют собой большие и сложные наборы

Как проектировать программы

взаимодействующих функций, и никто не может написать все эти
функции за один день. Программисты подключаются к проектам,
пишут код, покидают проекты; иные берут их программы и работают над ними. Другая трудность заключается в склонности клиентов
менять свое мнение о задаче, которую они действительно хотят решить. Обычно они правильно формулируют задачу в целом, но часто
ошибаются в некоторых деталях. Хуже того, такие сложные логические конструкции, как программы, почти всегда страдают от человеческих ошибок; проще говоря, программисты тоже могут ошибаться.
В конце концов, кто-то обнаруживает эти ошибки, и программисты
должны их исправить. Им нужно прочитать программный код, написанный месяцы, год или двадцать лет тому назад, и изменить его.
Упражнение 33. Почитайте о проблеме «2000 года». 
Здесь мы представляем рецепт проектирования, который объединяет пошаговый процесс со способом организации программ вокруг
обрабатываемых данных. Для читателей, которые не любят долго
смотреть на пустой экран, этот рецепт предлагает способ систематического движения вперед. Для тех из вас, кто учит других проектировать программы, рецепт послужит инструментом выявления трудностей, которые испытывают новички. Кто-то сможет применить наш
рецепт в других областях, например в медицине, журналистике или
инженерии. Тем, кто хочет стать настоящим программистом, рецепт
проектирования поможет научиться разбираться в существующих
программах и работать с ними, даже если они написаны программистами, не использующими подобные рецепты проектирования.
Остальная часть этой главы посвящена первым шагам в мире рецептов проектирования; а последующие главы и части так или иначе будут уточнять и расширять рецепт.

3.1. Проектирование функций
Информация и данные. Цель программы – описать вычислительный процесс, потребляющий некоторую информацию и производящий новую информацию. В этом смысле программа похожа на инструкции, которые учитель математики дает ученикам начальной
школы. Однако, в отличие от ученика, программа работает не только
с числами: она обрабатывает навигационную информацию, отыскивает адрес человека, переключает флаги или проверяет состояние видеоигры. Вся эта информация поступает из области реального мира,
часто называемой предметной областью программы, и результаты
вычислений, производимых программой, представляют дополнительную информацию об этой области.
Информация играет центральную роль в нашем описании. Информация – это факты о предметной области программы. Для программы, обслуживающей каталог мебели, такие понятия, как «стол
на пяти ножках» или «квадратный стол размером два на два метра»,

99

100

Глава 3

являются фрагментами информации. Игровая программа имеет дело
с предметной областью другого типа, где «пять» может описывать
количество пикселей, на которое перемещается объект за такт часов.
А для программы расчета заработной платы «пять» может означать
процент удержаний.
Чтобы программа могла обрабатывать информацию, она должна
преобразовать ее в некоторую форму представления данных на языке программирования, затем обработать эти данные и по завершении преобразовать полученные данные в информацию. Интерактивная программа может даже смешивать эти шаги, собирая больше информации из внешнего мира по мере необходимости, и передавать
ее между шагами.
Мы используем BSL и DrRa­cket, поэтому вам не нужно беспокоиться о преобразовании информации в данные. В языке BSL функцию
можно применить непосредственно к данным и посмотреть, что она
производит. Это позволяет избежать серьезной «проблемы курицы
и яйца» при создании функций, преобразующих информацию в данные и наоборот. Для простых видов информации такие элементы
программы создаются легко и просто, но для более сложной информации может потребоваться знание, например, приемов синтаксического анализа, а это, в свою очередь, требует большого опыта в разработке программ.
Инженеры-программисты широко используют методологию модель-представление-контроллер (Model-View-Controller, MVC), чтобы
отделить обработку данных от преобразования информации в данные и данных в информацию. В настоящее время общепризнано, что
хорошо спроектированные программные системы обеспечивают это
разделение, хотя большинство вводных книг по программированию
все еще объединяют эти этапы. Таким образом, работа с BSL и DrRa­
cket позволяет сосредоточиться на разработке ядра программы,
а когда вы накопите достаточно опыта в этом, вы сможете научиться
проектировать элементы программ, отвечающие за преобразование
информации в данные и обратно.
В этой книге мы используем два предустановленных учебных пакета, чтобы продемонстрировать разделение данных и информации:
2htdp/batch-io и 2htdp/universe. В этой главе мы приступим к формированию рецептов проектирования интерактивных и неинтерактивных
программ, чтобы вы могли получить представление о том, как создаются полноценные программы. Имейте в виду, что библиотеки многих развитых языков программирования предлагают гораздо больше
контекстов для создания программ, и вам придется, так или иначе,
адаптировать рецепты проектирования под конкретные особенности
языка.
Учитывая центральную роль информации и данных, проектирование программы должно начинаться с определения связей между
фрагментами информации. В частности, мы, программисты, должны
решить, как на выбранном языке программирования представлять

101

Как проектировать программы

соответствующие фрагменты информации в виде данных и как интерпретировать данные при их преобразовании в информацию. Эту
идею поясняет абстрактная диаграмма на рис. 4.
Предметная область

Программа

представление
Информация

Данные
интерпретация

Рис. 4. Преобразование информации в данные и данных в информацию

Чтобы конкретизировать эту идею, рассмотрим несколько примеров. Предположим, вы проектируете программу, которая принимает
и производит числовую информацию. Выбор способа представления
в этом случае прост, но при интерпретации необходимо пояснить,
что означает число, например 42, в данной предметной области:
zz в программе, обрабатывающей изображения, 42 может озна-

чать расстояние в пикселях от верхнего края;

zz в игровой программе 42 может означать расстояние, на которое

переместился игровой объект за один такт часов;
zz в программе, выполняющей физические расчеты, 42 может
озна­чать температуру по шкале Фаренгейта, Цельсия или
Кельвина;
zz в программе, которая ведет каталог мебели, 42 может означать
размер стола;
zz в любой программе, независимо от предметной области, 42 может просто означать количество символов в строке.
Поэтому очень важно знать, как перейти от чисел-информации
к числам-данным и наоборот.
Поскольку эти знания так важны для всех, кто читает Для обозначения
программу, мы часто записываем их в виде комментари- чего-то, подобного
мноев, которые называем определениями данных. Определение «математическому
жеству», инженеры-проданных служит двум целям. Во-первых, дает осмысленное граммисты используют
имя набору данных – классу. А во-вторых, информирует чи- термин «класс».
тателей, как создать экземпляры этого класса и как определить, принадлежит ли некоторый произвольный фрагмент данных
коллекции.
Вот определение данных для одного из примеров, приведенных
выше:

102

Глава 3
; Температура -- это число.
; интерпретация: число выражает температуру в градусах Цельсия

Первая строка вводит имя для коллекции данных – Температура –
и сообщает, что класс состоит из чисел. Например, если вас спросят,​​
является ли число 102 температурой, то вы сможете ответить «да»,
потому что 102 – это число, а все числа представляют температуру.
Точно так же если вас спросят, ​​является ли строка «холодный» температурой, то вы сможете уверенно ответить «нет», потому что никакая
строка в этой программе не является температурой. А если вас попросят представить образец температуры, то вы сможете показать число,
например -400.
Если вам известно, что минимально возможная температура имеет величину примерно –274 °C, то возникает естественный вопрос:
можно ли выразить это знание в определении данных. Поскольку
наши определения данных на самом деле являются простыми описаниями классов на простом человеческом языке, мы действительно
можем дать гораздо более точное определение класса температур,
чем показано выше. Для таких определений данных мы используем
в этой книге стилизованную форму естественного языка, а в следующей главе представим способ наложения ограничений, таких как
«больше -274».
До настоящего момента нам встречались имена четырех классов
данных: Число (Number), Строка (String), Изображение (Image) и Логическое значение (Boolean). При этом формулирование нового определения данных означает не что иное, как введение нового имени
для существующей формы данных, скажем имени «температура» для
чисел. Однако даже этих ограниченных знаний достаточно, чтобы
объяснить схему процесса проектирования.
Теперь вы можете
Процесс проектирования. Как только вы поймете, как
вернуться к разделу
представлять
входную информацию в виде данных и как
«Системное проектиинтерпретировать
выходные данные в виде информации,
рование программ» во
вступлении и особенно проектирование каждой отдельной функции превращается
к рецепту 1.
в простой процесс:
1. Выразить представление информации в виде данных. Для этого достаточно однострочного комментария:
; Мы обозначаем числами длину в сантиметрах.

Сформулировать определения классов данных, которые считаются критически важными для успеха программы, как мы сделали это с классом Температура.
2. Записать сигнатуру, описание назначения и заголовок функции.
Сигнатура функции – это комментарий, сообщающий читателям вашего проекта, сколько входных параметров принимает
ваша функция, каким классам они принадлежат и какие данные эта функция производит. Вот три примера:

Как проектировать программы
zz на входе – строка, на выходе – число:
; Строка -> Число

zz на входе – температура, на выходе – строка:
; Температура -> Строка

Как указывает эта сигнатура, введение псевдонима для существующей формы данных позволяет легко понять ваши
намерения.
Тем не менее мы советуем пока воздержаться от определения таких псевдонимов, потому что это может вызвать путаницу. Чтобы уметь находить правильный баланс между потребностью в новых именах и удобочитаемостью программ,
нужна практика, а нам еще нужно понять более важные
идеи;
zz на входе число, строка и изображение:
; Число Строка Изображение -> Изображение

Стоп! Ответьте на вопрос: что получается на выходе этой
функции?
Описанием назначения в языке BSL называется однострочный
комментарий, описывающий цель функции. Если вы затрудняе­
тесь сформулировать цель, то запишите как можно более короткий ответ на следующий вопрос:
Что вычисляет эта функция?
Каждый читатель вашей программы должен понимать, что
вычисляют ваши функции, без необходимости читать саму
функцию.
Для программы, состоящей из множества функций, тоже должно быть сформулировано описание назначения. Вообще говоря, хорошие программисты пишут два описания назначения:
одно – для читателя, которому, возможно, понадобится изменить код, и другое – для человека, который захочет использовать программу, не читая ее код.
Наконец, заголовок – это упрощенное определение функции.
Выберите одно имя переменной для каждого класса входных
данных в сигнатуре, а роль тела функции может играть любой
фрагмент данных выходного класса. Вот три заголовка функций, которые соответствуют трем сигнатурам, упомянутым
выше:
zz (define (f a-string) 0);
zz (define (g n) "a");
zz (define (h num str img) (empty-scene 100 100)).

103

104

Глава 3

Имена параметров здесь отражают тип данных, представляемых этими параметрами. Иногда предпочтительнее использовать имена, подсказывающие назначение параметра.
Формулируя описание назначения, часто бывает полезно использовать имена параметров, поясняющие вычисления. Например:
; Число Строка Изображение -> Изображение
; добавляет строку s в изображение img,
; с отступом в y пикселей от верхнего края и 10 -- от левого края
(define (add-image y s img)
(empty-scene 100 100))

Теперь можете щелкнуть на кнопке RUN (Выполнить) и поэкспериментировать с функцией. Конечно, результат всегда будет
одним и тем же, что делает эксперименты довольно скучными.
3. Проиллюстрируйте сигнатуру и описание назначения несколькими примерами применения функции. Для этого выберите по
одному экземпляру данных каждого входного класса из сигнатуры и покажите, каким должен быть результат.
Предположим, что вы разрабатываете функцию, которая вычисляет площадь квадрата. Очевидно, что эта функция принимает длину стороны квадрата, которую проще всего представить числом (положительным). Допустим, вы сделали первый
шаг в соответствии с рецептом и теперь добавляете примеры
между описанием назначения и заголовком:
; Число -> Число
; вычисляет площадь квадрата со стороной len
; дано: 2, ожидаемый результат: 4
; дано: 7, ожидаемый результат: 49
(define (area-of-square len) 0)

Термин
«инвентарь»
предложил
Стивен Блох
(Stephen Bloch).

4. Следующий шаг – составить инвентарь, который поможет понять, что дано и что нужно вычислить. В случае с простыми
функциями, которые мы рассматриваем в данный момент, мы
знаем, что они получают данные через параметры.
Параметры представляют пока неизвестные нам значения, но
мы знаем, что, опираясь на эти неизвестные данные, функция
должна вычислить свой результат. Чтобы напомнить себе об
этом факте, заменим тело функции макетом.
На данный момент макет содержит только параметры, поэтому
предыдущий пример выглядит так:
(define (area-of-square len)
(... len ...))

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

Как проектировать программы

5. Настал момент написать код. В общем случае написание кода
означает программирование, но в более узком смысле этого
слова, а именно – написание выполняемых выражений и определений функций.
Для нас написание кода означает замену тела функции выражением, которое использует элементы макета и вычисляет то,
о чем говорится в описании назначения. Вот полное определение area-of-square:
; Число -> Число
; вычисляет площадь квадрата со стороной len
; дано: 2, ожидаемый результат: 4
; дано: 7, ожидаемый результат: 49
(define (area-of-square len)
(sqr len))

Чтобы завершить функцию add-image, требуется немного больше усилий: см. листинг 9. В частности, функция должна преобразовать заданную строку s в изображение и поместить его
в заданную сцену.
6. Последний шаг в правильном процессе проектирования – проверка функции на заранее придуманных вами примерах. Пока
что тестирование выполняется так: щелкните на кнопке RUN
(Выполнить) и введите примеры выражений применения
функции в области взаимодействий:
> (area-of-square 2)
4
> (area-of-square 7)
49

Листинг 9. Окончательный результат после пятого шага
; Число Строка Изображение -> Изображение
; добавляет строку s в изображение img,
; с отступом в y пикселей от верхнего края и 10 -- от левого края
; дано:
;
y = 5,
;
s = "hello", и
;
img = (empty-scene 100 100)
; ожидаемый результат:
;
(place-image (text "hello" 10 "red") 10 5 ...)
;
где ... -- это (empty-scene 100 100)
(define (add-image y s img)
(place-image (text s 10 "red") 10 y img))

Фактические результаты должны совпадать с ожидаемыми;
обязательно проверьте все результаты и убедитесь, что они
в точности соответствуют указанным в примерах. Если какой-то из фактических результатов не соответствует ожидаемому, проверьте три возможных случая:
a) вы ошиблись в расчетах и неверно определили ожидаемый
результат для некоторых примеров;

105

106

Глава 3

b) определение функции содержит ошибку и дает неверный
результат. В этом случае в вашей программе имеется логическая ошибка, известная также как жучок (bug);
c) ошибки имеют место и в примерах, и в определении функции.
Обнаружив несоответствие между ожидаемыми и фактическими результатами, мы рекомендуем сначала проверить
правильность ожидаемых результатов. Если они верные, то
ошибка, скорее всего, скрыта в определении функции. Иначе
исправьте пример и снова выполните тесты. Если проблема
осталась, то, возможно, вы столкнулись с третьей, более редкой ситуацией.

3.2. Практические упражнения: функции
Несколько первых упражнений из представленных ниже почти в точности повторяют упражнения из раздела 2.1, отличаясь только тем,
что вместо слова «определите» здесь используется слово «спроектируйте». Это означает, что вы должны создать эти функции, следуя
рецепту проектирования, и ваши решения должны включать все элементы, предполагаемые рецептом.
Как следует из названия раздела, эти упражнения имеют целью
помочь вам закрепить процесс проектирования на практике. Пока
шаги не вошли у вас в привычку, никогда не пропускайте их, потому
что иначе увеличивается риск допустить ошибки, которых легко избежать. В программировании еще много места для сложных ошибок,
и не стоит тратить время на глупости.
Упражнение 34. Спроектируйте функцию string-first, которая извлекает первый символ (1String) из непустой строки. Считайте пока,
что функция никогда не будет получать пустые строки. 
Упражнение 35. Спроектируйте функцию string-last, которая извлекает последний символ (1String) из непустой строки. 
Упражнение 36. Спроектируйте функцию image-area, которая подсчитывает количество пикселей в заданном изображении. 
Упражнение 37. Спроектируйте функцию string-rest, которая создает строку, подобную данной, но без первого символа. 
Упражнение 38. Спроектируйте функцию string-remove-last, которая создает строку, подобную данной, но без последнего символа. 

3.3. Знание предметной области
У многих начинающих программистов возникает естественный вопрос: какие знания необходимы, чтобы определить тело функции.
Нетрудно догадаться, что для этого необходимо знать и понимать

Как проектировать программы

предметную область программы. Существует две формы такого знания предметной области:
1) знания из внешних предметных областей, таких как математика, музыка, биология, гражданское строительство, искусство
и т. д. Поскольку программисты не могут знать все предметные
области, где применяются вычисления, они должны быть готовы понимать язык различных предметных областей, чтобы обсуждать проблемы с экспертами в этих областях. Математика
находится на пересечении многих, но не всех предметных областей. Поэтому программистам приходится осваивать новые
термины, решая проблемы с экспертами в предметной области;
2) знание библиотечных функций, доступных в выбранном языке
программирования. Если ваша задача – перевести на язык программирования математическую формулу, включающую функцию тангенса, то выбранный вами язык должен иметь такую​​
функцию, как, например, tan в BSL. Если ваша задача связана
с графикой, то вам пригодится знание возможностей библиотеки 2htdp/image.
Поскольку нельзя заранее предсказать, в какой предметной области
вам придется работать или какой язык программирования вы будете
использовать, крайне важно, чтобы у вас было четкое представление
обо всех возможностях любых языков программирования, которые
существуют и подходят для решения ваших задач. В противном случае вашу работу возьмет на себя какой-нибудь эксперт в предметной
области с недостаточными знаниями в области программирования.
Распознать задачи, требующие знания предметной области, можно по определениям данных. В определениях данных используются
классы, поддерживаемые выбранным языком программирования,
поэтому определение тела функции (и программы) в основном зависит от опыта в данной области. Позже, когда мы познакомимся с составными формами данных, проектирование функций потребует от
нас знаний в области информатики.

3.4. От функций к программам
Не все программы состоят из определения единственной функции.
Большинство программ состоят из нескольких функций; многие
также используют определения констант. Но в любом случае важно
систематически проектировать каждую функцию, хотя глобальные
константы и вспомогательные функции немного меняют процесс
проектирования.
После определения глобальных констант ваши функции могут использовать их для вычисления результатов. Чтобы напомнить себе об
их существовании, эти константы можно добавлять в макеты; в кон-

107

108

Глава 3

це концов, они относятся к элементам, которые могут способствовать
определению функции.
Потребность в множестве функций возникает, например, потому,
что интерактивным программам необходимы функции, обрабатывающие события от клавиатуры и мыши, преобразующие текущее состояние в музыку, и, возможно, многие другие. Даже неинтерактивным программам может потребоваться несколько различных функций для решения нескольких отдельных задач. Иногда постановка
проблемы сама предлагает эти задачи, а иногда, в процессе разработки какой-либо функции, может возникнуть потребность во вспомогательных функциях.
По этим причинам мы рекомендуем создать и сохранить
Термином «список
список
необходимых функций – список желаний. Каждый элежеланий» мы обязаны
Джону Стоуну мент в списке желаний должен включать: описательное имя
(John Stone). функции, ее сигнатуру и описание назначения. Создание неинтерактивной программы начинайте с добавления главной
функции в список желаний и приступайте к ее проектированию. Создание интерактивной программы можно начать с добавления в список обработчиков событий, функции stop-when и функции scene-rendering. Пока список не опустеет, выбирайте из него очередной элемент
и проектируйте функцию. Если во время проектирования обнаружится, что вам нужна еще одна функция, добавьте ее в список. Когда список опустеет, программа будет готова.

3.5. О тестировании
Тестирование быстро превращается в трудоемкую работу. Конечно,
запускать небольшие программы в области взаимодействий несложно, но для этого требуется много механического труда и сложных
проверок. По мере развития своих систем программистам приходится проводить множество проверок. Вскоре эта работа становится непосильной, и программисты начинают пренебрегать ею. В то же время тестирование – это самый главный инструмент для обнаружения
и устранения недостатков. Небрежное тестирование влечет просачивание ошибок в функции, то есть появление функций со скрытыми
дефектами, а функции с дефектами тормозят развитие проекта, часто
самым неожиданным образом.
Поэтому очень важно автоматизировать тестирование, чтобы не
проводить его вручную. Подобно многим языкам программирования, BSL имеет свои средства тестирования, и DrRa­cket знает об этом.
Для знакомства с этими средствами тестирования вернемся к функции, преобразующей температуру по Фаренгейту в температуру по
Цельсию, представленную в разделе 2.5. Вот ее определение:
; Число -> Число
; преобразует температуру по Фаренгейту в температуру по Цельсию

Как проектировать программы
; дано: 32, ожидаемый результат: 0
; дано: 212, ожидаемый результат: 100
; дано: -40, ожидаемый результат: -40
(define (f2c f)
(* 5/9 (- f 32)))

Чтобы протестировать функцию, необходимо трижды применить
ее и сравнить полученные результаты с ожидаемыми. Вот как сформулировать эти тесты в области определений DrRa­cket:
(check-expect (f2c -40) -40)
(check-expect (f2c 32) 0)
(check-expect (f2c 212) 100)

Если теперь щелкнуть на кнопке RUN (Выполнить), то вы увидите
отчет BSL о том, что программа успешно прошла все три теста и от вас
ничего больше не требуется.
Кроме возможности автоматически запускать тесты в форме
check-expect, поддержка тестирования предлагает еще одно преимущество: вывод сообщений об ошибках, если тесты терпят неудачу.
Чтобы увидеть, как это работает, измените один из тестов так, чтобы
результат сравнения ожидаемого результата с фактическим оказался
ложным, например:
(check-expect (f2c -40) 40)

Теперь после щелчка на кнопке RUN (Выполнить) появится дополнительное окно с текстом, сообщающим, что один из трех тестов
потерпел неудачу. Для неудачного теста в окне будут показаны: вычисленное значение, результат вызова функции (-40); ожидавшееся
значение (40) и гиперссылка на определение теста, потерпевшего неудачу.
Листинг 10. Тестирование в BSL
; Число -> Число
; преобразует температуру по Фаренгейту в температуру по Цельсию
(check-expect (f2c -40) -40)
(check-expect (f2c 32) 0)
(check-expect (f2c 212) 100)
(define (f2c f)
(* 5/9 (- f 32)))

Определения check-expect можно разместить выше или ниже определений тестируемых функций. После щелчка на кнопке RUN (Выполнить) DrRa­cket выберет все определения check-expect и выполнит их
после добавления определений всех функций в «словарь» операций.
В листинге 10 показано, как использовать эту свободу и объеди­нить
примеры с шагом тестирования. Вместо записи примеров в виде комментариев их можно преобразовать непосредственно в тесты. Закончив проектировать функцию, щелкните на кнопке RUN (Выполнить) –

109

110

Глава 3

и тесты выполнятся автоматически. И если позже вам понадобится
изменить функцию по какой-либо причине, следующий щелчок на
кнопке RUN (Выполнить) поможет вам проверить функцию повторно.
И последнее, но не менее важное: check-expect также может выполнять проверки с изображениями, то есть позволяет протестировать
функции, создающие изображения. Предположим, вы решили спроектировать функцию render, которая помещает изображение автомобиля, с именем CAR, в сцену с именем BACKGROUND. Для этой функции
можно сформулировать следующие тесты:
(check-expect (render 50)
)
(check-expect (render 200)
)
Дополнительные
способы формулирования тестов вы найдете
в интермеццо 1.

Альтернативный вариант:
(check-expect (render 50)
(place-image CAR 50 Y-CAR BACKGROUND))
(check-expect (render 200)
(place-image CAR 200 Y-CAR BACKGROUND))

Этот альтернативный подход помогает понять, как выразить тело
функции, и поэтому он предпочтительнее. Один из способов разработки таких выражений – проведение экспериментов в области взаи­
модействий.
Поскольку DrRa­cket дает такую удобную возможность автоматизации тестов, мы немедленно переключимся на этот стиль тестирования и будем использовать его на протяжении всей оставшейся части
книги. Эта форма тестирования называется модульным тестированием, и фреймворк модульного тестирования в BSL специально предназначен для начинающих программистов. Однажды вы перейдете
на какой-нибудь другой язык программирования, и одной из ваших
первых задач станет выбор подходящего фреймворка модульного
тес­тирования.

3.6. Проектирование интерактивных
программ
В предыдущей главе мы специально познакомили вас с библиотекой
2htdp/universe, чтобы в этом разделе показать, как рецепт проектирования, кроме всего прочего, помогает систематически проектировать
программы. Он начинается с краткого обзора библиотеки 2htdp/universe, где будет представлен список определений данных и сигнатур
функций, а затем представит рецепт проектирования программ.
Учебный пакет предполагает, что программист разработает определение данных, представляющее состояние программы, и функцию
render, которая знает, как создать изображение для каждого возмож-

Как проектировать программы

ного состояния. В зависимости от потребностей программы программист должен также спроектировать функции, обрабатывающие такты
часов, нажатия клавиш и события мыши. Наконец, интерактивной
программе может потребоваться остановиться по достижении конечного состояния; функция end? должна распознавать такие конечные
состояния. Эта идея схематично представлена в листинге 11.
Листинг 11. Список желаний для проектирования программ
; СостояниеМира: данные, представляющие состояние мира в некоторый момент (cw)
; СостояниеМира -> Изображение
; когда это необходимо, big-bang получит изображение, соответствующее
; текущему состоянию мира, вычислив выражение (render cw)
(define (render cw) ...)
; СостояниеМира -> СостояниеМира
; с каждым тактом часов big-bang будет получать следующее
; состояние мира из (clock-tick-handler cw)
(define (clock-tick-handler cw) ...)
; СостояниеМира Строка -> СостояниеМира
; по нажатии каждой клавши big-bang будет получать следующее
; состояние из (keystroke-handler cw ke), где ke представляет клавишу
(define (keystroke-handler cw ke) ...)
; СостояниеМира Число Число Строка -> СостояниеМира
; для каждого действия мышью big-bang будет получать следующее
; состояние из (mouse-event-handler cw x y me), где x и y -; координаты события, а me -- его описание
(define (mouse-event-handler cw x y me) ...)
; СостояниеМира -> ЛогическоеЗначение
; после каждого события big-bang вычислит (end? cw)
(define (end? cw) ...)

Предположим, что вы уже имеете упрощенное представление о работе big-bang и можете сосредоточиться на действительно важной задаче проектирования программ. Давайте создадим конкретный пример, следуя рецепту дизайна.
Задача. Спроектируйте программу, которая перемещает автомобиль в пределах холста слева направо, по три пикселя за такт
часов.
С такой постановкой задачи легко представить, как будут выглядеть сцены, генерируемые программой:

В этой книге мы часто будем называть предметную область интер­
активной программы big-bang «миром» и говорить о проектировании
«мировых программ».

111

112

Глава 3

Рецепт проектирования мировых программ, так же как рецепт
проектирования функций, – это инструмент, помогающий постепенно перейти от постановки задачи к действующей программе. Он
включает три больших шага и один маленький.
1. Для всех свойств мира, которые остаются неизменными с течением времени и необходимы для визуализации в виде изображения, создайте константы. В BSL такие константы создаются
через определения. Применительно к мировым программам
мы различаем два вида констант:
a) «физические» константы описывают общие атрибуты объектов в мире, такие как скорость движения объекта, его цвет,
высоту, ширину, радиус и т. д. Конечно, на самом деле эти
константы не относятся к физическим фактам, но многие из
них аналогичны физическим аспектам реального мира.
В контексте нашей задачи такими «физическими» константами являются радиус колес автомобиля и расстояние между колесами:
(define WIDTH-OF-WORLD 200)
(define WHEEL-RADIUS 5)
(define WHEEL-DISTANCE (* WHEEL-RADIUS 5))

Обратите внимание, что вторая константа вычисляется на
основе первой;
b) графические константы – изображения предметов в мире.
Программа будет объединять их в изображения, представляющие полное состояние мира.
Вот графические константы с изображениями колес нашего
Мы настоятельно автомобиля:

советуем поэкспериментировать в области взаимодействий
DrRa­cket и попробовать
самостоятельно
создать такие графические константы.

(define WHEEL
(circle WHEEL-RADIUS "solid" "black"))
(define SPACE
(rectangle ... WHEEL-RADIUS ... "white"))
(define BOTH-WHEELS
(beside WHEEL SPACE WHEEL))

Обычно графические константы вычисляются, и в вычислениях часто используются физические константы и другие
графические константы.
Хорошей практикой считается снабжать определения констант
комментариями, объясняющими их назначение.
2. Свойства, меняющиеся со временем – с каждым тактом часов,
после нажатия клавиш или действий мышью, – определяют текущее состояние мира. Ваша задача – разработать представление данных для всех возможных состояний мира. Результатом
должно быть определение данных, сопровождаемое коммен-

Как проектировать программы

тарием, из которого читатели смогут узнать, как информация
о мире преобразуется в данные и как данные интерпретируются в информацию о мире.
Для представления состояния мира выбирайте простые формы
данных.
В нашем примере с течением времени изменяется расстояние
от автомобиля до левого края холста. Расстояние до правого
края тоже меняется, но для создания изображения достаточно
расстояния до одного края, левого или правого. Расстояние измеряется числами, поэтому вполне подойдет следующее определение данных:
; СостояниеМира -- это число.
; интерпретация: число пикселей от левого края
; сцены до автомобиля

Как вариант в роли состояния мира можно использовать счетчик прошедших тактов часов. Реализацию этого варианта мы
оставляем вам в качестве самостоятельного упражнения.
3. После определения представления данных, описывающих состояние мира, нужно спроектировать ряд функций, чтобы на их
основе можно было сформировать правильное выражение bigbang.
Прежде всего вам понадобится функция, преобразующая любое заданное состояние в изображение, с по­мощью которой
выражение big-bang сможет отобразить последовательность состояний в виде изображений:
; render

Затем вы должны решить, какие события будут изменять те или
иные аспекты состояния мира. В зависимости от решения вам
потребуется спроектировать все или часть из следующих трех
функций:
; clock-tick-handler
; keystroke-handler
; mouse-event-handler

Наконец, если в постановке задачи оговаривается, что программа должна остановиться после достижения некоторого состояния, то вы должны спроектировать функцию:
; end?

Обобщенные сигнатуры и описание назначения этих функций
вы найдете в листинге 11. Измените эти обобщенные формулировки в соответствии с конкретными задачами, которые вы
решаете, чтобы читатели знали, что эти функции вычисляют.
Проще говоря, желание спроектировать интерактивную про-

113

114

Глава 3

грамму автоматически влечет добавление нескольких начальных пунктов в список желаний. Выполните эти пункты один за
другим, и вы получите законченную мировую программу.
Давайте воплотим этот шаг. Функция big-bang требует в обязательном порядке передать ей функцию отображения, поэтому
нам остается только выяснить, понадобятся ли нам какие-либо
функции обработки событий. Согласно постановке нашей задачи, автомобиль должен двигаться слева направо с течением
времени, поэтому нам определенно потребуется функция, обрабатывающая события хода часов. Таким образом, получаем
такой список желаний:
; СостояниеМира -> Изображение
; помещает изображение автомобиля в сцену BACKGROUND
; на расстоянии x пикселей от левого края
(define (render x)
BACKGROUND)
; СостояниеМира -> СостояниеМира
; прибавляет 3 к x, чтобы переместить автомобиль вправо
(define (tock x)
x)

Обратите внимание, как мы адаптировали формулировки целей к нашей задаче, чтобы рассказать, как big-bang будет использовать эти функции.
4. Наконец, необходимо определить главную функцию (main).
В отличие от всех других функций, главные функции мировых программ не требуют проектирования или тестирования.
Единственное их предназначение – дать простую возможность
запустить мировую программу из области взаимодействий
в DrRa­cket.
Единственное решение, которое потребуется принять, касается
аргументов main. В данной задаче главная функция принимает
один аргумент: начальное состояние мира:
; СостояниеМира -> СостояниеМира
; запускает программу с некоторым начальным состоянием
(define (main ws)
(big-bang ws
[on-tick tock]
[to-draw render]))

Теперь можно запустить эту интерактивную программу:
> (main 13)

и наблюдать, как автомобиль начнет движение слева направо,
отступив 13 пикселей от левого края. Программа остановится,
когда вы закроете окно big-bang. Помните, что после остановки
big-bang возвращает текущее состояние мира.

Как проектировать программы

115

Конечно же, совсем необязательно использовать имя «Состояние­
Мира» для обозначения класса данных, представляющих состояния
мира. Вы можете использовать любое другое имя, при условии что
будете последовательно применять его в сигнатурах функций, занимающихся обработкой событий. Точно так же необязательно использовать имена tock, end? или render. Эти функции можно назвать
как угодно, главное, чтобы вы использовали те же имена в выра­
жении big-bang. Наконец, вы, возможно, заметили, что элементы
в выражении big-bang могут следовать в любом порядке, с единст­венным условием: начальное состояние всегда должно следовать
первым.
Теперь рассмотрим остальную часть процесса проекти- Хорошие программисты
рования программы, используя рецепт проектирования устанавливают
для функций и другие идеи, с которыми мы познакомились единственную точку
управления для любых
к данному моменту.
аспектов своих
Упражнение 39. Хороший программист обеспечит воз- программ, а не только
можность увеличения или уменьшения изображения, тако- для графических
го как CAR, изменением значения в одном месте программы. констант. К этому
Мы начали разработку изображения автомобиля с простого вопросу мы еще не раз
вернемся в последующих
определения:
главах.
(define WHEEL-RADIUS 5)

Определение WHEEL-DISTANCE основано на радиусе колеса. Соответственно, увеличение значения WHEEL-RADIUS с 5 до 10 увеличит размер
изображения автомобиля вдвое. Такой способ организации программ
называют единственной точкой управления, и эта идея должна максимально использоваться при проектировании.
Определите свое изображение автомобиля так, чтобы WHEEL-RADIUS
оставался единственной точкой управления. 
Следующий пункт в списке желаний – функция обработки тактов
часов:
; СостояниеМира -> СостояниеМира
; перемещает автомобиль на 3 пикселя вправо с каждым тактом часов
(define (tock ws) ws)

Поскольку роль состояния мира играет расстояние между левым
краем холста и автомобилем и автомобиль движется со скоростью
три пикселя за такт часов, краткое описание назначения объединяет
эти два факта в один. Это также упрощает создание примеров и определение функции:
; СостояниеМира -> СостояниеМира
; перемещает автомобиль на 3 пикселя вправо с каждым тактом часов
; примеры:
;
дано: 20, ожидаемый результат 23
;
дано: 78, ожидаемый результат 81
(define (tock ws)
(+ ws 3))

116

Глава 3

Последний шаг в процессе проектирования требует убедиться
в верности примеров. Итак, щелкаем на кнопке RUN (Выполнить)
и проверяем следующие выражения:
> (tock 20)
23
> (tock 78)
81

Поскольку полученные результаты совпали с ожидаемыми, проектирование функции tock можно считать завершенным.
Упражнение 40. Оформите примеры в виде тестов на языке BSL,
то есть в форме выражений check-expect. Введите ошибку в код функции и повторно выполните тесты. 
Второй пункт в списке желаний – функция, преобразующая состоя­
ние мира в изображение:
; СостояниеМира -> Изображение
; помещает изображение автомобиля в сцену BACKGROUND
; в соответствии с состоянием мира
(define (render ws)
BACKGROUND)

Чтобы добавить примеры для функции render, составим таблицу (см. табл. 3). В ней перечислены заданные и ожидаемые состоя­
ния мира, а также изображения сцен (в верхней половине), которые должны получиться. Для ваших первых нескольких функций
отобра­жения состояния мира вы можете нарисовать эти изображения вручную.
Таблица 3. Примеры для программы, перемещающей автомобиль
cw
50

Изображение

100
150
200
cw
50
100
150
200

Выражение
(place-image CAR 50 Y-CAR BACKGROUND)
(place-image CAR 100 Y-CAR BACKGROUND)
(place-image CAR 150 Y-CAR BACKGROUND)
(place-image CAR 200 Y-CAR BACKGROUND)

Такая таблица наглядно объясняет, что будет возвращать функция – движущийся автомобиль, – но она не объясняет, как функция
получает этот результат. Чтобы исправить данный недостаток, мы
рекомендуем записывать выражения, подобные тем, что показаны
в нижней половине табл. 3, которые создают изображения, показанные в верхней половине. Имена, состоящие исключительно из заглав-

Как проектировать программы

ных букв, – это, как нетрудно догадаться, константы: изображение автомобиля (CAR), фиксированная координата y (Y-CAR) и фоновая сцена
(BACKGROUND), которая в начальный момент времени пуста.
Эта расширенная таблица представляет образец формулы, используемой в теле функции render:
; СостояниеМира -> Изображение
; помещает изображение автомобиля в сцену BACKGROUND
; в соответствии с состоянием мира
(define (render cw)
(place-image CAR cw Y-CAR BACKGROUND))

И это почти все, что нужно для создания простой мировой программы.
Упражнение 41. Завершите пример и запустите программу. То
есть после решения упражнения 39 определите константы BACKGROUND
и Y-CAR. Затем добавьте все определения функций, включая тесты для
них. Добившись успешного выполнения всех тестов, добавьте дерево
для создания декораций. Мы использовали такой код:
(define tree
(underlay/xy (circle 10 "solid" "green")
9 15
(rectangle 2 20 "solid" "brown")))

чтобы создать фигуру, напоминающую дерево. Также добавьте в выражение big-bang предложение, останавливающее анимацию, как
только автомобиль скроется за правым краем сцены. 
Определившись с представлением состояния мира в виде данных,
внимательный программист, возможно, не раз пересмотрит это фундаментальное проектное решение на последующих этапах процесса
проектирования. Например, в нашей задаче мы представляем автомобиль как точку. Но изображение автомобиля – это не просто математическая точка, не имеющая ни ширины, ни высоты. Поэтому
интерпретация предложения «количество пикселей от левого края»
не выглядит однозначной. Что имеется в виду? Расстояние от левого края сцены до левой стороны автомобиля? До точки в его центре?
Или до правой стороны? Мы проигнорировали эту проблему и положились в принятии решений на примитивы обработки изображений
в BSL. Если вам не нравится результат, вернитесь к определению данных и измените его или его интерпретацию по своему вкусу.
Упражнение 42. Измените интерпретацию определения данных
так, чтобы состояние обозначало координату X правой стороны автомобиля. 
Упражнение 43. Рассмотрим ту же постановку задачи с определением данных на основе времени:
; СостояниеАнимации – это число.
; интерпретация: число тактов часов,
; прошедших с начала анимации

117

118

Глава 3

Как и в предыдущем определении данных, это также относит состояние мира к классу чисел. Однако его интерпретация объясняет,
что данное число означает нечто совершенно иное.
Спроектируйте функции tock и render. Затем определите выражение big-bang, чтобы вновь получить анимацию автомобиля, движущегося слева направо.
Как, по вашему мнению, эта программа связана с animate
Иногда бывает сложно
из пролога?
правильно обрабатыИспользуйте определение данных, чтобы спроектировать движения мыши,
потому что они вать программу, которая перемещает автомобиль по синуне совсем такие, соидальной траектории. (В жизни не надо так ездить!) 
какими кажутся.
Мы заканчиваем этот раздел иллюстрацией обработки
Чтобы коротко познакособытий
мыши, которая также демонстрирует преимущест­
миться с причинами,
прочтите примечание ва разделения представления и модели. Предположим, мы
«О мышах и клавишах» решили дать людям возможность перемещать автомобиль
в онлайн-версии книги. через «гиперпространство»:
Задача. Спроектируйте программу, которая перемещает автомобиль в пределах холста слева направо, по три пикселя за такт
часов. В ответ на щелчок мышью в любой точке в пределах
холста программа должна переместить автомобиль в точку
с координатой x, соответствующей координате x щелчка.
Здесь жирным выделено предложение, дополняющее формулировку предыдущей задачи.
Столкнувшись с изменившейся задачей, мы используем процесс
проектирования, чтобы внести необходимые изменения. При правильном применении этот процесс естественным образом определяет, что нужно добавить в существующую программу, чтобы удовлетворить расширенную постановку задачи. Итак, начнем.
1. Новых свойств не добавилось, а значит, нам не нужны новые
константы.
2. Программа все так же обрабатывает единственное свойство,
изменяющееся с течением времени, – координату x автомобиля. Следовательно, имеющегося представления данных достаточно.
3. Обновленная постановка задачи требует добавить обработчик
событий мыши, но не требует отказаться от перемещения автомобиля по часам. Поэтому добавляем следующий пункт в список желаний:
; СостояниеМира Число Число Строка -> СостояниеМира
; перемещает автомобиль в координату x события,
; если это событие "button-down"
(define (hyper x-position-of-car x-mouse y-mouse me)
x-position-of-car)

3. Наконец, нам нужно изменить функцию main и включить обработку событий мыши. Для этого достаточно добавить пред-

Как проектировать программы

119

ложение on-mouse, которое напрямую связано с новым пунктом
в нашем списке желаний:
(define (main ws)
(big-bang ws
[on-tick tock]
[on-mouse hyper]
[to-draw render]))

Как видите, изменившаяся постановка задачи требует лишь
добавить обработку щелчков мышью, а все остальное остается
прежним.
Теперь осталось лишь спроектировать еще одну функцию, и для
этого мы используем рецепт проектирования функций.
Первые два шага рецепта проектирования функций мы уже выполнили. Теперь следующий шаг – определить несколько примеров применения функции:
; СостояниеМира Число Число Строка -> СостояниеМира
; перемещает автомобиль в координату x события,
; если это событие "button-down"
; дано: 21 10 20 "enter"
; ожидаемый результат: 21
; дано: 42 10 20 "button-down"
; ожидаемый результат: 10
; дано: 42 10 20 "move"
; ожидаемый результат: 42
(define (hyper x-position-of-car x-mouse y-mouse me)
x-position-of-car)

В примерах говорится, что если в строковом параметре передается строка "button-down", то функция должна вернуть значение x-mouse;
во всех остальных случаях должно возвращаться значение x-position-of-car.
Упражнение 44. Оформите примеры в виде тестов BSL. Щелкните
на кнопке RUN (Выполнить) и посмотрите, как все они терпят не­
удачу. 
Чтобы завершить определение функции, вспомним, о чем В следующей главе мы
рассказывалось в прологе, в частности об условном выраже- подробно расскажем
проектировании
нии cond. Используя cond, можно уместить определение hyper ос по­
мощью cond.
в две строки:
; СостояниеМира Число Число Строка -> СостояниеМира
; перемещает автомобиль в координату x события,
; если это событие "button-down"
(define (hyper x-position-of-car x-mouse y-mouse me)
(cond
[(string=? "button-down" me) x-mouse]
[else x-position-of-car]))

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

120

Глава 3
(main 1)

в области взаимодействий DrRa­cket и попробуйте переместить автомобиль через гиперпространство.
Вы можете спросить, почему нам так легко удалось изменить программу. Тому есть две причины. Во-первых, эта книга и описываемое
в ней программное обеспечение строго разделяют данные, за которыми следит программа, – модель – и изображение, которое она показывает, – представление (вид). В частности, функции, обрабатывающие
события, никак не затрагивают отображение состояния. Чтобы изменить способ отображения состояния, нам достаточно будет сосредоточиться на функции, указанной в предложении to-draw. Во-вторых,
рецепты проектирования программ и функций правильно организуют программы. Если что-то изменится в постановке задачи, то повторное выполнение пунктов рецепта проектирования естественным
образом укажет, где необходимо внести изменения в первоначальное
решение. Это может показаться очевидным для простых задач, с которыми мы сейчас имеем дело, но такой подход критически важен
для задач, с которыми программисты сталкиваются в реальном мире.

3.7. Миры виртуальных питомцев
Упражнения в этом разделе вводят два первых элемента игры с виртуальным питомцем. В самом начале мы имеем изображение кошки,
которая прогуливается по холсту. В процессе прогулки кошка устает
и показывает свою усталость. Как и с любым домашним питомцем,
вы можете попробовать погладить кошку, чтобы немного поднять ей
настроение, или даже покормить, что поднимет ей настроение еще
больше.
Итак, начнем с изображения нашей любимой кошки:

(define cat1

)

Скопируйте изображение кошки и вставьте его в DrRa­cket, затем
дайте изображению имя с по­мощью define, как показано выше.
Упражнение 45. Спроектируйте мировую программу «виртуальная кошка», которая непрерывно перемещает кошку слева направо.
Назовем ее cat-prog и предположим, что она принимает аргумент
с начальной позицией кошки. Кроме того, заставьте кошку перемещаться на три пикселя за каждый такт часов. Всякий раз, когда кошка
будет исчезать за правым краем сцены, она должна появляться из-за
левого края. Для этого вам может пригодиться функция modulo. 

Как проектировать программы

Упражнение 46. Улучшите анимацию кошки, используя немного
другое изображение:

(define cat2

)

Измените функцию отображения из упражнения 45 так, чтобы она
использовала первое или второе изображение кошки, в зависимости от четности или нечетности координаты x. Прочитайте описание
функции odd? в HelpDesk и используйте выражение cond для выбора
изображения кошки. 
Упражнение 47. Спроектируйте мировую программу, которая
поддерживает и отображает «шкалу счастья». Назовем это gauge-prog
и предположим, что она принимает аргумент с максимальным уровнем счастья. В первый момент программа должна показать на шкале
указанный максимальный уровень счастья. С каждым тактом часов
этот уровень должен уменьшаться на -0.1, но никогда не опускаться
ниже 0. Каждый раз, когда нажимается клавиша со стрелкой вниз, уровень счастья должен увеличиваться на 1/5; а каждый раз, когда нажимается стрелка вверх, уровень счастья должен увеличиваться на 1/3.
Для отображения уровня счастья используйте сцену со сплошным
красным прямоугольником в черной рамке. Если уровень счастья равен 0, прямоугольник должен исчезнуть; если уровень счастья максимальный, прямоугольник должен пересекать всю сцену.
Примечание. Когда вы получите достаточный объем знаний, мы
покажем, как совместить программу gauge-prog с решением упражнения 45. После этого вы сможете помочь кошке стать более счастливой.
Если погладить кошку, ее настроение немного улучшится. Если покормить ее, ее настроение улучшится намного больше.
Итак, теперь у вас наверняка проснулось желание больше узнать
о разработке мировых программ, чем рассказали первые три главы. 

121

4. И
 нтервалы, перечисления
и детализация
На данный момент вы знаете четыре варианта представления информации в виде данных: числа, строки, изображения и логические значения. Для многих задач этого достаточно, но есть много других задач,
для которых этих четырех типов данных в BSL (или других языках программирования) недостаточно. Настоящим проектировщикам нужны
дополнительные способы представления информации в виде данных.
Хорошие программисты должны уметь проектировать программы
с ограничениями на эти встроенные типы данных. Один из примеров подобных ограничений – перечислить несколько элементов некоторого типа и сказать, что только эти элементы будут использоваться для решения поставленной задачи. Перечисление элементов
возможно, только когда их количество ограничено. Чтобы приспособить под свои нужды типы с «бесконечным» коПод словом «бесконечное»
личеством элементов, мы вводим интервалы –
может просто подразумеваться «настолько большое наборы элементов, удовлетворяющих опредеколичество элементов, ленному свойству.
что прямое их перечисление
Определение перечислений и интервалов предне представляется
полагает различение элементов разных типов.
возможным».
Для их различения в коде требуются условные
функции, то есть функции, выбирающие разные способы вычисления
результата в зависимости от значения некоторого аргумента. Как писать такие функции, мы рассказывали в разделе «Множество способов
вычисления» в прологе и в разделе 1.6. Однако ни там, ни там не использовались рецепты проектирования. В обоих разделах мы просто представили некоторые конструкции на нашем любимом языке программирования (BSL) и показали несколько примеров их использования.
В этой главе мы обсудим общий подход к проектированию перечислений и интервалов – новых форм описания данных. Начнем
с того, что вновь рассмотрим выражение cond. Затем обсудим три
разных типа описаний данных: перечисления, интервалы и детализации. Перечисление указывает каждый отдельный элемент данных,
принадлежащий перечислению, а интервал определяет диапазон
данных. Последний тип описания, детализация, смешивает первые
два, указывая диапазоны в одном предложении своего определения
и конкретные элементы данных в другом. В заключение главы мы
представим общую стратегию проектирования для таких ситуаций.

4.1. Программирование с условиями
Давайте вспомним краткое введение в условные выражения в прологе. Поскольку cond – самая сложная форма выражения в этой книге,
внимательно рассмотрим его общую форму определения:

Интервалы, перечисления и детализация

123

(cond
[ВыражениеУсловия1 ВыражениеРезультата1]
[ВыражениеУсловия2 ВыражениеРезультата2]
...
[ВыражениеУсловияN ВыражениеРезультатаN])

Квадратные скобки отделяют друг
от друга разные ветви условного
выражения. Вместо [...] можно
использовать (...).

Условное выражение начинается с (cond и заканчивается закрывающей круглой скобкой ). Вслед за ключевым словом программист
записывает столько ветвей cond, сколько необходимо; каждая ветвь
состоит из двух выражений, заключенных в квадратные скобки: [ и ].
Ветви условного выражения cond также часто называют условиями
или предложениями cond.
Вот пример функции, в которой используется условное выражение:
(define (next traffic-light-state)
(cond
[(string=? "red" traffic-light-state) "green"]
[(string=? "green" traffic-light-state) "yellow"]
[(string=? "yellow" traffic-light-state) "red"]))

Подобно математическому примеру в прологе, этот пример иллюст­
рирует удобство использования условных выражений cond. Во многих задачах функция должна различать несколько разных ситуаций.
В выражении cond можно определить по одной ветви для каждой ситуации и тем самым напомнить читателю кода, что в постановке задачи говорится о разных условиях.
Замечание с прагматической точки зрения: сравните выражение cond с выражениями if из раздела 1.6. Последние отличают одну
конкретную ситуацию от всех остальных и хуже подходят для задач
с несколькими ситуациями; их лучше всего использовать, когда мы
хотим сказать: «или так, или иначе». Поэтому мы всегда используем
cond, когда хотим напомнить читателю нашего кода, что из определений данных вытекает несколько разных ситуаций. В других фрагментах кода мы используем любую наиболее удобную конструкцию.
В ситуациях со сложными условиями в выражении cond иногда бывает желательно иметь возможность сказать: «во всех остальных случаях». В таких случаях выражения cond разрешают использовать ключевое слово else в самой последней строке в определении cond:
(cond
[ВыражениеУсловия1 ВыражениеРезультата1]
[ВыражениеУсловия2 ВыражениеРезультата2]
...
[else ВыражениеРезультатаПоУмолчанию])

Если по ошибке ключевое слово else окажется в любой другой строке в определении выражения cond, то DrRa­cket сообщит об ошибке:
> (cond
[(> x 0) 10]
[else 20]
[(< x 10) 30])

124

Глава 4
cond:found an else clause that isn't the last clause in its
cond expression

(cond: встречено условие else, находящееся не в последней строке
в выражении cond).
То есть BSL отвергает грамматически неправильные фразы, потому
что нет смысла выяснять, что такие фразы могли бы означать.
Представьте проект функции в игровой программе, которая вычисляет некоторую награду в конце игры. Вот ее заголовок:
; ПоложительноеЧисло -- это Число, которое больше или равно 0.
; ПоложительноеЧисло -> Строка
; вычисляет награду по заданному числу очков s

А вот два варианта ее реализации для сравнения:
(define (reward s)
(cond
[( Светофор
; моделирует работу светофора,
; автоматически переключающегося с течением времени
(define (traffic-light-simulation initial-state)
(big-bang initial-state
[to-draw tl-render]
[on-tick tl-next 1]))

Аргументом функции является начальное состояние мира для
выражения big-bang, которое DrRa­cket преобразует в изображение
с по­мощью tl-render. События хода системных часов обрабатываются функцией tl-next. Также обратите внимание, что здесь явно
настраивается продолжительность одного такта часов равной одной
секунде.
Завершите проектирование tl-render и tl-next и скопируйте их
в область определений DrRa­cket. Вот несколько тестовых примеров
для tl-render:
(check-expect (tl-render "red")
(check-expect (tl-render "yellow")

)
)

Ваша функция tl-render может использовать эти изображения непосредственно. Но если вы решите конструировать изображения
с по­мощью функций из библиотеки 2htdp/image, то спроектируйте
вспомогательную функцию для создания изображения одноцветной
лампочки. Затем используйте функцию place-image для размещения
лампочек в сцене. 
Упражнение 60. Как вариант в представлении данных для программы, моделирующей работу светофора, можно использовать числа вместо строк:
;
;
;
;

Светофор -- это одно из трех значений:
-- 0 интерпретация: красный сигнал
-- 1 интерпретация: зеленый сигнал
-- 2 интерпретация: желтый сигнал

Это представление значительно упрощает определение tl-next:
; Ч-Светофор -> Ч-Светофор
; возвращает следующее состояние, опираясь на текущее состояние cs
(define (tl-next-numeric cs) (modulo (+ cs 1) 3))

Сформулируйте тесты для tl-next-numeric.
Какая функция точнее передает свое предназначение, tl-next или
tl-next-numeric? Объясните почему. 

149

Интервалы, перечисления и детализация

Упражнение 61. Как отмечалось в разделе 3.4, программы должны определять константы и использовать имена
констант вместо фактических значений. Определение данных, описывающее состояния светофора, тоже должно использовать константы:

Эту форму определения
данных использовал бы
опытный проектировщик.

(define RED 0)
(define GREEN 1)
(define YELLOW 2)
;
;
;
;

С-Светофор -- это одно из трех значений:
-- RED
-- GREEN
-- YELLOW

При выборе удачных имен определение данных не требует описания правил их интерпретации.
Листинг 18. Символический светофор
; С-Светофор -> С-Светофор
; выдает следующее состояние, опираясь на текущее состояние cs
(check-expect (tl-next-... RED) YELLOW)
(check-expect (tl-next-... YELLOW) GREEN)
(define (tl-next-numeric cs)
(modulo (+ cs 1) 3))
(define (tl-next-symbolic cs)
(cond
[(equal? cs RED) GREEN]
[(equal? cs GREEN) YELLOW]
[(equal? cs YELLOW) RED]))

В листинге 18 показаны две функции, изменяющие состояние
светофора. Какая из них спроектирована правильно, в соответствии
с рецептом проектирования? Какая из них продолжит работать после
изменения констант, как показано ниже:
(define RED "red")
(define GREEN "green")
(define YELLOW "yellow")

Это поможет вам ответить на вопросы?
ПРИМЕЧАНИЕ. Функция equal?, использованная в листинге 18,
сравнивает два произвольных значения, независимо от их типов. Равенство – сложная тема в мире программирования. КОНЕЦ 
Вот еще одна задача с конечным числом состояний, добавляющая
несколько дополнительных сложностей:
Постановка задачи. Спроектируйте мировую программу, которая моделирует работу двери с автоматическим доводчиком.
Если такая дверь заперта, ее можно открыть ключом. Незапертая дверь автоматически закрывается, но ее можно толкнуть

150

Глава 4

и распахнуть. Как только человек пройдет через дверь и отпус­
тит ее, она автоматически закроется, и после этого ее можно
снова запереть.
Чтобы выделить основные элементы, нарисуем диаграмму переходов (см. слева на рис. 6). Подобно светофору, дверь имеет три состояния: заперта, закрыта и открыта. При отпирании и запирании дверь
переходит из состояния «заперта» в состояние «закрыта» и наоборот.
Чтобы открыть незапертую дверь, ее нужно толкнуть. Оставшийся
переход отличается от других, потому что не требует никаких действий со стороны кого-либо или чего-либо. Дверь сама закрывается
со временем. Соответствующая стрелка перехода подписана словом
«время», чтобы подчеркнуть это.

заперта
отпирание запирание
закрыта
толчок

*время*

открыта

"заперта"
"u"

"l"

"закрыта"
""

*такт часов*

"открыта"

Рис. 6. Диаграмма переходов для автоматически закрывающейся двери

Следуя рецепту, начнем с перевода трех возможных состояний
в данные:
(define LOCKED "locked")
(define CLOSED "closed")
(define OPEN "open")
;
;
;
;

СостояниеДвери -- это одно из значений:
-- LOCKED (заперта)
-- CLOSED (закрыта)
-- OPEN (открыта)

Вспомним также урок из упражнения 61, а именно что лучше определить символические константы и формулировать определения
данных в терминах этих констант.
Следующий шаг в рецепте проектирования мировой программы
требует преобразовать действия в предметной области – стрелки
на диаграмме слева – во взаимодействия с компьютером, поддержку которых можно получить с по­мощью библиотеки 2htdp/universe.

Интервалы, перечисления и детализация

Наша диаграмма состояний и переходов двери, в частности стрелка
от состояния «открыта» к состоянию «закрыта», предполагает использование системных часов. Для реализации взаимодействий, обозначенных другими стрелками, можно использовать нажатия клавиш
или щелчки мышью. Давайте используем нажатия клавиш: «u» – для
отпирания двери, «l» – для запирания и клавишу пробела – для имитации толчка, открывающего дверь. На диаграмме справа на рис. 6
эти варианты представлены графически; она транслирует диаграмму
конечного автомата из мира информации в мир данных.
Решив использовать время и нажатия клавиш для обозначения
действий, мы должны спроектировать функции, отображающие текущее состояние мира, представленное как СостояниеДвери, и преобразующие его в следующее состояние мира. Разумеется, их следует
внести в список желаний:
zz door-closer, закрывает открытую дверь по истечении одного

такта часов;
zz door-action, выполняет действие в зависимости от нажатой клавиши;
zz door-render, преобразует текущее состояние в изображение.
Стоп! Сформулируйте соответствующие сигнатуры.
Начнем с door-closer. Поскольку door-closer играет роль обработчика событий часов, его сигнатура определяется нашим выбором представления СостояниеДвери в качестве коллекции набора состояний
мира:
; СостояниеДвери -> СостояниеДвери
; закрывает открытую дверь по истечении одного такта часов
(define (door-closer state-of-door) state-of-door)

Придумывать примеры не составляет никакого труда, когда мир
может находиться только в одном из трех состояний. Здесь, как и в некоторых математических примерах выше, для выражения основной
идеи мы использовали таблицу:
исходное состояние
LOCKED
CLOSED
OPEN

желаемое состояние
LOCKED
CLOSED
CLOSED

Стоп! Выразите эти примеры в виде тестов на языке BSL.
Чтобы получить макет, нам понадобится условное выражение
с тремя условиями:
(define (door-closer state-of-door)
(cond
[(string=? LOCKED state-of-door) ...]
[(string=? CLOSED state-of-door) ...]
[(string=? OPEN state-of-door) ...]))

151

152

Глава 4

а превратить этот макет в определение функции нам помогут примеры:
(define (door-closer state-of-door)
(cond
[(string=? LOCKED state-of-door) LOCKED]
[(string=? CLOSED state-of-door) CLOSED]
[(string=? OPEN state-of-door) CLOSED]))

Не забудьте запустить свои тесты.
Вторая функция, door-action, обрабатывает остальные три стрелки
на диаграмме. Функции, обслуживающие события клавиатуры, используют два элемента информации: состояние мира и описание события. То есть сигнатура этой функции выглядит так:
; СостояниеДвери СобытиеКлавиатуры -> СостояниеДвери
; выполняет определенное действие
; в зависимости от события клавиатуры k и состояния s
(define (door-action s k)
s)

И снова представим примеры в табличной форме:
исходное состояние
LOCKED
CLOSED
CLOSED
OPEN

событие клавиатуры
"u"
"l"
""


желаемое состояние
CLOSED
LOCKED
OPEN
OPEN

Примеры объединяют информацию из нашего рисунка с выбранными нами соответствиями между действиями и событиями клавиатуры. В отличие от таблицы с примерами для светофора, эта таблица
неполная. Подумайте, какие еще примеры можно было бы привести,
а затем попробуйте понять, почему нашей таблицы достаточно.
Опираясь на эти примеры, легко сформулировать законченную
реа­лизацию:
(check-expect
(check-expect
(check-expect
(check-expect
(check-expect

(door-action
(door-action
(door-action
(door-action
(door-action

(define (door-action
(cond
[(and (string=?
CLOSED]
[(and (string=?
LOCKED]
[(and (string=?
OPEN]
[else s]))

LOCKED "u") CLOSED)
CLOSED "l") LOCKED)
CLOSED " ") OPEN)
OPEN "a") OPEN)
CLOSED "a") CLOSED)

s k)
LOCKED s) (string=? "u" k))
CLOSED s) (string=? "l" k))
CLOSED s) (string=? " " k))

Интервалы, перечисления и детализация

Обратите внимание на использование оператора and для объединения двух условий: одно условие проверяет текущее состояние двери,
а другое – событие клавиатуры.
Наконец, нам нужно отобразить состояние мира в виде сцены:
; СостояниеДвери -> Изображение
; преобразует состояние s в изображение с текстом
(check-expect (door-render CLOSED)
(text CLOSED 40 "red"))
(define (door-render s)
(text s 40 "red"))

Эта упрощенная функция создает изображение с крупным текстом.
А вот так программа использует все эти функции:
; СостояниеДвери -> СостояниеДвери
; имитирует дверь с автоматическим доводчиком
(define (door-simulation initial-state)
(big-bang initial-state
[on-tick door-closer]
[on-key door-action]
[to-draw door-render]))

Теперь пришло время собрать все вместе и запустить в DrRa­cket,
чтобы увидеть, правильно ли работает наша программа.
Упражнение 62. Во время работы программы состояние «открыта» практически не видно. Измените программу так, чтобы продолжительность такта часов составляла три секунды. Запустите программу и понаблюдайте за ней. 

153

5. Добавляем структуру
Предположим, что мы решили спроектировать мировую программу,
имитирующую мяч, прыгающий вверх и вниз между полом и потолком воображаемой идеальной комнаты. Предположим, что мяч перемещается с постоянной скоростью – два пикселя за один такт часов. Если следовать рецепту проектирования, то
Математикам первая наша цель – определить представление
известны приемы
данных, меняющихся с течением времени. В дан«объединения» двух
чисел в одно, из которо- ном случае со временем меняются местоположего потом можно обрат- ние и направление движения мяча, но это два знано получить исходные чения, а big-bang может конт­ролировать только
числа. Программисты
одно значение. Возникает вопрос: как в один блок
считают такие уловки
вредными, потому что данных уместить два изменяющихся блока инони скрывают истинные формации?
цели программы.
Вот еще один сценарий, где возникает тот же
вопрос. Мобильный телефон – это несколько миллионов строк кода, заключенных в пластик. Кроме всего прочего, он
управляет вашими контактами. У каждого контакта есть имя, номер
телефона, адрес электронной почты и, возможно, другая информация. Когда приходится работать со множеством контактов, каждый
отдельный контакт лучше всего представить в виде единого экземпляра данных; в противном случае составные части могут случайно
перепутаться.
Для решения подобных проблем каждый язык программирования
предоставляет некоторый механизм объединения нескольких значений в единый составной фрагмент данных и извлечения составляющих значений, когда это необходимо. Эта глава знакомит с подобным
механизмом, имеющимся в языке BSL, так называемой поддержкой
определения структурных типов, и рассказывает, как проектировать
программы, работающие с составными данными.

5.1. От позиций к структурам posn
Местоположение на холсте однозначно определяется двумя элементами данных: расстоянием от левого края и расстоянием от верхнего
края. Первое называется координатой x, а второе – координатой y.
Среда программирования DrRa­cket, которая, по сути, является программой на BSL, представляет такие позиции в виде структуры posn.
Структура posn объединяет два числа в одно значение. Создать экземпляр структуры posn можно с по­мощью операции make-posn, которая
принимает два числа и создает экземпляр posn. Например, вот выражение, которое создает экземпляр структуры posn, в котором координата x имеет значение 3, а координата y – значение 4:
(make-posn 3 4)

155

Добавляем структуру

Структура posn – это такой же тип данных, как число, логическое
значение или строка. В частности, элементарные операции и функции могут принимать и создавать структуры. При создании экземп­
ляра структуры posn программа может дать ему имя:
(define one-posn (make-posn 8 6))

Стоп! Опишите one-posn в терминах координат.
Прежде чем продолжить, познакомимся с правилами вычислений
с использованием структур posn. Это поможет нам создавать функции,
обрабатывающие структуры posn, и понимать, что они вычисляют.

5.2. Вычисления со структурами posn
Особенности функций и законы, связанные с ними, хорошо знакомы из
начальной алгебры, но структуры posn – это, кажется, что-то новенькое.
С другой стороны, экземпляры posn должны выглядеть как декартовы
точки на плоскости, с которыми вы, возможно, сталкивались раньше.
Выбор отдельных координат декартовой точки – тоже зна- Спасибо Нилу Торонто
комый процесс. Например, если учитель скажет вам: «Взгля- (Neil Toronto)
ните на график на рис. 7 и скажите, что такое px и py», – то, за библиотеку plot.
скорее всего, вы ответите: «31 и 26», – потому что знаете, что
должны прочитать значения в точках пересечения с осями координат
вертикальной и горизонтальной линий, исходящих из p.
50

Ось y

40
30

p

20
10
0

0

10

20

30
Ось х

40

50

Рис. 7. Декартова точка

Вот как эту идею выразить на BSL. Предположим, вы ввели код
(define p (make-posn 31 26))

в область определений и щелкнули на кнопке RUN (Выполнить), а затем произвели следующие взаимодействия:
> (posn-x p)
31
> (posn-y p)
26

156

Глава 5

Определение p напоминает обозначение точки на декартовой плос­
кости, а ссылки posn-x и posn-y подобны индексам при p: px и py.
С вычислительной точки зрения для структур posn имеют силу два
тождества:
(posn-x (make-posn x0 y0)) == x0
(posn-y (make-posn x0 y0)) == y0

DrRa­cket использует эти тождества во время вычислений. Вот пример вычислений с использованием структур posn:
(posn-x p)
== ; DrRa­cket замещает p выражением (make-posn 31 26)
(posn-x (make-posn 31 26))
== ; DrRa­cket использует тождество для posn-x
31

Стоп! Подтвердите верность второго взаимодействия, выполнив
вычисления вручную. Также используйте движок пошаговых вычислений в DrRa­cket для перепроверки.

5.3. Программирование с posn
Теперь рассмотрим порядок проектирования функции, которая вычисляет расстояние от некоторой точки на холсте до начала координат:

Как показывает этот рисунок, под «расстоянием» в данном случае
подразумевается длина прямого отрезка, соединяющего точку с верхним левым углом холста.
Вот описание назначения и заголовок:
; вычисляет расстояние от точки ap до начала координат
(define (distance-to-0 ap)
0)

Главная особенность функции distance-to-0 состоит в том, что она
принимает одно значение, структуру posn, и возвращает одно значение – расстояние от заданной точки до начала координат.
Чтобы составить примеры, нужно знать, как вычислить это расстоя­
ние. Для точек со значением 0 в одной из координат результатом будет другая координата:

Добавляем структуру
(check-expect (distance-to-0 (make-posn 0 5)) 5)
(check-expect (distance-to-0 (make-posn 7 0)) 7)

Для всех других случаев можно попробовать вывести формулу самостоятельно или вспомнить школьный курс геометрии. Эта формула относится к предметной области, которую вы можете хорошо
знать, но если это не так, то мы подскажем вам эту формулу; в конце
концов, эта предметная область не является информатикой. Итак, вот
как выглядит формула вычисления расстояния (x, y):

Используя эту формулу, легко можно составить еще несколько примеров применения функции:
(check-expect (distance-to-0 (make-posn 3 4)) 5)
(check-expect (distance-to-0 (make-posn 8 6)) 10)
(check-expect (distance-to-0 (make-posn 5 12)) 13)

Мы специально подобрали такие примеры, чтобы вам было проще
проверить их результаты. Но подобная простота характерна не для
всех структур posn.
Стоп! Подставьте координаты x и y из примеров в формулу. Проверьте вручную верность ожидаемых результатов во всех пяти примерах.
Теперь можно перейти к определению функции. Как показывают
примеры, при проектировании distance-to-0 не требуется различать
разные ситуации; можно просто вычислить расстояние от точки с координатами x и y внутри данной структуры posn. Но функция должна
каким-то образом получить эти координаты из структуры posn. Для
этого можно использовать элементарные функции posn-x и posn-y.
В частности, функция distance-to-0 должна вычислить выражения
(posn-x ap) и (posn-y ap), потому что ap – это имя заданной структуры
posn:
(define (distance-to-0 ap)
(... (posn-x ap) ...
... (posn-y ap) ...))

Имея макет и примеры, определить реализацию функции не составит большого труда:
(define (distance-to-0 ap)
(sqrt
(+ (sqr (posn-x ap))
(sqr (posn-y ap)))))

Функция вычисляет квадраты значений выражений (posn-x ap)
и (posn-y ap), представляющих координаты x и y, находит их сумму
и извлекает из суммы квадратный корень. В DrRa­cket можно быстро
убедиться, что наша новая функция возвращает правильные результаты для всех наших примеров.

157

158

Глава 5

Упражнение 63. Вычислите вручную следующие выражения:
zz (distance-to-0 (make-posn 3 4));
zz (distance-to-0 (make-posn 6 (* 2 4)));
zz (+ (distance-to-0 (make-posn 12 5)) 10).

Запишите все шаги, предположив, что вычисление sqr – это один
шаг. Сравните записанные шаги с результатами вычислений с по­
мощью движка пошаговых вычислений в DrRa­cket. 
Упражнение 64. Манхэттенским расстоянием от точки до начала
координат называют длину пути, проложенного по прямоугольной
сетке улиц в Манхэттене. Вот пара примеров:

Слева показан «прямой» способ прокладки пути, когда сначала
путь прокладывается максимально влево, а затем вверх, насколько необходимо. Для сравнения справа показан способ «случайного
блуж­дания», когда путь прокладывается на несколько кварталов влево, затем на несколько кварталов вверх и т. д., до достижения пункта
назначения, в данном случае начало координат.
Стоп! Имеет ли значение выбор того или иного способа прокладки
пути?
Спроектируйте функцию manhattan-distance, которая измеряет манхэттенское расстояние от данной точки до начала координат. 

5.4. Определение структурных типов
В отличие от чисел или логических значений, структуры, такие как
posn, обычно не являются частью языка программирования. Язык
предоставляет только механизм определения структур, а все остальное оставляется на усмотрение программиста. То же верно и для BSL.
Определение структурного типа – это еще одна форма определений, помимо определений констант и функций. Вот как создатель
DrRa­cket определил структуру posn на BSL:
Использование квадратных скобок
в определении структуры не является
обязательным требованием, и здесь мы
использовали их исключительно для удобства,
чтобы отделить имена полей. Квадратные
скобки можно заменить круглыми скобками.

(define-struct posn [x y])

В общем виде определение структуры выглядит так:
(define-struct ИмяСтруктуры [Имя поля ...])

Добавляем структуру

Ключевое слово define-struct сообщает, что определяется новый тип
структуры. За ним следует имя структуры. Третья часть в определении структуры – это последовательность имен полей, заключенная
в скобки.
Определение структуры фактически определяет функции. Но, в отличие от обычного определения функции, определение структуры
определяет сразу несколько функций. В частности, определяются
три вида функций:
zz один конструктор – функция, которая создает экземпляры

структуры. Конструктор принимает столько значений, сколько
полей имеется в структуре; как уже упоминалось, структурой
называют экземпляр структуры. Фраза тип структуры – это
общее название набора всех возможных экземпляров;
zz по одному селектору для каждого поля, извлекающему значение поля из экземпляра структуры;
zz один предикат структуры, который, как и обычные предикаты,
отличает экземпляры от всех других типов значений.
Программа может использовать их, как если бы они были обычными функциями.
Любопытно отметить, что определение типа структуры автоматически создает имена для множества новых операций. Так конструктор получает имя структуры с префиксом «make-», а селекторы – имя
структуры с окончаниями, соответствующими именам полей. Наконец, предикат – это просто имя структуры со знаком вопроса «?», который произносится как «да?» при чтении вслух.
Это соглашение об именах выглядит сложным и, возможно, даже
вносит некоторую путаницу. Но, немного попрактиковавшись, вы
быстро освоите его. Оно также объясняет функции, поддерживаемые
структурами posn: make-posn – конструктор, posn-x и posn-y – селекторы.
Мы пока не сталкивались с posn?, тем не менее теперь мы знаем, что
эта функция существует; назначение предикатов мы подробно рассмотрим в следующей главе.
Упражнение 65. Взгляните на следующие определения типов
структур:
zz (define-struct movie [title producer year]);
zz (define-struct person [name hair eyes phone]);
zz (define-struct pet [name number]);
zz (define-struct CD [artist title price]);
zz (define-struct sweater [material size producer]).

Выпишите имена функций (конструкторов, селекторов и предикатов), которые создаются одновременно с ними. 
Но давайте оставим структуры posn и посмотрим на определение
структуры, которое мы могли бы использовать для отслеживания
контактов, например, в вашем сотовом телефоне:

159

160

Глава 5
(define-struct entry [name phone email])

Вот имена функций, которые создаются этим определением:
zz make-entry, принимает три значения и создает экземпляр кон-

такта entry;
zz entry-name, entry-phone и entry-email, принимают один экземпляр
entry и возвращают значение соответствующего поля;
zz entry?, предикат.
Поскольку каждый экземпляр entry объединяет три значения, выражение
(make-entry "Al Abe" "666-7771" "lee@x.me")

создаст экземпляр структуры entry со строкой "Al Abe" в поле name,
строкой "666-7771" в поле phone и строкой "lee@x.me" в поле email.
Упражнение 66. Вернитесь к определениям структур в упражнении 65 и попробуйте предположить, какие значения могли бы храниться в их полях. Затем создайте хотя бы по одному экземпляру для
каждого определения структуры. 
Каждое определение структуры создает новый тип структур, отличный от всех остальных. Эта выразительность необходима программистам, чтобы передать назначение структуры с по­мощью
ее имени. Каждый раз, когда структура создается, проверяется или
из нее извлекаются поля, текст программы явно будет напоминать
читателю об этом назначении. Если бы программисты не думали
о том, что кто-то будет читать их код в будущем, то они могли бы
использовать одно определение структуры для структур с одним полем, другое для структур с двумя полями, третье для структур с тремя и т. д.
А теперь давайте решим задачу программирования.
Задача. Определите тип структуры для программы, имитирующей «прыгающий мяч», которая упоминалась в самом начале
этой главы. Местоположение мяча – это одно число, определяю­
щее расстояние в пикселях от верхнего края холста. Абсолютная скорость – это количество пикселей, на которое мяч перемещается за такт часов. Фактическая скорость – это абсолютная
скорость плюс направление движения.
Поскольку мяч перемещается только по вертикали, для представления фактической скорости достаточно обычного числа:
zz положительное число означает, что мяч движется вниз;
zz отрицательное число означает, что мяч движется вверх.

На основе этого знания предметной области можно сформулировать следующее определение типа структуры:
(define-struct ball [location velocity])

Добавляем структуру

161

Оба поля будут содержать числа, поэтому (make-ball 10 -3) – хороший пример данных. Согласно этому примеру, мяч находится на расстоянии 10 пикселей от верхнего края холста и перемещается вверх со
скоростью 3 пикселя за такт часов.
Обратите внимание, что структура ball просто объединяет два
числа, подобно структуре posn. Встретив в программе выражение
(ball-velocity a-ball), вы сразу поймете, что эта программа имеет
дело с представлением мяча и его скоростью. Напротив, если бы программа использовала структуры posn, то выражение (posn-y a-ball)
могло бы ввести читателя кода в заблуждение: он мог бы подумать,
что выражение относится к координате y.
Упражнение 67. Вот еще один способ представления прыгающего
мяча:
(define SPEED 3)
(define-struct balld [location direction])
(make-balld 10 "up")

Попробуйте интерпретировать этот фрагмент кода и создайте другие экземпляры balld. 
Поскольку структуры являются значениями, так же как числа, логические значения или строки, то логично предположить, что экземпляр одной структуры может находиться внутри экземпляра другой
структуры. Возьмем для примера игровые объекты. В отличие от прыгающих мячей, такие объекты не всегда движутся исключительно по
вертикали, они могут двигаться по диагонали и вообще как Согласно законам
угодно. Для описания местоположения и скорости мяча, физики, чтобы
движущегося по двумерному мировому холсту, требуются определить следующее
местоположение
два числа: по одному для каждого направления. Местопо- объекта, нужно
ложение такого объекта определяется двумя числами – ко- прибавить скорость
ординатами x и y. Скорость описывает характер изменения к его местоположению.
местоположения по горизонтали и вертикали. Иначе гово- Разработчики должны
узнать, к кому обраря, чтобы узнать, где будет находиться объект в следующий щаться с вопросами,
момент, необходимо эти «числа, описывающие характер касающимися предметной области.
изменений» прибавить к соответствующим координатам.
Как мы уже знаем, местоположение можно представить
с по­мощью структуры posn. Для представления скоростей определим
тип структуры vel:
(define-struct vel [deltax deltay])

В ней имеется два поля: deltax и deltay. Слово «delta» обычно используется для обозначения приращений, когда речь идет о моделировании физических действий, а окончания x и y указывают, какая ось
координат подразумевается.
Теперь объединим структуры posn и vel в структуре ball, представляющей мяч, движущийся по прямым линиям, но не обязательно по
вертикали или горизонтали:

162

Глава 5
(define ball1
(make-ball (make-posn 30 40) (make-vel -10 5)))

Экземпляр этой структуры можно представить как мяч, находящийся в 30 пикселях от левого и в 40 пикселях от верхнего края холста
и перемещающийся за каждый такт часов на 10 пикселей влево (потому что вычитание 10 из координаты x приблизит его к левоАльтернативное му краю) и на 5 вниз (потому что прибавление положительрешение – использовать
ного числа к координате y увеличивает расстояние от верхкомплексные числа. Если
него
края).
вы знакомы с ними, то
Упражнение 68. Вместо использования вложенных
могли бы использовать
их для представления структур для представления мяча можно определить струкместоположения и скотуру с четырьмя полями:
рости. Например, в BSL
комплексное число 4-3i
можно использовать для
обозначения местоположения или скорости
(4, –3).

(define-struct ballf [x y deltax deltay])

На языке программистов такое представление называется плоским представлением. Создайте экземпляр ballf, интерпретируемый так же, как экземпляр ball1. 
Рассмотрим еще один пример вложенных структур: списков контактов. Многие сотовые телефоны поддерживают списки контактов,
которые позволяют указывать несколько телефонных номеров для
каждого имени: домашний, рабочий и сотовый. Для телефонных номеров можно также указать код города и местный номер. Поскольку
эта информация имеет вид вложений, лучше определить вложенное
представление данных:
(define-struct centry [name home office cell])
(define-struct phone [area number])
(make-centry "Shriram Fisler"
(make-phone 207 "363-2421")
(make-phone 101 "776-1099")
(make-phone 208 "112-9981"))

Идея состоит в том, что запись в списке контактов имеет четыре
поля: имя и три телефонных номера. Последние представлены экземплярами phone, которые отделяют код города и местный номер телефона.
В окружающей нас действительности вложенная информация присутствует повсюду, и лучший способ представить такую ​​информацию
в виде данных – отразить вложенность с по­мощью вложенных экземпляров структур. Это упрощает интерпретацию данных в прикладной области программы, а также переход от примеров информации
к данным. Конечно, на самом деле задача определения данных состоит в том, чтобы указать, как преобразовывать информацию в данные
и обратно. Однако, прежде чем перейти к приемам определения данных с использованием структур, мы систематически исследуем вычисления со структурами и поразмышляем о них.

163

Добавляем структуру

5.5. Вычисления со структурами
Типы структур обобщают данные двумя способами. Во-пер- Большинство языков
вых, структура может иметь произвольное количество по- программирования
лей: ноль, одно, два, три и т. д. Во-вторых, поля в структу- поддерживают также
структуры или похожие
рах именуются, а не нумеруются. Это упрощает чтение кода, на структуры типы,
потому что гораздо легче запомнить, что фамилия доступна в которых используются числовые имена
в поле с именем last-name, чем в поле с номером 7.
Аналогично вычисления с экземплярами структур обоб- полей.
щают манипуляции с данными. Чтобы оценить эту идею, рассмотрим
схематический способ представления экземпляров структуры в виде
ящиков с количеством отсеков, совпадающим с количеством полей.
Вот пример определения структуры:
(define pl (make-entry "Al Abe" "666-7771" "lee@x.me"))

и схема:
entry
name

phone

email

"Al Abe" "666-7771" "lee@x.me"
Надпись entry, напечатанная наклонным шрифтом, указывает, что
здесь изображен экземпляр данного типа структуры; каждая ячейка
тоже имеет свою подпись. Вот еще один пример:
(make-entry "Tara Harp" "666-7770" "th@smlu.edu")

Ему соответствует похожая схема, но с другим содержимым:
entry
name

phone

email

"Tara Harp" "666-7770" "th@smlu.edu"
Экземпляры вложенных структур тоже можно изобразить как наборы ячеек, вложенные в другие ячейки. Вот пример схемы для экземпляра структуры ball1, который был создан выше:
ball
location
x

posn
y

30 40

velocity
vel
deltax deltay

-10 +5

В этом случае внешняя ячейка содержит две вложенные ячейки, по
одной для каждого поля.
Упражнение 69. Нарисуйте схему представления для решения
упражнения 65. 

164

Глава 5

В контексте таких схем селектор можно сравнить с кнопкой, открывающей конкретный отсек в ящике определенного типа и тем самым
позволяющей владельцу извлечь содержимое. Следуя этой логике,
применение entry-name к pl даст строку:
> (entry-name pl)
"Al Abe"

Но попытка применить entry-name к экземпляру структуры posn закончится ошибкой:
> (entry-name (make-posn 42 5))
entry-name:expects an entry, given (posn 42 5)

(entry-name: ожидалась entry, а получена (posn 42 5)).
Если в отсеке находится другой ящик, может потребоваться использовать два селектора подряд, чтобы добраться до нужного числа:
> (ball-velocity ball1)
(make-vel -10 5)

Применение ball-velocity к ball1 вернет значение поля скорости –
экземпляр vel. Чтобы получить скорость по оси x, нужно применить
селектор к результату первого селектора:
> (vel-deltax (ball-velocity ball1))
-10

Внутреннее выражение извлекает скорость из ball1, а внешнее –
значение поля deltax, которое в данном случае равно -10.
Кроме того, взаимодействия показывают, что экземпляры структуры являются значениями. DrRa­cket выводит их точно так, как они
были введены:
> (make-vel -10 5)
(make-vel -10 5)
> (make-entry "Tara Harp" "666-7770" "th@smlu.edu")
(make-entry "Tara Harp" "666-7770" "th@smlu.edu")
> (make-centry
"Shriram Fisler"
(make-phone 207 "363-2421")
(make-phone 101 "776-1099")
(make-phone 208 "112-9981"))
(make-centry ...)

Стоп! Попробуйте выполнить последнее действие самостоятельно,
чтобы увидеть правильный результат.
Вообще говоря, определение структуры не только создает новые
функции и новые способы создания значений, но также добавляет
новые законы вычислений. Эти законы являются обобщением законов, с которыми мы познакомились в разделе 5.2, когда исследовали
структуру posn. Чтобы лучше понять их, рассмотрим пример.
Когда DrRa­cket встречает определение типа структуры с двумя полями:

Добавляем структуру
(define-struct ball [location velocity])

вводятся два закона, по одному для каждого селектора:
(ball-location (make-ball l0 v0)) == l0
(ball-velocity (make-ball l0 v0)) == v0

Для других структур вводятся аналогичные законы. Например, для
определения
(define-struct vel [deltax deltay])

DrRa­cket добавит следующие два закона в свою базу знаний:
(vel-deltax (make-vel dx0 dy0)) == dx0
(vel-deltay (make-vel dx0 dy0)) == dy0

Используя эти законы, можно объяснить результаты взаимодействий из примера выше:
(vel-deltax (ball-velocity ball1))
== ; DrRa­cket заменит структуру ball1 ее значением
(vel-deltax
(ball-velocity
(make-ball (make-posn 30 40) (make-vel -10 5))))
== ; DrRa­cket использует закон для ball-velocity
(vel-deltax (make-vel -10 5))
== ; DrRa­cket использует закон для vel-deltax
-10

Упражнение 70. Опишите законы, действующие для следующих
определений структур:
(define-struct centry [name home office cell])
(define-struct phone [area number])

Используйте движок пошаговых вычислений в DrRa­cket, чтобы
убедиться, что следующее выражение действительно возвращает 101:
(phone-area
(centry-office
(make-centry "Shriram Fisler"
(make-phone 207 "363-2421")
(make-phone 101 "776-1099")
(make-phone 208 "112-9981")))) 

В заключение, чтобы окончательно понять идею определения
структур, мы должны обсудить предикаты. Как уже упоминалось,
каждое определение структуры вводит один новый предикат. DrRa­
cket использует эти предикаты, чтобы определить, правильно ли
применяется селектор к указанному значению. Более подробно эта
идея объясняется в следующей главе, а здесь мы просто покажем, что
предикаты похожи на предикаты из «арифметики». Предикат number?
распознает числа, предикат string? – строки, предикаты posn? и entry?
распознают экземпляры структур posn и entry. Эту нашу догадку мож-

165

166

Глава 5

но подтвердить экспериментами в области взаимодействий. Предположим, что область определений содержит следующие определения:
(define ap (make-posn 7 0))
(define pl (make-entry "Al Abe" "666-7771" "lee@x.me"))

Если верна наша догадка, что предикат posn? отличает экземпляры
структуры posn от всех других значений, то можно предположить, что
он вернет #false для чисел и #true для ap:
> (posn?
#true
> (posn?
#false
> (posn?
#false
> (posn?
#true

ap)
42)
#true)
(make-posn 3 4))

Аналогично предикат entry? отличает экземпляры структуры entry
от любых других значений:
> (entry? pl)
#true
> (entry? 42)
#false
> (entry? #true)
#false

В общем случае предикат распознает именно те значения, которые
созданы с по­мощью конструктора с тем же именем. В интермеццо 1
подробно объясняется этот закон, а также представлены другие законы вычислений в BSL.
Упражнение 71. Добавьте следующий код в область определений
DrRa­cket:
; расстояния определяются в пикселях:
(define HEIGHT 200)
(define MIDDLE (quotient HEIGHT 2))
(define WIDTH 400)
(define CENTER (quotient WIDTH 2))
(define-struct game [left-player right-player ball])
(define game0
(make-game MIDDLE MIDDLE (make-posn CENTER CENTER)))

Щелкните на кнопке RUN (Выполнить) и вычислите следующие
выражения:
(game-ball game0)
(posn? (game-ball game0))
(game-left-player game0)

Объясните результаты шаг за шагом. Проверьте свои объяснения
с по­мощью движка пошаговых вычислений в DrRa­cket. 

Добавляем структуру

5.6. Программирование со структурами
Правильное программирование требует правильного определения
данных. С введением определений типов структур определение данных становится еще интереснее. Помните, что определение данных
обеспечивает способ представления информации в виде данных
и интерпретацию этих данных как информации. В случае со структурами это требует описания того, какие данные в каких полях хранятся. Для некоторых определений структур сформулировать такие
описания легко и просто:
(define-struct posn [x y])
; Posn -- это структура:
; (make-posn Число Число)
; интерпретация: точка, находящаяся на расстоянии
; x пикселей от левого и y пикселей от верхнего края холста

Нет смысла использовать другие типы данных для создания posn.
Точно так же все поля в entry – в структуре записей в списке контактов – явно должны быть строками, как описывалось в предыдущем
разделе:
(define-struct entry [name phone email])
; Entry -- это структура:
; (make-entry Строка Строка Строка)
; интерпретация: имя, номер телефона и адрес электронной почты

Встретив posn или entry, читатель легко сможет интерпретировать
экземпляры этих структур.
Сравните эту простоту с определением структуры ball, которое допускает, как минимум, две разные интерпретации:
(define-struct ball [location velocity])
; Ball-1d -- это структура:
; (make-ball Число Число)
; интерпретация 1: расстояние от верхнего края и скорость
; интерпретация 2: расстояние от левого края и скорость

Какую бы из них мы ни использовали в программе, мы должны
постоянно придерживаться ее. Однако, как показано в разделе 5.4,
структуры ball можно использовать совершенно иначе:
; Ball-2d -- это структура:
; (make-ball posn vel)
; интерпретация: 2-мерные местоположение и скорость
(define-struct vel [deltax deltay])
; Vel -- это структура:
; (make-vel Число Число)
; интерпретация: (make-vel dx dy) означает приращение
; на dx пикселей [за такт] координаты x и
; на dy пикселей [за такт] координаты y

167

168

Глава 5

Здесь мы назвали вторую коллекцию данных Ball-2d, в противовес
Ball-1d, чтобы описать представление данных для мяча, способного
перемещаться по прямым линиям по мировому холсту. Проще говоря, одну и ту же структуру можно использовать двумя разными способами. Конечно, в рамках одной программы лучше придерживаться
одного и только одного способа использования, иначе вы создадите
себе проблемы на ровном месте.
Кроме того, Ball-2d ссылается на другое определение данных,
а именно на определение Vel. Несмотря на то что все иные определения данных, использовавшиеся до сих пор, относились к встроенным
коллекциям данных (Число, Логическое значение, Строка), вполне
приемлемо (и это часто используется на практике), когда одно из наших определений данных ссылается на другое.
Упражнение 72. Сформулируйте определение данных для приведенного выше определения структуры phone, соответствующее приведенным примерам.
Затем сформулируйте определение данных для телефонных номеров, используя это определение структуры:
(define-struct phone# [area switch num])

Исторически сложилось так, что первые три цифры определяют
код города, следующие три – код телефонной станции в вашем райо­
не, а последние четыре цифры – телефонный номер в этом районе.
Опишите содержимое трех полей как можно точнее с использованием интервалов. 
На этом этапе вам, возможно, интересно понять, что на самом деле
означают определения данных. Этот вопрос и ответ на него – тема
следующего раздела. А пока мы продолжим обсуждать особенности
использования определений данных при проектировании программ.
Вот описание задачи для контекста.
Задача. Ваша команда разрабатывает интерактивную игровую
программу, которая перемещает красную точку по холсту размером 100×100 и позволяет игрокам использовать мышь для
перемещения точки в исходную позицию. Вот как далеко вы
продвинулись:
(define MTS (empty-scene 100 100))
(define DOT (circle 3 "solid" "red"))
; A Posn представляет состояние мира.
; Posn -> Posn
(define (main p0)
(big-bang p0
[on-tick x+]
[on-mouse reset-dot]
[to-draw scene+dot]))

Ваша задача – создать функцию scene+dot, которая добавляет
красную точку на пустой холст в указанной позиции.

Добавляем структуру

Описание задачи диктует следующую сигнатуру вашей функции:
; Posn -> Изображение
; добавляет красную точку в MTS в позицию p
(define (scene+dot p) MTS)

Добавить описание назначения несложно. Как упоминалось в разделе 3.1, такое заявление использует параметр функции для выражения результата функции.
Теперь сформулируем пару примеров и оформим их в виде тестов:
(check-expect (scene+dot (make-posn 10 20))
(place-image DOT 10 20 MTS))
(check-expect (scene+dot (make-posn 88 73))
(place-image DOT 88 73 MTS))

Так как функция принимает структуру posn, очевидно, что она
должна извлекать значения полей x и y:
(define (scene+dot p)
(... (posn-x p) ... (posn-y p) ...))

После добавления этих дополнительных элементов в макет тела
функции мы легко сможем дописать остальное определение. Используя place-image, данная функция помещает DOT в MTS в координаты, содержащиеся в p:
(define (scene+dot p)
(place-image DOT (posn-x p) (posn-y p) MTS))

Функции могут создавать и возвращать структуры. Вернемся к нашему примеру выше, потому что он как раз предусматривает такую
задачу.
Задача. Вашему коллеге предложено определить функцию x+,
которая принимает структуру posn, увеличивает координату x
на 3 и возвращает новую структуру с обновленной координатой x.
Как вы наверняка помните, функция x+ должна обрабатывать такты часов.
Мы можем взять за основу несколько первых шагов из процесса
проектирования scene+dot:
; Posn -> Posn
; увеличивает координату x в p на 3
(check-expect (x+ (make-posn 10 0)) (make-posn 13 0))
(define (x+ p)
(... (posn-x p) ... (posn-y p) ...))

Сигнатура, описание назначения и пример – все это вытекает из
постановки задачи. Вместо заголовка – функции с результатом по
умолчанию – наш макет содержит два выражения с селекторами для
posn. В конце концов, информация для вычисления результата долж-

169

170

Глава 5

на поступать из входных данных, а входные данные – это структура,
содержащая два значения.
Теперь мы легко можем завершить определение. Поскольку желаемый результат – структура posn, функция использует make-posn для
объединения составных частей:
(define (x+ p)
(make-posn (+ (posn-x p) 3) (posn-y p)))

Упражнение 73. Спроектируйте функцию posn-up-x, которая принимает структуру posn p и число n и создает новую структуру posn, подобную p, с числом n в поле x.
Интересно отметить, что x+ можно определить, используя posn-up-x:
(define (x+ p)
(posn-up-x p (+ (posn-x p) 3)))

Примечание. Такие функции, как posn-up-x, называются функциями обновления, или функциями-сеттерами. Они чрезвычайно полезны
при разработке больших программ. 
Функция также может создавать экземпляры структур из элементарных данных. Конечно, у нас уже есть встроенная функция make-posn,
которая делает именно это, но давайте рассмотрим еще один пример.
Задача. Другому коллеге поручено спроектировать функцию
reset-dot, которая устанавливает точку в координаты, где выполнен щелчок мышью.
Чтобы решить эту задачу, вспомним, как рассказывалось в разделе 4.3, что обработчики событий мыши принимают четыре значения:
текущее состояние мира, координаты x и y указателя мыши в момент
события и описание события MouseEvt.
Добавив знания из постановки задачи в рецепт проектирования
программы, получаем сигнатуру, описание назначения и заголовок:
; Posn Число Число MouseEvt -> Posn
; для щелчков мышью, (make-posn x y); иначе p
(define (reset-dot p x y me) p)

В примерах вызова обработчика событий мыши нам понадобятся
структура posn, два числа и MouseEvt – специально сформированная
строка. Например, щелчок мыши может быть представлен одной из
двух строк: "button-down" и "button-up". Первая сообщает, что пользователь нажал кнопку мыши, вторая – что отпустил. Вот два примера, которые вы, возможно, захотите изучить и погонять в интерпретаторе:
(check-expect
(reset-dot (make-posn 10 20) 29 31 "button-down")
(make-posn 29 31))
(check-expect
(reset-dot (make-posn 10 20) 29 31 "button-up")
(make-posn 10 20))

171

Добавляем структуру

Эта функция использует только элементарные данные, но описание назначения и примеры предполагают, что она различает два вида
событий мыши MouseEvts: "button-down" и все остальные. Такое разделение предполагает использование выражения cond:
(define (reset-dot p x y me)
(cond
[(mouse=? "button-down" me) (... p ... x y ...)]
[else (... p ... x y ...)]))

Следуя рецепту проектирования, этот макет ссылается на парамет­
ры, чтобы напомнить, какие данные доступны.
Дописать остальное определение снова не представляет труда, потому что описание назначения явно говорит, что должна вычислить
функция в каждом из двух случаев:
(define (reset-dot p x y me)
(cond
[(mouse=? me "button-down") (make-posn x y)]
[else p]))

Как и прежде, мы могли бы отметить, что make-posn создает экземпляры posn, но вы это и так знаете, и нет необходимости постоянно
напоминать об этом.
Упражнение 74. Скопируйте все необходимые определения конс­
тант и функций в область определений DrRa­cket. Добавьте тесты
и убедитесь, что они выполняются успешно. Затем запустите программу и с по­мощью мыши поместите красную точку в выбранное
вами место. 
Во многих программах приходится иметь дело с вложенными
структурами. Проиллюстрируем этот момент еще одним небольшим
отрывком из мировой программы.
Задача. Ваша команда проектирует игровую программу, которая перемещает объект по холсту с изменяющейся скоростью.
Выбранное представление данных требует наличия двух определений:
Не забывайте, что все
(define-struct ufo [loc vel])
; UFO -- это структура:
; (make-ufo Posn Vel)
; интерпретация: (make-ufo p v) определяет местоположение
; p и скорость перемещения v

это имеет отношение
к физике.

Ваша задача – разработать функцию ufo-move-1, которая вычисляет местоположение НЛО спустя один такт часов.
Начнем с определения примеров, которые помогут понять суть
определения данных:
(define
(define
(define
(define

v1
v2
p1
p2

(make-vel 8 -3))
(make-vel -5 -3))
(make-posn 22 80))
(make-posn 30 77))

Порядок следования
этих определений
имеет значение.
См. интермеццо 1.

172

Глава 5
(define
(define
(define
(define

u1
u2
u3
u4

(make-ufo
(make-ufo
(make-ufo
(make-ufo

p1
p1
p2
p2

v1))
v2))
v1))
v2))

Первые четыре определения создают элементы Vel и Posn. Последние четыре – создают все возможные комбинации из первых четырех.
Теперь напишем сигнатуру, описание назначения, несколько примеров и заголовок функции:
; UFO -> UFO
; определяет, куда переместится u за один такт часов;
; скорость остается неизменной
(check-expect (ufo-move-1 u1) u3)
(check-expect (ufo-move-1 u2)
(make-ufo (make-posn 17 77) v2))
(define (ufo-move-1 u) u)

В примерах применения функции используем примеры данных
и наши знания о местоположениях и скоростях. В частности, мы знаем, что транспортное средство, движущееся на север со скоростью
60 миль в час и на запад со скоростью 10 миль в час, через час окажется в 60 милях к северу от начальной точки и в 10 милях к западу. Через два часа оно будет находиться в 120 милях к северу от начальной
точки и в 20 милях к западу.
Как всегда, функция, принимающая экземпляр структуры, может
(и, вероятно, должна) извлечь информацию из структуры для вычисления результата. Итак, снова добавим выражения с селекторами
в определение функции:
(define (ufo-move-1 u)
(... (ufo-loc u) ... (ufo-vel u) ...))

Примечание. После добавления селекторов в макет функции возникает вопрос: потребуется ли нам продолжить уточнение извлекаемых данных? В конце концов, эти два селектора извлекают экземпляры posn и vel соответственно, которые в свою очередь являются
структурами, и мы можем извлечь из них значения их полей. Вот как
будет выглядеть макет функции в случае утвердительного ответа:
; UFO -> UFO
(define (ufo-move-1 u)
(... (posn-x (ufo-loc u))
... (posn-y (ufo-loc u))
... (vel-deltax (ufo-vel
... (vel-deltay (ufo-vel

...
...
u)) ...
u)) ...))

Однако такой подход делает макет функции довольно сложным.
При проектировании реалистичных программ доведение этой идеи
до логического конца может приводить к созданию невероятно сложных схем. Поэтому установим следующее правило:

Добавляем структуру

173

Если функция имеет дело с вложенными структурами, для обработки каждого уровня вложенности должна быть создана отдельная функция.
Во второй части книги это правило станет еще более важным, и мы
немного уточним его. Конец.
Теперь сосредотачиваемся на том, как объединить данные экземпляры posn и vel, чтобы вычислить следующее местоположение НЛО,
как того требуют наши знания физики. В частности, мы знаем, что
нужно «сложить» их вместе, где слово «сложить» может не означать
операцию, которую мы обычно применяем к числам. Итак, представим, что у нас есть функция сложения Vel и Posn:
; Posn Vel -> Posn
; складывает v и p
(define (posn+ p v) p)

Запишем сигнатуру, описание назначения и заголовок, следуя
установленному нами рецепту проектирования. Это называется «загадывать желание» и является частью «составления списка желаний»,
как описано в разделе 3.4.
Загадывать желания нужно так, чтобы потом мы смогли реализовать функцию, над которой работаем. Для этого можно использовать
методику деления сложных задач на более простые подзадачи, которая поможет нам решить задачу небольшими шагами. Вот полное
определение ufo-move-1 для данной постановки задачи:
(define (ufo-move-1 u)
(make-ufo (posn+ (ufo-loc u) (ufo-vel u))
(ufo-vel u)))

Поскольку ufo-move-1 и posn+ у нас полностью определены, можно
даже щелкнуть на кнопке RUN (Выполнить) и проверить, не сообщит
ли DrRa­cket о грамматических ошибках. Естественно, тесты завершаются с ошибками, потому что posn+ – это пока простое желание, но не
функция, которая нам нужна.
Теперь сосредоточимся на posn+. Мы выполнили первые В геометрии операция,
два шага из рецепта проектирования (определили данные, соответствующая
функции posn+,
сигнатуру/назначение/заголовок), поэтому дальше созда- называется переносом.
дим примеры. Один простой способ создать примеры для
«желания» – использовать примеры для исходной функции и превратить их в примеры для новой функции:
(check-expect (posn+ p1 v1) p2)
(check-expect (posn+ p1 v2) (make-posn 17 77))

В данном случае мы знаем, что выражение (ufo-move-1 (make-ufo p1
v1)) должно дать в результате p2. В то же время мы знаем, что ufomove-1 применяет posn+ к p1 и v1, то есть для этих входных данных posn+
должна дать p2.

174

Глава 5

Стоп! Проверьте наши расчеты вручную, чтобы убедиться, что вы
понимаете все, что мы делаем.
Теперь добавим селекторы в наш макет:
(define (posn+ p v)
(... (posn-x p) ... (posn-y p) ...
... (vel-deltax v) ... (vel-deltay v) ...))

Поскольку posn+ использует экземпляры Posn и Vel и каждый экземпляр является структурой с двумя полями, мы получаем четыре
выражения. В отличие от выражений с вложенными селекторами,
приведенных выше, это простые применения селекторов к пара­
метрам.
Если вспомнить, что означают эти четыре выражения, или как мы
вычисляли желаемый результат на основе двух структур, то мы с легкостью сможем завершить определение posn+:
(define (posn+ p v)
(make-posn (+ (posn-x p) (vel-deltax v))
(+ (posn-y p) (vel-deltay v))))

Первый шаг – прибавить скорость по горизонтали к координате x
и скорость по вертикали к координате y. Это дает два выражения, по
одному для каждой новой координаты. С помощью make-posn новые
координаты можно объединить в одну структуру Posn.
Упражнение 75. Введите эти определения и тестовые примеры
в область определений DrRa­cket и убедитесь, что они выполняются
без ошибок. Это первый случай, когда вы имеете дело с «желанием»,
и вы должны убедиться, что понимаете, как работает эта пара функций. 

5.7. Вселенная данных
У каждого языка своя вселенная данных. Эти данные представляют информацию из внешнего мира, описывающую
его. Именно этими данными манипулируют программы.
Эта вселенная данных представляет собой коллекцию, которая содержит не только все встроенные данные, но также
любые экземпляры данных, которые любая программа может когда-либо создать.
Слева на рис. 8 показан один из способов представления вселенной
данных в языке BSL. Поскольку существует бесконечно много чисел
и строк, коллекция всех данных бесконечна. Мы обозначили «бесконечность» на рис. 8 многоточием «...», но настоящие определения
должны избежать таких неточностей.
Ни программы, ни отдельные функции в программах никогда не
будут сталкиваться со всей вселенной данных. Цель определения данных – описать части этой вселенной и дать им имена, чтобы потом

Помните, что
математики называют
это коллекциями
данных, или множеством
классов данных.

175

Добавляем структуру

можно было кратко ссылаться на них. Другими словами, определение
именованных данных – это описание коллекции данных, а выбранное имя можно использовать в других определениях данных и в сигнатурах функций. Имя в сигнатуре функции указывает, какие данные
будет обрабатывать функция и, неявно, с какой частью вселенной
данных она не будет иметь дело.

0 1 2 3 ...

0 1 2 3 ...

#true #false
"hello"
"world"
"good"
"bye"

#true #false
"hello"
"world"
"good"
"bye"



4-3i



4-3i

Рис. 8. Вселенная данных

Определения данных, которые мы использовали в первых четырех
главах, ограничивались встроенными коллекциями данных. Для этого мы использовали явную или неявную детализацию всех значений,
включенных в коллекцию. Например, область с серым фоном справа
на рис. 8 соответствует следующему определению данных:
;
;
;
;

BS -- одно из значений:
--- "hello",
--- "world",
--- число пи.

Это конкретное определение данных выглядит довольно бессмысленным, но обратите внимание на стилизованное сочетание слов на
естественном языке и на языке BSL. Это определение является точным и недвусмысленным. Оно точно определяет, какие элементы
принадлежат BS, а какие нет.
Определение структур полностью меняет картину. Когда программист определяет структуры, вселенная расширяется и дополняется
всеми возможными экземплярами структуры. Например, добавление posn означает появление экземпляров posn со всеми возможными значениями в двух полях. Кружок в центре на рис. 9 изображает
добавление этих значений, включая такую кажущуюся бессмыслицу,
как (make-posn "hello" 0) и (make-posn (make-posn 0 1) 2). И да, некоторые
из этих экземпляров posn не имеют для нас никакого смысла. Но программа на BSL может сконструировать любой из них.

176

Глава 5

0 1 2 3 ...

#true #false
"hello"
"world"
"good"
"bye"

4-3i



( make-posn "hello" 0)
( make-posn "world" 1)
( make-posn "good" 2)
( make-posn "bye" 3)
( make-posn
( make-posn 0 1) 2)
( make-posn 0 3)
( make-posn 1 3)
( make-posn 2 3)
( make-posn 3 3)

( make-ball -1 0)
( make-ball -1 1)
( make-ball -1 2)
( make-ball -1 3)
make-ball "bye" #t)

Рис. 9. Добавление структуры во вселенную данных

Добавление еще одного определения структуры снова расширяет
вселенную данных всеми возможными комбинациями. Допустим, мы
добавили определение структуры ball с двумя полями. Как показано в третьем круге на рис. 9, добавление этого определения создает
коллекцию экземпляров ball, которые содержат числа, структуры posn
и т. д., а также экземпляры posn, которые содержат экземпляры ball.
Попробуйте создать такие экземпляры в DrRa­cket! Добавьте
(define-struct ball [location velocity])

в область определений, щелкните на кнопке RUN (Выполнить) и создайте несколько экземпляров структур.
С практической точки зрения, определение данных с использованием структур описывает большие коллекции данных через комбинации существующих определений данных. Когда мы пишем
; Posn -- это (make-posn Число Число)

мы описываем бесконечное количество возможных экземпляров
posn. Как и выше, в определениях данных используются комбинации
выражений на естественном языке, наборов данных, определенных
в другом месте, и конструкторов данных. На данный момент в определении данных больше ничего не должно присутствовать.
Определение данных с использованием структур определяет новую коллекцию данных, состоящую из экземпляров этих структур, которые будут использоваться нашими функциями. Например, определение данных с использованием структуры posn определяет заштрихованную область в центральном круге вселенной на рис. 9, который
включает все структуры posn, два поля которых содержат числа. В то
же время есть возможность создать экземпляр posn, не удовлетворяющий требованию, что оба поля должны содержать числа:
(make-posn (make-posn 1 1) "hello")

Эта структура содержит экземпляр posn в поле x и строку в поле y.

Добавляем структуру

Упражнение 76. Сформулируйте определения данных для следующих определений типов структур:
zz (define-struct movie [title producer year]);
zz (define-struct person [name hair eyes phone]);
zz (define-struct pet [name number]);
zz (define-struct CD [artist title price]);
zz (define-struct sweater [material size producer]).

Сделайте разумные предположения о том, какие значения могут
содержаться в каждом поле. 
Упражнение 77. Определите структуру и данные для представления времени суток. Время состоит из трех чисел: часов, минут и секунд. 
Упражнение 78. Определите структуру и данные для представления трехбуквенных слов. Слово состоит из строчных букв, представленных односимвольными строками 1String от "a" до "z", плюс #false.
Примечание. Это упражнение является частью проекта игры «Виселица»; см. упражнение 396. 
Программисты не только пишут определения данных, но и читают
их, чтобы понять, как работает та или иная программа, расширить
типы данных, с которыми они могут работать, устранить ошибки
и т. д. Мы читаем определения данных, чтобы понять, как создавать
данные, принадлежащие указанной коллекции, и выяснить, принадлежит ли экземпляр данных указанному классу.
Поскольку определения данных играют такую важную роль в процессе проектирования, часто желательно сопровождать определения
данных примерами, как мы сопровождаем функции примерами, иллюстрирующими их поведение. Создавать примеры данных из определений довольно просто:
zz для встроенной коллекции данных (число, строка, логическое

значение, изображение) используйте любые примеры, какие
вам нравятся;
ПРИМЕЧАНИЕ. Иногда для обозначения встроенных коллекций данных люди используют описательные имена, например
NegativeNumber или OneLetterString. Но они не являются заменой
хорошо написанного определения данных. КОНЕЦ.

zz для перечисления используйте несколько элементов перечис-

ления;

zz для интервалов используйте конечные точки (если они есть)

и хотя бы одну точку внутри;

zz для детализаций проиллюстрируйте каждую часть отдельно;
zz для структур следуйте описанию на естественном языке, то

есть используйте конструктор и выберите примеры из коллекций данных, соответствующих каждому полю.

177

178

Глава 5

Это все, что потребуется для создания примеров на основе определений данных в большей части этой книги, правда, сами определения данных будут становиться все сложнее с каждой последующей
главой.
Упражнение 79. Сконструируйте примеры для следующих определений данных:
zz ; Color -- одна из строк, обозначающих цвет:

;
;
;
;
;
;
;

---------------

"white"
"yellow"
"orange"
"green"
"red"
"blue"
"black"

ПРИМЕЧАНИЕ. DrRa­cket распознает многие другие строки как
цвета. КОНЕЦ.
zz ; H -- это Число между 0 и 100.

; интерпретация: представляет уровень счастья
zz (define-struct person [fstname lstname male?])

; Person -- это структура:
;
(make-person Строка Строка Логическое_значение)
Насколько оправданно использовать имя поля, похожее на имя
предиката?
zz (define-struct dog [owner name age happiness])

; Dog -- это структура:
; (make-dog Person Строка ПоложительноеЧисло H)
Добавьте интерпретацию в это определение данных.
zz ; Weapon -- одно из значений:

;
;
;
;

--- #false
--- Posn
интерпретация: #false означает, что ракета еще не запущена;
Posn представляет местоположение запущенной ракеты

Последнее определение является примером необычной детализации, сочетающей встроенные данные с типом структуры. Подобные
определения мы подробно рассмотрим в следующей главе.

5.8. Проектирование с использованием
структур
Добавление структурных типов усиливает необходимость неукоснительного выполнения всех шести шагов в рецепте проектирования.
Мы больше не можем полагаться на встроенные коллекции данных

Добавляем структуру

для представления информации. Теперь ясно, что программисты
должны создавать определения данных для всех задач, кроме самых
простых.
Этот раздел добавляет еще один рецепт проектирования, иллюст­
рируя его следующим образом:
Задача. Спроектируйте функцию, которая вычисляет расстояние
от объектов в трехмерном пространстве до начала координат.
Поехали:
1. Когда задача требует представления информации, части которой неразделимо связаны друг с другом или описывают естест­
венное целое, необходимо определить структуру. Структура
должна включать столько полей, сколько имеется соответствующих свойств. Экземпляр такой структуры соответствует целому, а значения полей – атрибутам этого целого.
Определение данных для структуры предполагает определение
имени для коллекции допустимых экземпляров. Кроме того,
определение данных должно описывать, какие данные какому
полю соответствуют. В определении можно использовать только имена встроенных коллекций данных или ранее объявленных определений данных.
В конце концов, мы (и другие) должны иметь возможность создавать экземпляры структуры, используя определение данных.
В противном случае наше определение данных будет считаться
неполным. Чтобы гарантировать возможность создания экземпляров, определения данных должны сопровождаться примерами данных.
Вот как можно применить эту идею к нашей задаче:
(define-struct r3 [x y z])
; R3 -- это структура:
; (make-r3 Число Число Число)
(define ex1 (make-r3 1 2 13))
(define ex2 (make-r3 -1 0 3))

Определение структуры вводит новый тип структуры, r3,
а определение данных – имя R3 для всех экземпляров структуры r3, содержащих только числа.
2. По-прежнему нужно определить сигнатуру, описание назначения и заголовок функции, но этот шаг ничем не отличается от
аналогичного шага в прежнем рецепте проектирования.
Стоп! Выполните этот шаг самостоятельно для данной постановки задачи.
3. Используйте примеры из первого шага для создания примеров применения функции. Для каждого поля, представляющего интервал или перечисление, обязательно создайте примеры
с использованием конечных и промежуточных точек. Мы наде-

179

180

Глава 5

емся, что вы сможете выполнить этот шаг самостоятельно для
данной постановки задачи.
4. Функция, принимающая структуры, часто, но не всегда извлекает значения из различных полей структур. Чтобы напомнить
себе о такой возможности, добавьте селектор для каждого поля
в макеты таких функций.
Вот как это можно представить для данной постановки задачи:
; R3 -> Число
; определяет расстояние от p до начала координат
(define (r3-distance-to-0 p)
(... (r3-x p) ... (r3-y p) ... (r3-z p) ...))

При желании рядом с каждым выражением применения селектора можно показать, какие данные оно извлекает из этой
структуры; подобную информацию можно взять из определения данных. Стоп! Просто сделайте это!
5. При определении функции используйте селекторы из макета.
Но имейте в виду, что некоторые из них могут не понадобиться.
6. Тестирование. Протестируйте функцию сразу же, как только будет написан заголовок функции. Повторяйте тестирование,
пока не будут охвачены все выражения. Тестируйте снова всякий раз, когда будете вносить изменения.
Завершите решение поставленной задачи. Если вы не можете вспомнить формулу вычисления расстояния от точки
до начала координат в трехмерном пространстве, то по­
ищите ее в книге по геометрии.
Упражнение 80. Создайте макеты для функций, принимающих
экземпляры следующих структур:

Там вы найдете такую
формулу:

zz (define-struct movie [title director year]);
zz (define-struct pet [name number]);
zz (define-struct CD [artist title price]);
zz (define-struct sweater [material size color]).

Для этой задачи не нужно создавать определения данных. 
Упражнение 81. Спроектируйте функцию time->seconds, которая
принимает экземпляр структуры с представлением времени суток
(см. упражнение 77) и возвращает количество секунд, прошедших
с полуночи. Например, если в структуре передается время 12 часов
30 минут и 2 секунды, то применение time->seconds к этому экземп­
ляру должно вернуть 45002. 
Упражнение 82. Спроектируйте функцию compare-word. Функция
должна принимать два трехбуквенных слова (см. упражнение 78)
и возвращать слово, указывающее, где указанные слова совпадают и не совпадают. Функция должна сохранять содержимое полей
в структуре с результатом, соответствующие поля в исходных словах

Добавляем структуру

совпадают, а в поле, где обнаружено несовпадение, она должна по­
мес­тить значение #false.
Подсказка. Упражнение подразумевает две задачи: сравнение
слов и сравнение «букв». 

5.9. Структура в мире
Когда мировая программа должна следить за двумя независимыми
элементами информации, для представления данных о состоянии
мира необходимо использовать коллекцию структур. Одно поле хранит одну часть информации, а другое поле – вторую. Естественно,
если мир предметной области характеризуется большим количеством
элементов информации, определение структуры должно содержать
столько полей, сколько имеется отдельных элементов информации.
Рассмотрим игру с космическими захватчиками, в которой имеются два игровых объекта: НЛО и танк. НЛО спускается вертикально вниз, а танк движется по горизонтали вдоль нижнего края сцены.
Если оба объекта движутся с известной постоянной скоростью, то
для их описания достаточно определить по одному элементу информации для каждого объекта: координату y для НЛО и координату x
для танка. Для объединения этих элементов информации требуется
структура с двумя полями:
(define-struct space-game [ufo tank])

Мы оставляем вам, как самостоятельное задание, сформулировать
адекватное определение данных для этого определения структуры, включая интерпретацию. Обратите внимание на дефис в имени
структуры. Язык BSL позволяет использовать всевозможные символы
в именах переменных, функций, структур и имен полей. Какие имена
получат селекторы для этой структуры? Какое имя получит предикат?
Каждый раз, когда мы говорим «элемент информации», мы не всегда имеем в виду одно число или одно слово. Элемент информации
сам может объединять несколько элементов информации. Создание
представления данных для такого рода информации естественным
образом приводит к вложенным структурам.
Добавим немного остроты в нашу воображаемую игру с космическими захватчиками. НЛО, спускающееся только по вертикали, – это
скучно. Чтобы сделать игру более интересной, в которой танк атакует
НЛО, НЛО должен иметь возможность спускаться не по прямой, возможно, прыгая случайным образом. Для реализации этой идеи нам
понадобятся две координаты, описывающие местоположение НЛО,
поэтому пересмотрим наше определение данных для космической
игры:
; SpaceGame -- это структура:
; (make-space-game Posn Число).

181

182

Глава 5
; интерпретация: (make-space-game (make-posn ux uy) tx)
; описывает конфигурацию, согласно которой НЛО находится в позиции
; (ux,uy), а танк -- на поверхности земли с x-координатой tx

Чтобы научиться понимать, какие представления данных необходимы для мировых программ, необходима практика. В следующих
двух разделах мы рассмотрим несколько достаточно сложных постановок задач. Решите их, прежде чем переходить к играм, которые вы,
возможно, захотите разработать самостоятельно.

5.10. Графический редактор
Чтобы написать программу на языке BSL, нужно запустить среду программирования DrRa­cket и набрать текст программы на клавиатуре.
Посмотрите, как набирается текст. Нажатие клавиши со стрелкой влево перемещает курсор влево; нажатие клавиши Backspace (или Delete) стирает одну букву слева от курсора (или справа соответственно), если, конечно, такая буква имеется.
Этот процесс называется «редактированием», хотя точнее его было
бы называть «редактированием текста программы», потому что далее мы будем использовать слово «редактирование» для обозначения более сложной задачи, чем набор текста на клавиатуре. Когда вы
пишете и редактируете другие виды документов, то, вероятно, используете иные программные приложения, называемые текстовыми
процессорами, хотя специалисты по информатике называют их все
редакторами или даже графическими редакторами.
Теперь вы обладаете достаточным объемом знаний, чтобы разработать универсальную программу, которая действует как однострочный редактор для простого текста. Редактирование в данном случае
включает ввод букв и какое-либо изменение уже существующего текста, в том числе удаление и вставку букв. Это подразумевает наличие
некоторого представления о позиции в тексте. Люди называют эту
позицию курсором; большинство графических редакторов отображают его особым образом, чтобы его легко было обнаружить.
Взгляните на следующую конфигурацию редактора:

hello world
Кто-то уже ввел текст «helloworld» и пять раз нажал клавишу со
стрелкой влево, в результате чего курсор переместился из конца текс­
та в положение между буквами «o» и «w». Если теперь нажать клавишу пробела, то изображение в редакторе изменится следующим
образом:

hello world

Добавляем структуру

Проще говоря, это действие приведет к вставке пробела и перемещению курсора вправо, между пробелом и буквой «w».
Учитывая вышесказанное, редактор должен поддерживать два элемента информации:
1) введенный текст;
2) текущее положение курсора.
А это предполагает использование структуры с двумя полями.
Мы можем представить несколько разных способов преобразования информации в данные и обратно. Например, одно поле в структуре может содержать весь введенный текст, а другое – индекс, то
есть количество символов между началом строки и курсором. Другое
представление данных – использовать две строки в двух полях: в одном поле хранится часть текста слева от курсора, а в другом – часть
текста справа. Мы выбрали этот второй подход к представлению состояния редактора:
(define-struct editor [pre post])
; Editor -- это структура:
; (make-editor Строка Строка)
; интерпретация: (make-editor s t) описывает редактор
; с видимым в нем текстом (string-append s t), где
; курсор отображается между s и t

Решите несколько следующих упражнений, исходя из этого представления данных.
Упражнение 83. Спроектируйте функцию render, которая принимает состояние редактора Editor и создает изображение.
Назначение функции – показать текст в пустой сцене размером
200×20 пикселей. Курсор должен отображаться как красный прямоугольник с размерами 1×20, а текст строки – черным шрифтом с размером 16.
Сконструируйте в области взаимодействия DrRa­cket изображение,
которое будет играть роль образца. Мы сами начали со следующего
выражения:
(overlay/align "left" "center"
(text "hello world" 11 "black")
(empty-scene 200 20))

Возможно, вам понадобится заглянуть в документацию с описанием beside, above и других подобных функций. Если вам понравится
внешний вид изображения, используйте сконструированное выражение в качестве теста и руководства по проектированию функции
render. 
Упражнение 84. Спроектируйте функцию edit. Эта функция принимает два параметра, состояние редактора ed и KeyEvent ke, и создает редактор. Ее назначение – добавить символ из события ke в конец
поля pre в структуре ed, если ke не является событием нажатия клавиши Backspace ("\b"). В противном случае функция должна удалить

183

184

Глава 5

символ слева от курсора (если он есть). Функция игнорирует нажатие
клавиш табуляции ("\t") и Return ("\r").
Функция обращает внимание только на два события длиннее одной
буквы: "left" и "right". По нажатии клавиши со стрелкой влево курсор
должен переместиться на один символ влево (если он есть), а по нажатии клавиши со стрелкой вправо курсор должен переместиться на
один символ вправо (если он есть). Все другие события клавиатуры
игнорируются.
Сконструируйте несколько тестовых примеров для edit, обращая
внимание на особые случаи. Решая это упражнение, мы создали
20 примеров и превратили их все в тесты.
Подсказка. Рассуждайте об этой функции как о принимающей события клавиатуры – коллекции, которая определена как перечисление. Она может использовать вспомогательные функции для работы
со структурой Editor, представляющей состояние редактора. Держите
список желаний под рукой; вам потребуется спроектировать все эти
вспомогательные функции, такие как string-first, string-rest, stringlast и string-remove-last. Если вы еще не сделали этого, выполните
упражнения из раздела 2.1. 
Упражнение 85. Определите функцию run. Принимая поле pre
структуры Editor, она должна запускать интерактивный редактор,
используя функции render и edit из двух предыдущих упражнений
в предложениях to-draw и on-key соответственно. 
Упражнение 86. Обратите внимание, что если попытаться ввес­
ти длинный текст, то наша программа-редактор не сможет отобразить его весь. Не вместившийся в сцену текст просто будет обрезан
по правому краю. Измените функцию edit из упражнения 84 так,
чтобы она игнорировала нажатия клавиш, если после добавления
нового символа в конец поля pre текст окажется слишком длинным
для холста. 
Упражнение 87. Разработайте представление данных для редактора на основе нашей первой идеи с использованием строки и индекса. Затем снова выполните предыдущие упражнения, следуя рецепту проектирования. Подсказка: выполните упражнения из раздела 2.1, если вы еще не сделали этого.
Замечание о выборе дизайна. Главная цель упражнений – на­
учить вас проектированию. Как показывают примеры, самое первое
решение, принимаемое при проектировании, касается представления данных. Чтобы сделать правильный выбор, необходимо заранее
обдумать и взвесить каждый из возможных вариантов. Конечно, чтобы добиться успеха в этом вопросе, нужно набраться опыта. 

5.11. Больше виртуальных питомцев
В этом разделе мы продолжим наш проект виртуального зоопарка из раздела 3.7. В частности, цель этого упражнения – объединить

185

Добавляем структуру

программу кошачьего мира с программой управления индикатором
счастья. Объединенная программа должна отображать кошку, перемещающуюся по холсту, и с каждым шагом ее усталость нарастает,
а уровень счастья падает. Единственный способ поднять настроение
кошке – покормить ее (нажать клавишу со стрелкой вниз) или погладить (нажать клавишу со стрелкой вверх). Наконец, цель последнего
упражнения в этом разделе – создать еще одного счастливого виртуального питомца.
Упражнение 88. Определите структуру, которая хранит координату x кошки и уровень ее счастья. Затем сформулируйте определение
данных для кошек с названием VCat, включая интерпретацию. 
Упражнение 89. Спроектируйте мировую программу happy-cat, которая управляет гуляющей кошкой и ее уровнем счастья. В первый
момент после запуска программы кошка должна быть совершенно
довольна.
Подсказки. (1) Повторно используйте функции из мировых программ в разделе 3.7. (2) Используйте структуру из предыдущего
упражнения для представления состояния мира. 
Упражнение 90. Измените программу happy-cat из предыдущих
упражнений так, чтобы она прекращала выполнение всякий раз, когда уровень счастья кошки падает до 0. 
Упражнение 91. Расширьте определение структуры и определение данных из упражнения 88, включив в него поле направления
движения. Измените программу happy-cat так, чтобы кошка двигалась
в направлении, заданном в этом поле. Программа должна перемещать кошку в текущем направлении и разворачивать ее по достижении любого края сцены. 

(define cham

)

Приведенный выше рисунок хамелеона – это прозрачное изображение. Чтобы вставить его в DrRa­cket, выберите в меню пункт Insert
Image (Вставить рисунок). Вставка таким способом сохранит прозрачность пикселей рисунка.
При объединении частично прозрачного изображения с цветной
фигурой, например прямоугольником, изображение принимает цвет
фигуры. На рисунке хамелеона внутренняя часть прозрачная, а область снаружи – белая. Попробуйте выполнить следующее выражение
в DrRa­cket:
(define background
(rectangle (image-width cham)
(image-height cham)
"solid"
"red"))
(overlay cham background)

186

Глава 5

Упражнение 92. Спроектируйте программу cham, которая непрерывно перемещает хамелеона по холсту слева направо. По достижении правого края хамелеон должен исчезать и сразу же появляться
слева. Подобно кошке, хамелеон постепенно устает от прогулки, и его
уровень счастья падает.
Для управления шкалой уровня счастья хамелеона можно повторно использовать шкалу уровня счастья виртуальной кошки. Чтобы
поднять настроение хамелеону, его можно покормить (нажав клавишу со стрелкой вниз); гладить хамелеона нельзя. Конечно, как и все
хамелеоны, наш тоже может менять цвет: нажатие клавиши "r" делает его красным, "b" – синим, а "g" – зеленым. Объедините программы
хамелеона и кошки и по возможности повторно используйте функции из последней.
Начните с определения данных VCham для представления хаме­
леона. 
Упражнение 93. Скопируйте решение упражнения 92 и измените копию так, чтобы хамелеон прогуливался по трехцветному фону.
В нашем решении используются следующие цвета:
(define BACKGROUND
(beside (empty-scene WIDTH HEIGHT "green")
(empty-scene WIDTH HEIGHT "white")
(empty-scene WIDTH HEIGHT "red")))

Но вы можете использовать любые другие цвета. Посмот­
рите, как хамелеон меняет цвет, пересекая границу между
двумя цветами.
Примечание. Если внимательно посмотреть анимацию, то можно заметить, что хамелеон находится внутри белого прямоугольника.
Если вы умеете пользоваться программами для редактирования изображений, измените изображение так, чтобы белый прямоугольник
был невидим. Тогда хамелеон будет по-настоящему сливаться с фоном. 

Поешьте итальянской
пиццы, когда закончите!

6. Структуры и детализация
В двух предыдущих главах были представлены два способа формулировки определений данных. Способ с применением детализации (перечислений и интервалов) используется для создания небольших коллекций из больших. Способ с применением структур используется для
объединения нескольких коллекций. Конструирование представлений
данных является основой для правильного проектирования программ,
поэтому неудивительно, что у программистов часто возникает желание детализировать определения данных, включающие структуры, или
использовать структуры для объединения детализированных данных.
Вспомните игру с воображаемыми космическими захватчиками из
раздела 5.9 в предыдущей главе. Пока что в ней имеются один НЛО,
спускающийся из космоса, и один танк на земле, движущийся по горизонтали. В нашем представлении данных используется структура
с двумя полями: одно для представления данных об НЛО и другое –
для представления данных о танке. Естественно, игровой танк должен иметь возможность запускать ракеты. Вот так неожиданно мы
подошли к состоянию еще одного типа, содержащему три объекта,
движущихся независимо: НЛО, танк и ракету. Теперь у нас есть две
разные структуры: одна представляет два независимо движущихся
объекта, а другая – третий. Поскольку состояние мира может быть
только одной структурой, естественно использовать детализацию для
описания всех возможных состояний:
1) состояние мира – это структура с двумя полями;
2) состояние мира – это структура с тремя по- Не волнуйтесь,
лями.
в следующей части

книги мы расскажем, как

Что касается нашей предметной области – ре- реализовать запуск
альной игры, – первое состояние представляет любого количества
игру до того, как танк запустит свою единственную ракет без перезарядки.
ракету, а второе – после запуска ракеты.
В этой главе мы представим ​​основной замысел детализации определений данных, содержащих структуры. Поскольку у нас уже есть
все необходимые ингредиенты, то сразу же приступим к детализации
структур. После этого мы обсудим некоторые примеры, в том числе
примеры мировых программ, извлекающих выгоду из наших новых
возможностей. А в заключительном разделе поговорим об ошибках
в программировании.

6.1. Проектирование с использованием
детализации, снова
Для начала уточним постановку задачи для игры с космическими захватчиками из раздела 5.6.

188

Глава 6

Задача. Используя библиотеку 2htdp/universe, спроектируйте игровую программу, имитирующую бой с захватчиками из
космоса. Игрок управляет танком (маленький прямоугольник),
который должен защищать нашу планету (нижняя часть холста) от НЛО (одну из возможных реализаций вы найдете в разделе 4.4), который спускается сверху вниз. Чтобы помешать
НЛО приземлиться, игрок может запустить одну ракету (треугольник меньше танка), нажав клавишу пробела. Если ракета
попадет в НЛО, игрок выигрывает; иначе НЛО приземляется,
и игрок проигрывает.
Вот некоторые подробности о трех игровых объектах и ​​их перемещениях. Во-первых, танк движется с постоянной скоростью
вдоль нижнего края холста. Для изменения направления движения танка игрок может использовать клавиши со стрелками
влево и вправо. Во-вторых, НЛО спускается с постоянной скоростью, но совершает небольшие случайные прыжки влево или
вправо. В-третьих, после запуска ракета поднимается вертикально вверх с постоянной скоростью, которая в два раза больше скорости спуска НЛО. Наконец, считается, что ракета попала
в НЛО, если их координаты достаточно близки, независимо от
того, что понимается под «достаточной близостью».
Эта постановка задачи будет использоваться в следующих двух
подразделах в качестве рабочего примера, поэтому, прежде чем продолжить, внимательно изучите ее и решите следующее упражнение.
Это поможет вам понять задачу достаточно глубоко.
Упражнение 94. Нарисуйте несколько набросков игрового пейзажа на разных этапах. Используйте наброски, чтобы определить
постоянные и переменные элементы игры. Для начала определите
физические и графические константы, которые описывают размеры
мира (холста) и его объектов. Затем сконструируйте фоновый пейзаж. Наконец, создайте начальную сцену с использованием констант,
определяющих параметры танка, НЛО и фона. 
Определение детализации. Первый шаг в рецепте проектирования требует написать определение данных. Одна из целей определения данных – описать конструкцию данных, представляющих состояние мира; другая цель – описать все возможные элементы данных,
которые могут принимать функции обработки событий
Для этой игры с косми- в мировой программе. Мы еще не встречались с перечислеческими захватчиками ниями, включающими структуры, поэтому в первом подмы могли бы обойтись
одним определением разделе мы рассмотрим эту идею. Возможно, вам эта идея
структуры с тремя не покажется новой, тем не менее обратите на нее приполями, где третье стальное внимание.
поле содержит #false
Как отмечалось во введении к этой главе, игра, имитирую­
до запуска ракеты
щая
бой ракетного танка с космическими захватчиками,
и структуру posn с коортребует
представления данных для двух различных состоядинатами ракеты после
ее запуска. См. ниже. ний игры. Мы выбрали вариант с двумя структурами:

Структуры и детализация
(define-struct aim [ufo tank])
(define-struct fired [ufo tank missile])

Первая предназначена для представления периода времени, когда
игрок пытается поставить танк в позицию для выстрела, а вторая –
для представления состояний после выстрела ракеты. Однако, преж­
де чем сформулировать полное определение данных для состояния
игры, нужны создать представления данных для танка, НЛО и ракеты.
Допустим, что вы уже объявили такие физические константы, как
WIDTH и HEIGHT, которые являются предметом упражнения 94, и сформулируем следующие определения данных:
; UFO -- это Posn.
; интерпретация: (make-posn x y) -- местоположение НЛО
; (используется соглашение о системе координат с началом в левом верхнем углу)
(define-struct tank [loc vel])
; Tank -- это структура:
; (make-tank Число Число).
; интерпретация: (make-tank x dx) определяет позицию:
; (x, HEIGHT) и скорость перемещения танка: dx пикселей за такт часов
; Missile -- это Posn.
; интерпретация: (make-posn x y) -- местоположение ракеты

Каждое из этих определений данных описывает структуру, либо
вновь определенную, как tank, либо встроенную коллекцию данных
Posn. Вас может немного удивить то, что определения Posn используются для представления двух разных аспектов мира. С другой стороны, в реальном мире мы широко используем числа (а также строки
и логические значения) для представления самых разных видов информации, поэтому повторное использование коллекции структур,
таких как Posn, не является чем-то особенным.
Теперь можно сформулировать определения данных для состояния
игры с космическими захватчиками:
;
;
;
;
;

SIGS -- это одно из следующих значений:
-- (make-aim UFO Tank)
-- (make-fired UFO Tank Missile)
интерпретация: представляет полное состояние игры
с космическими захватчиками

Определение данных имеет форму детализации. При этом каждое
предложение описывает содержимое структуры точно так же, как
определения структур, которые мы видели до сих пор. Однако, как
показывает это определение, не всякое определение данных включает только одну структуру; в данном случае определение данных включает две разные структуры.
Смысл такого определения прост. Оно вводит имя SIGS для коллекции всех экземпляров структур, которые можно создать в соответствии с определением. Давайте создадим несколько примеров для
наглядности:

189

190

Глава 6
zz вот пример, описывающий состояние игры, когда танк переме-

щается к позиции пуска ракеты:

(make-aim (make-posn 20 10) (make-tank 28 -3))

zz следующий пример соответствует состоянию игры после пуска

ракеты:

(make-fired (make-posn 20 10)
(make-tank 28 -3)
(make-posn 28 (- HEIGHT TANK-HEIGHT)))

разумеется, имена, состоящие только из заглавных букв, – это
имена констант, которые вы должны были определить в упражнении 94;
zz и последний пример соответствует состоянию, когда ракета попала в НЛО:
(make-fired (make-posn 20 100)
(make-tank 100 3)
(make-posn 22 103))

Этот пример предполагает, что холст имеет ширину больше
100 пикселей.
Обратите внимание, что первый экземпляр SIGS создается в соответствии с первым предложением в определении данных, а второй
и третий – в соответствии со вторым предложением. Естественно,
числа в каждом поле зависят от вашего выбора значений глобальных
игровых констант.
Упражнение 95. Покажите, как при получении этих трех экземпляров использовался первый или второй вариант из определения
данных. 
Упражнение 96. Нарисуйте примерный вид игровой сцены для
каждого из трех состояний на холсте размером 200×200 пикселей. 
Рецепт проектирования. После знакомства с новым способом
формулировки определений данных пришла пора уточнить рецепт
проектирования. В этой главе исследуется возможность объединения
двух и более средств описания данных, и уточненный рецепт проектирования отражает это обстоятельство, особенно первый его шаг:
1) когда может понадобиться этот новый способ определения
данных? Вы уже знаете, что необходимость в детализации обу­
словлена различиями между разными классами информации
в постановке задачи. Точно так же потребность в определении
данных на основе структуры возникает из-за необходимости
сгруппировать несколько разных элементов информации.
Детализация разных форм данных, включая коллекции структур, требуется, когда в постановке задачи присутствуют разные
виды информации и когда хотя бы некоторые из них отличаются разным составом элементов.

Структуры и детализация

Следует иметь в виду, что определения данных могут ссылаться
на другие определения данных. Поэтому если конкретное предложение в определении данных выглядит слишком сложным,
допускается добавить отдельное определение данных для этого
предложения и сослаться на это вспомогательное определение.
И как всегда, нужно сформулировать примеры данных, используя их определения;
2) второй шаг остается прежним. Сформулируйте сигнатуру функции, в которой упоминаются только имена вновь добавленных
или встроенных коллекций данных, добавьте описание назначения и определите заголовок функции;
3) третий шаг тоже не изменился. Вы все так же должны сформулировать примеры применения функций, иллюстрирующие
описание назначения из второго шага, и все так же должны записать хотя бы по одному примеру для каждого элемента в детализации;
4) при разработке макета теперь необходимо учитывать два разных аспекта: саму детализацию и структуры в ее предложениях.
Для учета первого аспекта тело макета должно состоять из выражения cond, включающего столько условий, сколько имеется
элементов в детализации. Также в каждое условие нужно добавить выражение cond, идентифицирующее подклассы данных
в соответствующем элементе.
Для учета второго аспекта, если элемент представлен структурой, макет должен содержать выражения применения селекторов в каждом условии cond, идентифицирующем подкласс данных, описанный в элементе.
Однако если вы решили описать данные с использованием отдельного определения, то выражения селекторов добавлять
не нужно. Вместо этого следует создать макет для отдельного
определения данных в текущей задаче и ссылаться на этот макет путем применения функции. Это будет указывать на то, что
данный подкласс данных обрабатывается отдельно.
Прежде чем приступить к разработке макета, поразмышляйте о характере функции. Если формулировка задачи предполагает необходимость решения нескольких задач, вполне
вероятно, что вместо одной функции потребуется спроектировать и написать несколько функций. В таком случае пропустите
этот шаг;
5) заполните пробелы в макете. Чем сложнее определение данных, тем сложнее этот шаг. Но не отчаивайтесь, потому что
данный рецепт дизайна может помочь даже в самых сложных
ситуациях.
Если вы застопорились, то сначала заполните простые случаи,
а для остальных используйте значения по умолчанию. Некото-

191

192

Глава 6

рые тестовые примеры могут показывать неверный результат,
тем не менее это будет вполне видимый шаг вперед.
Если вы застопорились на некоторых случаях детализации,
проанализируйте примеры, соответствующие им. Определите, что вычисляют части макета, соответствующие заданным
входным данным. Затем подумайте, как объединить эти части
(и некоторые константы) для получения желаемого результата.
Имейте в виду, что вам может понадобиться вспомогательная
функция.
Кроме того, если ваш макет «вызывает» другой макет из-за
ссылок друг на друга в определениях данных, то исходите из
предположения, что та другая функция делает именно то, что
обещают ее описание назначения и примеры, даже если определение той другой функции еще не завершено;
6) тестирование. Если тесты не проходят, определите, где находится причина: в функциях, в тестах или там и там. Вернитесь
к соответствующему шагу.
Вернитесь к разделу 3.1, прочитайте рецепт простого проектирования и сравните его с этой версией.
А теперь проиллюстрируем использование этого рецепта на примере проектирования функции отображения для постановки задачи в начале данного раздела. Напомним, что для выражения bigbang нужна функция отображения, преобразующая состояние мира
в изобра­жение после каждого такта часов, щелчка мыши или нажатия
клавиши.
Сигнатура этой функции ясно говорит о том, что она отображает
экземпляр класса состояния мира в экземпляр класса изображений:
; SIGS -> Изображение
; добавляет TANK, UFO и, возможно, MISSILE в
; сцену BACKGROUND
(define (si-render s) BACKGROUND)

Здесь TANK, UFO, MISSILE и BACKGROUND – это имена констант, определяющие изображения, которые вы должны были создать в упражнении 94. Напомним, что эта сигнатура лишь следует обобщенной сигнатуре для функций отображения, которые всегда принимают коллекции состояний мира и создают какое-то изображение.
Детализация в определении данных включает два элемента, но мы
создадим три примера, используя примеры данных, приведенные
выше (см. табл. 4). В отличие от табличных функций, которые можно
найти в книгах по математике, эта таблица отображается вертикально. Левый столбец содержит примеры входных данных, а в правом
столбце представлены желаемые результаты. Как видите, мы использовали примеры данных из первого шага рецепта проектирования,
охватывающие оба элемента детализации.

193

Структуры и детализация

Таблица 4. Примеры отрисовки состояний игры
s
(make-aim
(make-posn 10 20)
(make-tank 28 -3))

(si-render s)

(make-fired
(make-posn 20 100)
(make-tank 100 3)
(make-posn 22 103))

(make-fired
(make-posn 10 20)
(make-tank 28 -3)
(make-posn 32 (- HEIGHT TANK-HEIGHT 10)))

Теперь перейдем к разработке макета – самому важному этапу процесса проектирования. Во-первых, мы знаем, что тело si-render должно содержать выражение cond с двумя условиями. Согласно рецепту
проектирования, этими условиями должны быть (aim? s) и (fired? s),
они различают два возможных вида данных, которые может получить si-render:
(define (si-render s)
(cond
[(aim? s) ...]
[(fired? s) ...]))

Во-вторых, добавим селекторы в каждое условие cond, обрабаты­
вающее структуры. В данном случае оба условия обрабатывают структуры: aim и fired. Первая имеет два поля и, следовательно, требует добавить в первое условие cond два селектора, а вторая имеет три поля
и, соответственно, требует добавить три селектора:
(define (si-render s)
(cond
[(aim? s) (... (aim-tank s) ... (aim-ufo s) ...)]
[(fired? s) (... (fired-tank s) ... (fired-ufo s)
... (fired-missile s) ...)]))

Макет содержит почти все, что нужно для завершения поставленной задачи. Чтобы завершить определение, нужно выяснить для
каждого условия в cond, как объединить имеющиеся значения, дабы
получить желаемый результат. Кроме входных данных, мы можем
использовать глобальные константы, например BACKGROUND, что явно
не будет лишним; элементарные или встроенные операции; и, если

194

Глава 6

потребуется, функции из списка желаний, то есть мы дополнительно
должны описать функции, которые хотели бы иметь.
Рассмотрим первое условие cond. Здесь у нас есть представление
данных о танке (aim-tank s) и представление данных об НЛО (aimufo s). Из первого примера в табл. 4 мы знаем, что функция должна
добавить в сцену два объекта. Кроме того, рецепт проектирования
предполагает, что если эти элементы данных имеют собственные
определения данных, то необходимо рассмотреть возможность определения дополнительных (вспомогательных) функций и использовать их для вычисления результата:
... (tank-render (aim-tank s)
(ufo-render (aim-ufo s) BACKGROUND))

Здесь tank-render и ufo-render – это функции, добавленные в список
желаний:
; Tank Изображение -> Изображение
; добавляет t в заданное изображение im
(define (tank-render t im) im)
; UFO Изображение -> Изображение
; добавляет u в заданное изображение im
(define (ufo-render u im) im)

Точно так же можно поступить со вторым условием в выражении
cond. В листинге 19 показано законченное определение функции
отобра­жения. Самое замечательное, что мы можем сразу же использовать наши функции из списка желаний, tank-render и ufo-render,
и нам остается только добавить функцию, отображающую ракету
в сцене. Вот соответствующая запись в списке желаний:
; Missile Изображение -> Изображение
; добавляет m в заданное изображение im
(define (missile-render m im) im)

Как и прежде, комментарии достаточно подробно описывают желаемое поведение функции.
Листинг 19. Законченное определение функции отображения
; SIGS -> Изображение
; отображает состояние игры поверх BACKGROUND
; примеры см. в табл. 4
(define (si-render s)
(cond
[(aim? s)
(tank-render (aim-tank s)
(ufo-render (aim-ufo s) BACKGROUND))]
[(fired? s)
(tank-render
(fired-tank s)
(ufo-render (fired-ufo s)
(missile-render (fired-missile s)
BACKGROUND)))]))

Структуры и детализация

Упражнение 97. Спроектируйте функции tank-render, ufo-render
и missile-render. Сравните первое выражение:
(tank-render
(fired-tank s)
(ufo-render (fired-ufo s)
(missile-render (fired-missile s)
BACKGROUND)))

со вторым:
(ufo-render
(fired-ufo s)
(tank-render (fired-tank s)
(missile-render (fired-missile s)
BACKGROUND)))

В каких случаях эти два выражения дают одинаковый результат? 
Упражнение 98. Спроектируйте функцию si-game-over? для использования в качестве обработчика stop-when. Игра должна останавливаться, когда НЛО достигает поверхности земли или ракета попадает в НЛО. В обоих случаях мы рекомендуем проверить близость
одного объекта к другому.
Предложение stop-when допускает второе необязательное подвыражение – функцию, которая отображает конечное состояние игры.
Спроектируйте функцию si-render-final и используйте ее как второе
подвыражение в предложении stop-when в функции main в упражнении 100. 
Упражнение 99. Спроектируйте функцию si-move. Она будет вызываться после каждого такта часов, чтобы определить новые позиции
игровых объектов. Соответственно, она должна принимать один экземпляр SIGS и порождать другой.
Вычислить новую позицию танка и ракеты (если она была запущена) относительно несложно. Они движутся по прямой с постоянной скоростью. Но НЛО при перемещении может совершать случайные прыжки влево или вправо. Поскольку мы выше не рассказывали
о функциях, возвращающих случайные числа, в оставшейся части
данного упражнения мы покажем, как решить эту проблему.
В BSL имеется функция, которая возвращает случайные числа. Введение этой функции наглядно показывает, почему сигнатуры и описания назначения играют такую важную роль в проектировании. Вот
соответствующие сведения о нужной вам функции:
; Число -> Число
; порождает число в интервале [0,n),
; число, полученное при каждом последующем вызове, может отличаться от предыдущего
(define (random n) ...)

Сигнатура и описание назначения точно описывают, что вычисляет
функция, поэтому теперь вы сможете поэкспериментировать с функцией random в области взаимодействий DrRa­cket. Стоп! Сделайте это!

195

196

Глава 6

Идея об использовании
random определяется
знанием языка BSL
и не является одним
из навыков проектирования, которые вы
должны приобрести,
поэтому мы включили
эту подсказку. Кроме
того, random – это
первая и единственная
элементарная функция
языка BSL, которая не
является математической функцией. Функции
в программировании
имеют множество
сходств с математическими функциями,
но это не идентичные
понятия.

Так как при каждом (почти) применении random возвращает разные числа, тестирование функций, использующих
random, может вызывать определенные трудности. Для начала разделите si-move на две части:
(define (si-move w)
(si-move-proper w (random ...)))
; SIGS Число -> SIGS
; перемещает НЛО на предсказуемое расстояние delta
(define (si-move-proper w delta)
w)

Такое определение позволяет отделить получение случайного числа от процесса перемещения игровых объектов.
Даже притом что random может возвращать разные результаты при каждом новом применении, si-move-proper можно
протестировать с предопределенными числовыми входными данными и получить одинаковый результат при одинаковых входных данных. Проще говоря, при таком подходе большая
часть кода останется доступной для тестирования.
Вместо прямого вызова random можно спроектировать функцию,
которая создает случайную координату x для НЛО. Для тестирования
такой функции можно использовать функцию check-random из фреймворка тестирования в языке BSL. 
Упражнение 100. Спроектируйте функцию si-control, которая будет играть роль обработчика событий клавиатуры. Она должна принимать состояние игры и событие клавиатуры и создавать новое состояние игры. Функция должна реагировать на три события:
zz нажатие клавиши со стрелкой влево должно вызывать переме-

щение танка влево;

zz нажатие клавиши со стрелкой вправо должно вызывать пере-

мещение танка вправо;

zz нажатие клавиши пробела должно запускать ракету, если она

еще не была запущена.

После создания этой функции вы сможете определить функцию simain, которая использует выражение big-bang для создания окна игры.
Желаем приятно провести время! 
Листинг 20. Отображение состояния игры, снова
; SIGS.v2 -> Изображение
; отрисовывает состояние игры поверх BACKGROUND
(define (si-render.v2 s)
(tank-render
(sigs-tank s)
(ufo-render (sigs-ufo s)
(missile-render.v2 (sigs-missile s)
BACKGROUND))))

Структуры и детализация

Представления данных редко бывают уникальными. Например, мы
могли бы использовать одну структуру для представления всех возможных состояний игры:
(define-struct sigs [ufo tank missile])
; SIGS.v2 (т. е. SIGS версии 2) -- это структура:
; (make-sigs UFO Tank MissileOrNot)
; интерпретация: представляет полное состояние
; с космическими захватчиками
;
;
;
;
;

MissileOrNot -- одно из значений:
-- #false
-- Posn
интерпретация: #false означает, что ракета находится в танке;
Posn определяет местоположение ракеты после пуска

В отличие от первого представления данных, описывающего со­
стоя­ние игры, эта вторая версия не делает различий между состояниями до и после пуска ракеты. Вместо этого каждое состояние содержит некоторые данные о ракете, правда, этот элемент данных может
быть простым значением #false, указывающим, что пуск ракеты еще
не был произведен.
В результате функции, обрабатывающие это второе представление состояния, отличаются от функций для первого представления.
В частности, функции, использующие элемент SIGS.v2, не используют выражение cond, потому что в коллекции имеется только один тип
элементов. С точки зрения подхода к проектированию достаточно
использовать рецепт проектирования со структурами из раздела 5.8.
В листинге 20 показан результат проектирования функции отображения для этого представления данных.
Напротив, для проектирования функций с использованием представления MissileOrNot требуется рецепт из этого раздела. Давайте рассмотрим последовательность проектирования функции missile-render.v2, задача которой состоит в том, чтобы добавить ракету
в изображение. Вот определение заголовка:
; MissileOrNot Изображение -> Изображение
; добавляет изображение ракеты m в сцену s
(define (missile-render.v2 m s)
s)

При создании примеров мы должны учесть как минимум два случая: первый, когда m имеет значение #false, и второй, когда m является представлением Posn. В первом случае ракета не была запущена,
а значит, ее изображение не нужно добавлять в сцену. Во втором случае мы имеем координаты ракеты, в которых должно появиться изображение ракеты. Таблица 5 демонстрирует работу функции в этих
двух сценариях.
Упражнение 101. Преобразуйте примеры в табл. 5 в тесты. 
Теперь мы готовы приступить к определению макета. Поскольку
определение данных для главного аргумента (m) является детализа-

197

198

Глава 6

цией с двумя элементами, то тело функции, вероятно, будет состоять
из выражения cond с двумя условиями:
(define (missile-render.v2 m s)
(cond
[(boolean? m) ...]
[(posn? m) ...]))

Таблица 5. Примеры отображения состояний игры с танком и ракетой
m

(missile-render.v2 m s)

#false
(make-posn
32
(- HEIGHT
TANK-HEIGHT
10))

Согласно определению данных, первое условие в выражении cond
проверяет, является ли m логическим значением, а второе проверяет,
является ли оно экземпляром Posn. Если кто-то по ошибке применит
missile-render.v2 к значению #true и к какому-нибудь изображению,
то функция выполнит первое предложение в выражении cond. Далее
мы подробнее расскажем о таких ошибках.
Второй шаг в процессе создания макета требует использовать селекторы во всех условиях в выражении cond, которые имеют дело со
структурами. В нашем примере это относится ко второму условию,
поэтому добавим применение селекторов для извлечения координат x и y из заданного экземпляра Posn:
(define (missile-render.v2 m s)
(cond
[(boolean? m) ...]
[(posn? m) (... (posn-x m) ... (posn-y m) ...)]))

Сравните этот макет с макетом функции si-render выше. Определение данных для si-render включает два разных типа структур, поэтому макет данной функции содержит селекторы в обоих условиях
в выражении cond. Определение данных MissileOrNot смешивает элементарные значения со структурой, поэтому селекторы потребовались только во втором условии. Оба способа определения данных допустимы, и ваша главная задача – следуя рецепту, найти организацию
кода, которая соответствует определению данных.
Вот полное определение функции:
(define (missile-render.v2 m s)
(cond

Структуры и детализация
[(boolean? m) s]
[(posn? m)
(place-image MISSILE (posn-x m) (posn-y m) s)]))

Действуя шаг за шагом, сначала определяются простые условия;
в данной функции простым является первое условие. Поскольку оно
идентифицирует ситуацию, когда ракета не была запущена, то должно просто вернуть заданное значение s. Работая над вторым условием, нужно помнить, что (posn-x m) и (posn-y m) выбирают координаты
для изображения ракеты. Данная функция должна добавить изображение MISSILE в сцену s, поэтому вам нужно выяснить лучшую комбинацию элементарных операций и ваших собственных функций для
объединения этих четырех значений. Выбор операции комбинирования – это именно то место, где в игру вступает ваше творческое чутье
программиста.
Упражнение 102. Спроектируйте все остальные функции, необходимые для завершения игры с этим вторым определением данных. 
Упражнение 103. Сконструируйте представление данных для следующих четырех видов животных:
zz пауки, атрибутами которых являются: количество оставшихся

ног (мы предполагаем, что пауки могут терять ноги в результате несчастных случаев) и необходимый объем контейнера для
их транспортировки;
zz слоны, единственный атрибут которых – необходимый объем
контейнера для транспортировки;
zz удавы, атрибутами которых являются длина и обхват;
zz броненосцы, для которых вы сами должны определить соответствующие атрибуты, в том числе необходимый объем контейнера для транспортировки.
Напишите макеты функций, которые принимают аргументы, представляющие этих животных.
Спроектируйте функцию fits?, которая принимает экземпляр животного и описание контейнера. Она должна определить, достаточно
ли объема указанного контейнера для транспортировки заданного
животного. 
Упражнение 104. В любом городе есть свой парк транспортных
средств: автомобилей, фургонов, автобусов и внедорожников. Разработайте представление данных для автомобилей. Представление
каждого транспортного средства должно описывать максимальное
количество пассажиров, номерной знак и расход топлива (литров на
100 км). Напишите макеты функций, которые принимают автомобили. 
Упражнение 105. Некоторая программа содержит следующее
определение данных:
; Координата -- это одно из значений:
; -- NegativeNumber

199

200

Глава 6
;
;
;
;
;

интерпретация: для оси y, расстояние от верхнего края
-- PositiveNumber
интерпретация: для оси x, расстояние от левого края
-- Posn
интерпретация: обычные декартовы координаты точки

Составьте не менее, чем по два примера данных для каждого предложения в этом определении данных. Для каждого из примеров объясните его значение с по­мощью наброска на холсте. 

6.2. Смешивание миров
В этом разделе мы исследуем несколько проблем проектирования
мировых программ и начнем с простых упражнений, касающихся наших виртуальных питомцев.
Упражнение 106. В разделе 5.11 обсуждалось создание виртуальных питомцев с индикаторами довольства. Один из этих питомцев –
кошка; другой – хамелеон. Однако каждая из обсуждавшихся программ управляет только одним питомцем.
Спроектируйте мировую программу cat-cham. Она должна управлять заданным животным, кошкой или хамелеоном, перемещая его
по холсту, начиная с заданного местоположения. Вот описание представления данных для животных:
; VAnimal -- одно из значений
; -- a VCat
; -- a VCham

где VCat и VCham – определения данных, которые вы должны были
сконструировать в упражнениях 88 и 92.
Учитывая, что VAnimal является коллекцией состояний мира, вы
должны спроектировать:
zz функцию преобразования VAnimal в изображение;
zz функцию для обработки тактов часов и преобразующую состоя­

ние VAnimal в новое состояние VAnimal;

zz функцию обработки событий клавиатуры, чтобы дать возмож-

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

Изменить окрас кошки или погладить хамелеона по-прежнему невозможно. 
Упражнение 107. Спроектируйте программу cham-and-cat, которая
управляет сразу двумя животными: виртуальной кошкой и виртуальным хамелеоном. Вам потребуется определение данных «зоопарка»,
содержащее обоих животных и функции для работы с ними.
Постановка задачи не уточняет, какие клавиши использовать для
управления двумя животными. Вот два возможных толкования:

Структуры и детализация

1) каждое событие клавиатуры затрагивает оба животных;
2) каждое событие клавиатуры затрагивает только одно из животных.
В этом случае вам понадобится добавить в представление данных некоторый признак, определяющий животное в фокусе, то
есть животное, находящееся под управлением. Для переключения фокуса функция обработки событий клавиатуры может
интерпретировать нажатие клавиши «k» как команду «kitty»
(кошечка), а нажатие клавиши «l» как команду «lizard» (ящерица). После того как игрок нажмет клавишу «k», все следующие
нажатия клавиш будут применяться только к кошке, пока игрок
не нажмет клавишу «l».
Выберите один из вариантов и спроектируйте подходящую программу. 
Упражнение 108. По умолчанию светофор для пешеходного перехода показывает оранжевую фигурку стоящего человека на черном
фоне. Когда наступает момент разрешить пешеходам перейти улицу,
светофор получает сигнал и показывает зеленую фигурку идущего
человека. Этот период длится 10 секунд. Затем на табло светофора
появляются цифры 9, 8, ..., 0, причем нечетные числа имеют оранжевый цвет, а четные – зеленый. Когда обратный отсчет достигает 0,
светофор возвращается в состояние по умолчанию.
Спроектируйте мировую программу, реализующую такой пешеходный светофор. Светофор должен переключаться из состояния по
умолчанию по нажатии клавиши пробела на клавиатуре. Все остальные изменения состояния светофора должны происходить автоматически, с течением времени. Вы можете использовать следующие
изображения:

или нарисовать свои фигурки. 
Упражнение 109. Спроектируйте мировую программу, которая
распознает шаблон в последовательности событий клавиатуры. Первоначально программа показывает белый прямоугольник 100×100.
Встретив первую букву из заданного шаблона, она должна показать
желтый прямоугольник того же размера. После встречи с последней
буквой программа должна показать зеленый прямоугольник. Если
нажимается «неверная» клавиша, не соответствующая шаблону, программа должна показать красный прямоугольник.
В частности, программа должна распознавать любые последовательности, начинающиеся с символа «a», за которым следует произвольное сочетание символов «b» и «c» любой длины, и всякая по-

201

202

Глава 6

следовательность должна заканчиваться буквой «d». Очевидно, что
«acbd» – это одна из допустимых последовательностей; последовательности «ad» и «abcbbbcd» тоже допустимы. Но «da», «aa» или «d» –
это недопустимые последовательности.
Подсказка. Используйте в своем решении конечный автомат, идея
которого была представлена в разделе 4.7 как один из принципов
проектирования, лежащий в основе мировых программ. Как следует из названия, программа, реализующая конечный автомат, может
находиться в одном из конечного числа состояний. Первое состояние
называется начальным состоянием. Каждое событие клавиатуры заставляет автомат пересматривать свое текущее состояние; он может
остаться в том же состоянии или перейти в другое. Когда программа
распознает правильную последовательность событий, она переходит
в конечное состояние.
Таблица 6. Два способа определения данных для конечного автомата
;
;
;
;
;

Общепринятый

Сокращенный

ExpectsToSee.v1 -- одно из значений:
-- "начало, ожидается 'a'"
-- "ожидается 'b', 'c' или 'd'"
-- "конец"
-- "ошибка, неверная клавиша"

; ExpectsToSee.v2 -- одно из значений:
; -- AA
; -- BB
; -- DD
; -- ER
(define AA "начало, ...")
(define BB "ожидается ...")
(define DD "конец")
(define ER "ошибка, ...")

Для задачи распознавания последовательностей состояния
обычно обозначаются буквами, которые конечный автомат
ожидает увидеть дальше (см. табл. 6). Взгляните на последнее состояние, в котором говорится, что обнаружено нажатие
неверной клавиши. На рис. 10 показано, как представить эти
состояния, и связи между ними в виде диаграммы. Каждый узел соответствует одному из четырех состояний; каждая стрелка указывает, какое событие клавиатуры вызывает переход из одного состояния
в другое.
Историческая справка. В 1950-х годах Стивен К. Клини (Stephen
C. Kleene), которого мы назвали бы специалистом по информатике,
изобрел регулярные выражения для записи задач распознавания текстовых шаблонов. Нашу задачу Клини выразил бы таким регулярным
выражением:

В определении данных
справа используется
прием именования,
представленный
в упражнении 61.

a (b|c)* d

которое означает: «символ a, за которым могут следовать символы b
и c в любых комбинациях, пока не встретится символ d». 

203

Структуры и детализация

6.3. Ошибки ввода
Одной из центральных тем, обсуждаемых в этой главе,
является роль предикатов. Предикаты имеют решающее
значение при проектировании функций, обрабатывающих смешанные данные. Смешение данных возникает естественным образом, когда в формулировке задачи упоминается много разных видов информации, но
оно также возникает, когда вы передаете свои функции
и программы другим. В конце концов, вы знаете и учитываете особенности своих определений данных и сигнатур функций. Однако вы можете не знать, чем занимаются ваши друзья и коллеги и как кто-то, незнакомый
с языком BSL и программированием, использует ваши
программы. Поэтому в этом разделе мы покажем один из
способов защиты программ от недопустимого ввода.

Это самообман – ожидать,
что мы всегда будем строго
следовать определениям
сигнатур своих функций.
Вызов функции с недопустимыми данными случается
даже с самыми лучшими из
нас. Многие языки программирования похожи на BSL
и ожидают, что программисты будут внимательно
читать сигнатуры, но есть
и такие, которые сами
проверяют соответствие
вызовов сигнатурам, пусть
и за счет некоторой дополнительной сложности.

"b", "c"
AA

"a"

не "a"

BB

"d"

DD

не "b", "c" или "d"
ER

Рис. 10. Диаграмма конечного автомата

Давайте продемонстрируем этот момент с по­мощью простой программы. Вот функция для вычисления площади круга:
; Число -> Число
; вычисляет площадь круга с радиусом r
(define (area-of-disk r)
(* 3.14 (* r r)))

Наши друзья могут решить использовать эту функцию для решения домашних заданий по геометрии. К сожалению, они могут случайно применить эту функцию к строке, а не к числу. В случае такой
ошибки функция остановит выполнение программы с загадочным
сообщением:
> (area-of-disk "my-disk")
*:expects a number as 1st argument, given "my-disk"

(*:ожидалось число в 1-м аргументе, а получено «my-disk»).

204

Глава 6

С помощью предикатов можно предотвратить появление подобных
загадочных сообщений и вывести более понятное описание ошибки.
Такие версии проверяющих функций можно создавать для передачи
нашим друзьям. Друзья могут не знать языка BSL, поэтому мы должны
быть готовыми к тому, что они попробуют применить проверяющую
функцию к произвольным значениям: числам, строкам, изображениям, структурам posn и т. д. Мы не можем предугадать, какие структуры
будут определены в BSL, но мы знаем приблизительную форму определения данных для коллекции всех значений в языке BSL. Эта форма
определения данных показана в листинге 21. Как обсуждалось в разделе 5.7, определение данных для Any (любое значение) является открытым, потому что каждое определение структуры добавляет новые
экземпляры. Эти экземпляры сами могут содержать значения Any,
а это подразумевает, что определение данных Any должно ссылаться
на самого себя – первое время эта мысль может пугать.
Листинг 21. Вселенная данных в языке BSL
;
;
;
;
;
;
;
;
;

Any -- одно из значений в языке BSL:
-- Число
-- Логическое значение
-- Строка
-- Изображение
-- (make-posn Any Any)
...
-- (make-tank Any Any)
...

С учетом этой детализации макет проверяющей функции имеет
примерно следующую форму:
; Any -> ???
(define (checked-f v)
(cond
[(number? v) ...]
[(boolean? v) ...]
[(string? v) ...]
[(image? v) ...]
[(posn? v) (...(posn-x v) ... (posn-y v) ...)]
...
; какой селектор понадобится в следующем условии?
[(tank? v) ...]
...))

Конечно, невозможно перечислить все варианты из этого определения, но, к счастью, в этом нет необходимости. Мы знаем, что для
всех значений, допустимых для исходной функции, ее проверяющая
версия должна давать точно такие же результаты, а при получении
любых других значений – сообщать об ошибке.
В частности, наша функция checked-area-of-disk принимает произвольное значение BSL и использует area-of-disk для вычисления площади круга, если аргумент является числом. В противном случае она
должна остановить выполнение с сообщением об ошибке; в языке

Структуры и детализация

BSL для этого используется функция error, которая принимает строку
и останавливает программу:
(error "area-of-disk: number expected")

Вот как могло бы выглядеть определение функции checked-area-ofdisk:
(define MESSAGE "area-of-disk: number expected")
(define (checked-area-of-disk v)
(cond
[(number? v) (area-of-disk v)]
[(boolean? v) (error MESSAGE)]
[(string? v) (error MESSAGE)]
[(image? v) (error MESSAGE)]
[(posn? v) (error MESSAGE)]
...
[(tank? v) (error MESSAGE)]
...))

Использование предложения else поможет завершить это определение естественным образом:
; Any -> Число
; вычисляет площадь круга с радиусом v,
; если v является числом
(define (checked-area-of-disk v)
(cond
[(number? v) (area-of-disk v)]
[else (error "area-of-disk: number expected")]))

Давайте поэкспериментируем, чтобы убедиться, что у нас получилось то, что мы хотели:
> (checked-area-of-disk "my-disk")
area-of-disk:number expected

Создавать проверяющие функции важно, если вы собираетесь передавать свои программы другим. Однако гораздо важнее уметь проектировать программы, которые работают правильно. В этой книге
основное внимание уделяется процессу проектирования правильно
работающих программ, и, чтобы не отвлекаться, мы будем считать,
что всегда строго следуем определениям данных и сигнатур. Мы, по
крайней мере, поступаем так почти всегда, но в редких случаях можем попросить вас спроектировать проверяющие версии функций
или программ.
Упражнение 110. Проверяющая версия функции area-of-disk может также проверить, является ли аргумент функции положительным
числом. Внесите соответствующие изменения в check-area-of-disk. 
Упражнение 111. Взгляните на следующее определение:
(define-struct vec [x y])
; vec -- это
; (make-vec ПоложительноеЧисло ПоложительноеЧисло)
; интерпретация: представляет вектор скорости

205

206

Глава 6

Напишите функцию checked-make-vec – проверяющую версию элементарной операции make-vec. Она должна гарантировать возможность создания вектора, только если ее аргументы будут положительными числами. Другими словами, checked-make-vec должна обеспечить
соблюдение неформального определения данных. 
Предикаты. У многих из вас наверняка давно возник вопрос: как
создавать свои предикаты? В конце концов, если представить в общем виде, проверяющие функции имеют следующую форму:
; Any -> ...
; проверяет допустимость входного аргумента для функции g
(define (checked-g a)
(cond
[(XYZ? a) (g a)]
[else (error "g: bad input")]))

где функция g определяется так:
; XYZ -> ...
(define (g some-x) ...)

Мы предполагаем, что существует определение данных с именем
XYZ и выражение (XYZ? A) должно возвращать #true, если a является
экземпляром XYZ, и #false в противном случае.
Для функции area-of-disk, которая принимает число, вполне подходит предикат number?. Но для некоторых функций, таких как missile-render (см. выше), желательно определить свой предикат, потому
что MissileOrNot – это придуманная нами, а не встроенная коллекция
данных. Итак, давайте определим предикат для MissileOrNot.
Вспомним, как выглядит сигнатура предикатов:
; Any -> Boolean
; это элемент коллекции MissileOrNot?
(define (missile-or-not? a) #false)

Хорошей практикой считается формулировка описания назначения предиката в форме вопроса, потому что применение предиката
напоминает вопрос о значении. Знак вопроса «?» в конце имени предиката еще больше усиливает эту идею; некоторые могут добавлять
слово «да?» при произнесении имен таких функций.
Придумать примеры тоже несложно:
(check-expect (missile-or-not? #false) #true)
(check-expect (missile-or-not? (make-posn 9 2)) #true)
(check-expect (missile-or-not? "yellow") #false)

Первые два примера напоминают, что экземпляр MissileOrNot может быть либо значением #false, либо экземпляром Posn. Третий пример утверждает, что строка не является допустимым экземпляром
этой коллекции. Вот еще три теста:
(check-expect (missile-or-not? #true) #false)
(check-expect (missile-or-not? 10) #false)
(check-expect (missile-or-not? empty-image) #false)

Структуры и детализация

Объясните ожидаемые ответы!
Поскольку предикаты принимают любые значения BSL, их макеты
подобны макетам проверяющих функций checked-f. Стоп! Найдите этот
макет и еще раз внимательно рассмотрите, прежде чем читать дальше.
По аналогии с проверяющими функциями, в предикате не требуется определять все возможные условия. Достаточно добавить только
те, которые могут вернуть #true:
(define (missile-or-not? v)
(cond
[(boolean? v) ...]
[(posn? v) (... (posn-x v) ... (posn-y v) ...)]
[else #false]))

Все остальные случаи обобщаются условием else, которое возвращает #false.
Согласно этому макету, в определении missile-or-not? достаточно
перечислить все допустимые случаи:
(define (missile-or-not? v)
(cond
[(boolean? v) (boolean=? #false v)]
[(posn? v) #true]
[else #false]))

Коллекция MissileOrNot включает только значение #false; #true не
входит в нее. Мы выразили эту идею в форме выражения (boolean=?
#false v), но также можно было бы использовать выражение (false? v):
(define (missile-or-not? v)
(cond
[(false? v) #true]
[(posn? v) #true]
[else #false]))

Естественно, все экземпляры Posn тоже являются членами коллекции MissileOrNot, что объясняет #true во втором условии.
Упражнение 112. Переформулируйте предикат, используя выражение or. 
Упражнение 113. Спроектируйте предикаты для следующих определений данных из предыдущего раздела: SIGS, Координата (упражнение 105) и VAnimal. 
В заключение упомянем также два важных предиката, key-event?
и mouse-event?, которые наверняка пригодятся вам при создании мировых программ. Об их назначении вполне можно догадаться по
именам, но вы все равно загляните в документацию с их описанием,
чтобы убедиться, что понимаете, что именно они проверяют.

6.4. Проверка состояния мира
В мировой программе многое может пойти не так. Мы просто согласились с тем, что наши функции всегда применяются к надлежащему типу

207

208

Глава 6

данных, но мировая программа может манипулировать слишком многими разными данными одновременно, поэтому у нас не может быть
абсолютного доверия самим себе. Проектируя мировую программу,
которая обрабатывает такты часов, щелчки мышью, нажатия клавиш
и формирует изображение, очень легко ошибиться в одном из этих взаи­
модействий. При этом язык BSL может не распознать ошибку немедленно. Например, одна из наших функций может вернуть результат, не
являющийся экземпляром нашего представления данных для состоя­
ния мира. В такой ситуации big-bang примет этот экземпляр данных
и сохранит его до следующего события. И только когда следующий обработчик событий получит эти несоответствующие данные, программа
может завершиться с ошибкой. Хуже того, даже второй, третий и четвертый этапы обработки событий могут пропустить дальше эти несоответствующие данные, но затем, намного позже, произойдет взрыв.
Чтобы справиться с подобными проблемами, big-bang поддерживает дополнительное предложение check-with, которое принимает предикат для проверки состояния мира. Если, например, состояние мира
представлено числом, мы с легкостью могли бы отразить этот факт
следующим образом:
(define (main s0)
(big-bang s0 ... [check-with number?] ...))

Как только какая-либо функция обработки событий вернет какое-­
то значение, не являющееся числом, мир остановится с соответствую­
щим сообщением об ошибке.
Предложение check-with особенно полезно, когда определение данных является не простым классом со встроенным предикатом, таким
как number?, а чем-то другим, как, например, следующее определение
интервала:
; UnitWorld -- это число
; между 0 (включительно) и 1 (не включая его).

В этом случае необходимо сформулировать предикат для интервала:
; Any -> Логическое значение
; является ли x числом между 0 (включительно) и 1 (не включая его)?
(check-expect
(check-expect
(check-expect
(check-expect
(check-expect

(between-0-and-1?
(between-0-and-1?
(between-0-and-1?
(between-0-and-1?
(between-0-and-1?

"a")
1.2)
0.2)
0.0)
1.0)

#false)
#false)
#true)
#true)
#false)

(define (between-0-and-1? x)
(and (number? x) ( Логическое значение
; сравнивает два (состояния) светофора
(check-expect
(check-expect
(check-expect
(check-expect

(light=?
(light=?
(light=?
(light=?

"red" "red") #true)
"red" "green") #false)
"green" "green") #true)
"yellow" "yellow") #true)

(define (light=? a-value another-value)
(string=? a-value another-value))

После щелчка на кнопке RUN (Выполнить) все тесты выполнятся
успешно, но, к сожалению, другие взаимодействия обнаружат противоречие в наших намерениях:
> (light=? "salad" "greens")
#false
> (light=? "beans" 10)
string=?:expects a string as 2nd argument, given 10

(string=?:ожидается строка во 2-м аргументе, а получено значение 10).
Сравните эти взаимодействия с применением других встроенных
предикатов равенства:
> (boolean=? "#true" 10)
boolean=?:expects a boolean as 1st argument, given "#true"

(boolean=?:ожидается логическое значение в 1-м аргументе, а получено значение «#true»).
Попробуйте выполнить выражения (string=? 10 #true)
и (= 20 "help"). Все они сообщат об ошибке применения к не- Регистр символов
имеет значение; строка
верному аргументу.
"red" считается отличПроверяющая версия light=? предварительно проверяет, ной от "Red" и "RED".
являются ли оба аргумента экземплярами коллекции Све-

210

Глава 6

тофор. Если нет, то она сообщает об ошибке, подобной встроенным
предикатам равенства. Вот определение предиката light?:
; Any -> Логическое значение
; является ли данный экземпляр
(define (light? x)
(cond
[(string? x) (or (string=?
(string=?
(string=?
[else #false]))

Светофором?

"red" x)
"green" x)
"yellow" x))]

Теперь можно завершить предикат light=?, просто следуя первоначальному анализу. Функция проверяет принадлежность аргументов
к коллекции значений Светофор, и если это условие нарушается, то
применяет функцию error, чтобы сообщить об ошибке:
(define MESSAGE
"traffic light expected, given some other value")
; Any Any -> Логическое значение
; являются ли аргументы значениями Светофор, и если да,
; то являются ли они одинаковыми?
(check-expect
(check-expect
(check-expect
(check-expect

(light=?
(light=?
(light=?
(light=?

"red" "red") #true)
"red" "green") #false)
"green" "green") #true)
"yellow" "yellow") #true)

(define (light=? a-value another-value)
(if (and (light? a-value) (light? another-value))
(string=? a-value another-value)
(error MESSAGE)))

Упражнение 115. Исправьте предикат light=? так, чтобы он сообщал, какой из двух аргументов не является экземпляром коллекции
Светофор. 
Трудно представить, что ваши программы будут использовать
light=?, скорее, они будут использовать key=? и mouse=?, два предиката равенства, упоминавшихся в конце предыдущего раздела. Естест­
венно, key=? – предикат, сравнивающий два события клавиатуры;
аналогично mouse=? сравнивает два события мыши. Оба типа событий
представлены строками, однако важно понимать, что не все строки
представляют события клавиатуры или мыши.
Мы рекомендуем использовать key=? в обработчиках событий клавиатуры и mouse=? в обработчиках событий мыши. Использование
key=? в обработчике событий клавиатуры гарантирует, что функция
действительно будет сравнивать строки, представляющие события
клавиатуры, а не какие-то другие. Как только, например, функция
случайно получит в аргументе строку "hello\n world", предикат key=?
тут же заметит ошибку и сообщит нам об этом.

7. Итоги
В этой первой части книги мы исследовали ряд простых, но важных
уроков. Вот краткое их изложение.
1. Хороший программист проектирует программы. Плохой программист движется вперед методом проб и ошибок, пока не будет создана видимость, что программа работает.
2. Рецепт проектирования имеет два измерения. С одной стороны,
он описывает процесс проектирования, то есть последовательность выполняемых шагов. С другой – объясняет, как выбранное
представление данных влияет на процесс проектирования.
3. Каждая хорошо спроектированная программа состоит из множест­
ва определений констант, структур, данных и функций. В пакетных программах одна функция является «главной», и обычно она
состоит из нескольких других функций, выполняющих вычисления. В интерактивных программах роль главной функции играет
функция big-bang; она определяет начальное состояние программы, функцию вывода изображения и до трех обработчиков событий: тактов часов, щелчков мышью и нажатий клавиш на клавиатуре. В программах обоих типов функции определяются «сверху
вниз», начиная с главной функции, за которой следуют функции,
вызываемые в главной функции, и т. д.
4. Подобно многим другим языкам программирования, язык для начинающих студентов (Beginning Student Language, BSL) имеет словарь и грамматику. Программисты должны уметь определять
значение каждого предложения на языке, чтобы предсказать, что
программа получит в результате вычислений. Следующее интермеццо подробно объясняет эту идею.
5. Языки программирования, включая BSL, поставляются с богатым
набором библиотек, чтобы программистам не приходилось постоянно изобретать велосипед. Программист должен привыкнуть
к таким библиотечным функциям, особенно к их сигнатурам
и описаниям назначений. Это упростит жизнь.
6. Программист должен знать «инструменты», которые может предложить выбранный язык программирования. Эти инструменты
либо являются частью языка, как, например, cond или max, либо
«импортируются» из библиотек. В этом смысле убедитесь, что
понимаете следующие термины: определение типа структуры,
определение функции, определение константы, экземпляр
структуры, определение данных, big-bang и функция обработки событий.

Интермеццо 1. Язык
для начинающих студентов
В части I этой книги BSL рассматривается как естественный язык.
В ней были представлены «основные слова» языка, показано, как составлять «предложения» из «слов» и как использовать знания алгебры
для «чтения» этих «предложений». Такое введение помогает понять
язык до некоторой степени, но чтобы освоить его по-настоящему, необходимо формальное изучение.
Во многих отношениях аналогии, приводившиеся в первой части,
верны. В языке программирования есть словарь и грамматика, хотя
программисты эти элементы языка обычно называют синтаксисом.
Предложение в языке BSL – это выражение или определение. Грамматика BSL определяет правила формирования этих фраз. Но не все
грамматически верные предложения на естественном языке или
языке программирования имеют смысл. Например, фраза на естест­
венном языке «кошка свернулась клубком» имеет определенный
смысл, но фраза «кирпич – это автомобиль» не имеет смысла, хотя
и является грамматически верной. Чтобы определить, имеет ли предложение смысл, необходимо знать значение языка; программисты называют это семантикой.
В этом интермеццо мы рассмотрим язык BSL
Программисты, конечно,
как
расширение знакомого нам языка арифметидолжны знать принципы
вычислений, но они ки и алгебры. В конце концов, вычисления начилишь дополняют наются с этой формы простой математики, и мы
принципы должны понимать связь между законами матемапроектирования.
тики и вычислениями. Первые три раздела описывают синтаксис и семантику значительной части BSL. А в четвертой,
опираясь на эти новые знания BSL, возобновляется обсуждение ошибок. Остальные разделы заполняют оставшиеся пробелы, и в последнем обсуждаются инструменты, предназначенные для определения
тестов.

Словарь BSL
Во врезке ниже представлен базовый словарь языка BSL. Он состоит
из литеральных констант, таких как числа или логические значения;
имен, имеющих значение в языке BSL, например cond или +, и имен,
которым программы могут придавать значение с по­мощью define или
параметров функции.
Каждый пункт списка определяет множество посредством детализации его элементов. Можно, конечно, указать эти коллекции целиком, но мы считаем это излишним и доверяем вашей интуиции.
Просто имейте в виду, что каждое из этих множеств может включать
некоторые дополнительные элементы.

Язык для начинающих студентов

213

Базовый словарь BSL
Имя или переменная – это последовательность любых символов,
кроме следующих: " , ' ` ( ) [ ] { } | ; #:
zz примитив – это имя, изначально имеющее смысл в языке BSL,
например + или sqrt;
zz переменная – это имя, не имеющее предопределенного смысла.
Значение – это одно из следующих:
zz число – одно из: 1, -1, 3/5, 1.22, #i1.22, 0+1i и т. д. Синтаксис чисел в языке BSL довольно сложный, потому что охватывает
множество форматов представления чисел: положительные
и отрицательные числа, дроби натуральные и десятичные,
точные и приблизительные числовые значения, вещественные и комплексные числа, числа в системах счисления с основанием, отличным от 10, и многие другие. Понимание
форм записи чисел требует глубокого понимания грамматики и особенностей синтаксического анализа, обсуждение
которых выходит за рамки этого интермеццо;
zz логическое значение – всего два: #true и #false;
zz строка – одна из: "", "he says \"hello world\" to you", "doll" и т. д.
Как правило, строка – это последовательность символов, заключенная в пару двойных кавычек ";
zz изображение – это изображение в формате png, jpg, tiff и многих других форматах. Мы намеренно опускаем точное
определение изображения.
«Любое допустимое значение» мы обычно обозначаем как v,
v-1, v-2 и т. д.

Грамматика BSL
Во врезке «Базовая грамматика BSL» показана большая При чтении вслух
часть грамматики языка BSL, которая отличается удиви- грамматика звучит как
определение данных.
тельной простотой, по сравнению с другими языками. Что Грамматику можно
касается выразительной силы BSL, то пусть внешняя прос­ использовать для записи
тота не обманывает вас. Однако прежде всего мы должны многих определений
обсудить правила чтения грамматики. Каждая строка со данных.
знаком «равно» (=) представляет синтаксическую категорию,
сам знак = можно произнести как «один из», а знак вертикальной черты (|) – как «или». Многоточия обозначают любое количество повторений того, что предшествует многоточию. Например, определение
программа озна­чает, что программа не содержит ничего, или содержит
одно определение-выражения, или содержит последовательность из двух,
трех, четырех, пяти или более определений выражений. Поскольку

214

Интермеццо 1

этот пример не особо информативен, рассмотрим вторую синтаксическую категорию. Она утверждает, что определение – это либо
(define (переменная переменная) выражение)

потому что «любое количество повторений» подразумевает ноль повторений, либо
(define (переменная переменная переменная) выражение)

с одним повторением, либо
(define (переменная переменная переменная переменная) выражение)

с двумя повторениями.

Базовая грамматика BSL
программа = определение-или-выражение ...
определение-или-выражение = определение
| выражение
определение = (define (переменная переменная переменная ...) выражение)
выражение =
|
|
|
|
|

переменная
значение
(примитив выражение выражение ...)
(переменная выражение выражение ...)
(cond [выражение выражение] ... [выражение выражение])
(cond [выражение выражение] ... [else выражение])

И наконец, следует отметить три «слова», выделенных ненаклонным
шрифтом: define, cond и else. Согласно определению словаря BSL, эти три
слова являются именами. Но мы не упомянули, что эти имена имеют
предопределенное значение. В языке BSL эти слова служат маркерами, которые отделяют одни составные предложения от других, и в знак
признания их роли такие слова называются ключевыми словами.
Теперь мы готовы сформулировать цель грамматики. Грамматика
языка программирования диктует правила составления предложений на основе словаря. Некоторые предложения – это просто элементы словаря. Например, как описывается во врезке «Базовая грамматика BSL», 42 – это предложение на языке BSL:
Программа в DrRa­cket
в действительности
состоит из двух
отдельных частей:
области определений
и выражений в области
взаимодействий.

zz первая синтаксическая категория говорит, что про-

грамма – это определение-выражения. Выражения могут
ссылаться на определения;
zz вторая синтаксическая категория говорит, что определение-выражения является либо определением, либо выражением;

Язык для начинающих студентов

215

zz последнее определение перечисляет все способы формирова-

ния выражения, и второе из них – это значение.

Врезка «Базовая грамматика BSL» утверждает, что 42 – это значение, подтверждая последний пункт.
Определение грамматики показывает, как составлять сложные
предложения, построенные из других предложений. Например, определение сообщает, что определение функции начинается с символа «(»,
за которым следуют: ключевое слово define, еще один символ «(», последовательность, по крайней мере, из двух переменных, символ «)»,
выражение и, наконец, заключительная закрывающая круглая скобка
«)», которая соответствует самой первой открывающей круглой скобке. Обратите внимание, как ведущее ключевое слово define отделяет
определение от выражений.
Выражения бывают шести видов: переменные, константы, примитивы, функции и две разновидности условных выражений cond. Первые два – это атомарные предложения, последние четыре – составные. Так же как define, ключевое слово cond отделяет условные выражения от всего остального.
Вот три примера выражений: "all", x и (f x). Первый пример принадлежит к классу строк и, следовательно, является выражением.
Второй пример – это переменная, а всякая переменная – это выражение. Третий пример – это применение функции, потому что f и x – это
переменные.
Напротив, следующие предложения, заключенные в скобки, не являются допустимыми выражениями: (f define), (cond x) и ((f 2) 10).
Первое частично соответствует форме применения функции, но использует ключевое слово define, как если бы оно было переменной.
Второе предложение тоже не является допустимым выражением cond,
потому что содержит переменную во втором элементе, а не пару выражений, заключенных в круглые скобки. Последнее предложение не
является ни условным выражением, ни применением функции, потому что первая часть является выражением.
Наконец, можно заметить, что в грамматике не упоминаИмейте в виду, что
ются пробельные символы: пробелы, табуляции и переводы ваши программы на BSL
строки. BSL – достаточно вольный язык. Если между эле- будут изучаться двумя
ментами любой последовательности в программе имеются категориями читателей: людьми и средой
пробелы, DrRa­cket будет понимать ваши программы на BSL. программирования
Однако хорошим программистам может не понравиться DrRa­cket.
то, что вы пишете. Такие программисты используют пробелы, чтобы упростить чтение и понимание программ. Что особенно
важно, они предпочитают стиль, более удобный для людей, чем для
программ-трансляторов, обрабатывающих исходный код программ
(таких как DrRa­cket). Они осваивают этот стиль, читая примеры кода
в книгах и обращая внимание на их форматирование.

216

Интермеццо 1

Упражнение 116. Взгляните на следующие предложения:
1. x
2. (= y z)
3. (= (= y z) 0)
Объясните, почему они считаются синтаксически допустимыми
выражениями. 
Упражнение 117. Взгляните на следующие предложения:
1. (3 + 4)
2. number?
3. (x)
Объясните, почему они считаются синтаксически неверными выражениями. 
Упражнение 118. Взгляните на следующие предложения:
1. (define (f x) x)
2. (define (f x) y)
3. (define (f x y) 3)
Объясните, почему они считаются синтаксически допустимыми
определениями. 
Упражнение 119. Взгляните на следующие предложения:
1. (define (f "x") x)
2. (define (f x y z) (x))
Объясните, почему они считаются синтаксически неверными
определениями. 
Упражнение 120. Определите, какие из следующих предложений
являются допустимыми, а какие – нет:
1. (x)
2. (+ 1 (not x))
3. (+ 1 2 3)
Объясните, почему те или иные предложения являются допустимыми или неверными. Определите категорию – выражение или определение – допустимых предложений. 
ПРИМЕЧАНИЕ О ГРАММАТИЧЕСКОЙ ТЕРМИНОЛОГИИ. Компоненты составных предложений имеют имена. Мы ввели некоторые
из этих имен неофициально. В листинге 22 перечислены основные
условные обозначения.
Листинг 22. Синтаксические соглашения о терминологии
; применение функции:
(функция аргумент ... аргумент)
; определение функции:
(define (имя-функции параметр ... параметр)
тело-функции)
; условное выражение:
(cond

Язык для начинающих студентов
условное-предложение
...
условное-предложение)
; условное-предложение
[условие ответ]

В дополнение к терминам в листинге 22 мы также используем термин заголовок функции для обозначения второго компонента определения. Соответственно, компонент выражение называется телом функции. Люди, которые рассматривают языки программирования с точки
зрения математики, заголовок называют левой частью, а тело – правой частью. Иногда также можно услышать или увидеть термин фактические аргументы, обозначающий аргументы в выражении применения функции. КОНЕЦ.

Значение в языке BSL
Нажимая клавишу Enter на клавиатуре, вы фактически просите DrRa­
cket вычислить выражение. В процессе вычислений DrRa­cket использует законы арифметики и алгебры, чтобы получить значение. Во
врезке «Базовый словарь BSL» определяется, что значение (точнее
множество значений) – это просто подмножество всех выражений.
В множество входят логические значения, строки и изображения.
Правила вычислений делятся на две категории. Бесконечное количество правил, таких как правила арифметики, объясняет, как определить значение (результат) применения элементарной операции
к значениям:
(+ 1 1) == 2
(- 2 1) == 1
...

Как уже отмечалось, пара символов == говорит нам, что два выражения равны, согласно законам вычислений в BSL. Но арифметика
BSL является более универсальной, чем простые вычисления с числами. Она также включает правила вычислений с логическими значениями, строками и т. д.:
(not #true)
== #false
(string=? "a" "a") == #true
...

И так же как в алгебре, равенство всегда можно заменить тождест­
венными преобразованиями; как показано в листинге 23.
Листинг 23. Замена равенства тождественными преобразованиями
(boolean? (= (string-length (string-append "h" "w"))
(+ 1 3)))
==

217

218

Интермеццо 1
(boolean?
==
(boolean?
==
(boolean?
==
(boolean?
== #true

(= (string-length (string-append "h" "w")) 4))
(= (string-length "hw") 4))
(= 2 4))
#false)

Кроме того, важно знать правила из алгебры, чтобы понимать порядок применения функции к аргументам. Предположим, программа
содержит такое определение:
(define (f x-1 ... x-n)
f-body)

Тогда применение функции описывается правилом:
(f v-1 ... v-n) == f-body
; где все вхождения x-1 ... x-n
; заменяются v-1 ... v-n соответственно

Традиционно в таких языках, как BSL, мы называем это
правило правилом бета или бета-значения.
Это правило сформулировано максимально обобщенно,
поэтому лучше взглянуть на конкретный пример. Допустим,
у нас есть такое определение:

Более подробно это
правило рассматривается в разделе 17.2.

(define (poly x y)
(+ (expt 2 x) y))

и в области взаимодействий мы ввели выражение (poly 3 5). На первом этапе вычислений DrRa­cket применит правило бета:
(poly 3 5) == (+ (expt 2 3) 5) ... == (+ 8 5) == 13

Помимо правила бета, нам также нужны правила, определяющие
значения выражений cond. Эти правила являются алгебраическими, даже притом что они не изучаются в школьном курсе алгебры.
Если первое условие оценивается как #false, то оно отбрасывается,
а остальные условия остаются нетронутыми:
(cond
[#false ...]
[условие2 ответ2]
...)

== (cond
; первая строка отбрасывается
[условие2 ответ2]
...)

Это правило имеет имя condfalse. А вот пример правила condtrue:
(cond
[#true ответ-1]
[условие2 ответ2]
...)

== ответ-1

Это правило применяется также в случаях, когда первое условие –
else.

Язык для начинающих студентов

Рассмотрим следующий пример:
(cond
[(zero? 3) 1]
[(= 3 3) (+ 1 1)]
[else 3])
== ; согласно правилам арифметики и замены равного равным
(cond
[#false 1]
[(= 3 3) (+ 1 1)]
[else 3])
== ; согласно правилу condfalse
(cond
[(= 3 3) (+ 1 1)]
[else 3])
== ; согласно правилам арифметики и замены равного равным
(cond
[#true (+ 1 1)]
[else 3])
== ; согласно правилу condtrue
(+ 1 1)

Эти вычисления иллюстрируют применение арифметических правил и обоих правил cond.
Упражнение 121. Вычислите следующие выражения по шагам:
1. (+ (* (/ 12 8) 2/3)
(- 20 (sqrt 4)))
2. (cond
[(= 0 0) #false]
[(> 0 1) (string=? "a" "a")]
[else (= (/ 1 0) 9)])
3. (cond
[(= 2 0) #false]
[(> 2 1) (string=? "a" "a")]
[else (= (/ 1 2) 9)])
Используйте движок пошаговых вычислений в DrRa­cket, чтобы
проверить свои рассуждения. 
Упражнение 122. Пусть программа содержит следующее определение:
(define (f x y)
(+ (* 3 x) (* y y)))

Покажите по шагам, как DrRa­cket вычислит следующие выражения:
1. (+ (f 1 2) (f 2 1))
2. (f 1 (* 2 3))
3. (f (f 1 (* 2 3)) 19)
Используйте движок пошаговых вычислений в DrRa­cket, чтобы
проверить свои рассуждения. 

219

220

Интермеццо 1

Значения и вычисления
Движок пошаговых вычислений в DrRa­cket имитирует действия ученика, изучающего алгебру. В отличие от вас, этот
механизм прекрасно справляется с применением законов
арифметики и алгебры, представленных выше.
Вы можете и должны использовать этот механизм, когда затрудняетесь определить, как работает новая языковая конструкция. В разделах «Вычисления» предлагаются
специальные упражнения для этого, но вы можете составить свои
примеры, пропустить их через движок пошаговых вычислений и попытаться понять, почему он выполняет те или иные шаги.
Наконец, движок пошаговых вычислений можно использовать,
увидев результат программы, который может показаться вам ошибочным. Эффективное использование движка пошаговых вычислений для этой цели требует практики. Например, проводя подобные
исследования, часто приходится копировать блоки программы и удалять ненужные части. Но, освоив движок пошаговых вычислений, вы
обнаружите, что он помогает четко объяснить ошибки времени выполнения и логические ошибки в ваших программах.

Специалисты называют
движок пошаговых
вычислений в DrRa­cket
моделью вычислений.
В главе 21 будет
представлена ​​еще
одна модель –
интерпретатор.

Ошибки в BSL
Когда DrRa­cket обнаруживает, что какая-то фраза в круглых
скобках не принадлежит языку BSL, он сообщает о синтаксической ошибке. Чтобы определить, является ли синтаксически допустимой программа, заключенная в скобки, DrRa­
cket использует грамматику, представленную во врезке «Базовая грамматика BSL» выше, и рассуждает в соответствии с правилами, описанными выше. Однако не все синтаксически допустимые
программы являются осмысленными.
Когда DrRa­cket выполняет синтаксически допустимую программу
и обнаруживает, что какая-то операция применяется к значению неправильного типа, возникает ошибка времени выполнения. Рассмот­
рим синтаксически допустимое выражение (/ 1 0), которое, как вы
знаете из математики, не имеет значения. Поскольку все вычисления
в BSL основываются на правилах математики, DrRa­cket сообщает об
ошибке:

Практически полный
список сообщений об
ошибках приводится
в последнем разделе
этого интермеццо.

> (/ 1 0)
/:division by zero (/:деление на ноль)

Естественно, что та же ошибка будет обнаружена, даже если выражение, такое как (/ 1 0), будет глубоко вложено в другое выражение:
> (+ (* 20 2) (/ 1 (- 10 10)))
/:division by zero (/: деление на ноль)

Язык для начинающих студентов

Поведение DrRa­cket отражается в наших вычислениях, как описывается далее. Когда обнаруживается выражение, не имеющее значения, и правила вычислений не допускают дальнейшего упрощения,
мы говорим, что вычисления застопорились. Такое застопоривание
соответствует ошибке времени выполнения. Например, вычисление
выражения, приведенного выше, вызывает застопоривание:
(+ (* 20 2) (/ 1 (- 10 10)))
==
(+ (* 20 2) (/ 1 0))
==
(+ 40 (/ 1 0))

Эта последовательность вычислений также показывает, что DrRa­
cket исключает контекст застопорившегося выражения, потому что
оно сигнализирует об ошибке. В этом конкретном примере исключается сложение числа 40 с застопорившимся выражением (/ 1 0).
Не все вложенные застопорившиеся выражения сигнализируют об
ошибках. Допустим, программа содержит следующее определение:
(define (my-divide n)
(cond
[(= n 0) "inf"]
[else (/ 1 n)]))

Если применить функцию my-divide к числу 0, то DrRa­cket выполнит
следующую последовательность вычислений:
(my-divide 0)
==
(cond
[(= 0 0) "inf"]
[else (/ 1 0)])

В данной ситуации было бы неправильно говорить, что функция
сигнализирует об ошибке деления на ноль, даже притом, что выделенное подвыражение позволяет предположить это. Причина в том,
что выражение (= 0 0) вернет #true, поэтому второе условие cond не
играет никакой роли:
(my-divide 0)
==
(cond
[(= 0 0) "inf"]
[else (/ 1 0)])
==
(cond
[#true "inf"]
[else (/ 1 0)])
== "inf"

К счастью, наши законы вычислений автоматически решают подобные ситуации. Мы просто должны помнить, когда они применяются. Например, в выражении

221

222

Интермеццо 1
(+ (* 20 2) (/ 20 2))

сложение не будет выполнено до умножения или деления. Аналогично выделенное деление в
(cond
[(= 0 0) "inf"]
[else (/ 1 0)])

не заменит полное выражение cond, пока соответствующая строка не
станет первым условием.
В любых случаях следует руководствоваться следующим правилом:
Всегда выбирайте самое внешнее и крайнее левое вложенное выражение, готовое к вычислению.
Оно может показаться несколько упрощенным, но всегда объясняет результаты BSL.
Часто программисты также предпочитают знать, в какой функции
произошла ошибка. Вспомните проверяющую версию area-of-disk из
раздела 6.3:
(define (checked-area-of-disk v)
(cond
[(number? v) (area-of-disk v)]
[else (error "number expected")]))

Теперь представьте попытку применения checked-area-of-disk
к строке:
(- (checked-area-of-disk "a")
(checked-area-of-disk 10))
==
(- (cond
[(number? "a") (area-of-disk "a")]
[else (error "number expected")])
(checked-area-of-disk 10))
==
(- (cond
[#false (area-of-disk "a")]
[else (error "number expected")])
(checked-area-of-disk 10))
==
(- (error "number expected")
(checked-area-of-disk 10))

В этой точке можно попытаться вычислить второе выражение, но
даже притом что оно вернет результат, примерно равный 314, ваши
вычисления все равно столкнутся с выражением error, которое подобно застопорившемуся выражению. Проще говоря, вычисления закончатся на:
(error "number expected")

223

Язык для начинающих студентов

Логические выражения
В нашем текущем определении языка BSL отсутствуют выражения
or и and. Их добавление может служить примером, как изучать новые
языковые конструкции. Сначала нужно понять их синтаксис, а затем
семантику.
Вот дополненная грамматика выражений:
выражение = ...
| (and выражение выражение)
| (or выражение выражение)

Грамматика утверждает, что and и or являются ключевыми словами,
за каждым из которых следуют два выражения. Это не применение
функции.
Чтобы понять, почему and и or в языке BSL не являются функциями,
рассмотрим практику их использования. Предположим, нам нужно
сформулировать условие, которое определяет равенство выражения
(/ 1 n) и значения r:
(define (check n r)
(and (not (= n 0)) (= (/ 1 n) r)))

В данном примере проверка сформулирована через выражение and,
чтобы избежать ошибки деления на 0. Теперь применим check к 0 и 1/5:
(check 0 1/5)
== (and (not (= 0 0)) (= (/ 1 0) 1/5))

Если бы выражение and было обычной операцией, то мы
должны были бы вычислить оба подвыражения и в результате столкнулись бы с ошибкой. Однако and просто не вычислит второе выражение, если первое вернет #false. Проще
говоря, and выполняет вычисления по короткой схеме.
Теперь вы без труда смогли бы сами сформулировать
правила вычисления для выражений and и or. Вот еще один
способ объяснить их суть – преобразовать их в другие выражения:
(and выражение-1 выражение-2)

– это краткая форма записи для
(cond
[выражение-1 выражение-2]
[else #false])

и
(or выражение-1 выражение-2)

Чтобы гарантировать,
что выражение-2
вычисляет логическое
значение, в этих сокращениях стоило использовать (if выражение-2
#true #false) вместо
простого выражение-2.
Мы тут немного
приукрасили.

224

Интермеццо 1

– это краткая форма записи для
cond
[выражение-1 #true]
[else выражение-2])

То есть если у вас возникнут сомнения в том, как вычислить выражение and или or, используйте приведенные выше эквиваленты. Но
мы надеемся, что вы будете прекрасно понимать эти операции на интуитивном уровне, а этого почти всегда достаточно.
Упражнение 123. Использование if могло удивить вас, потому что
в этом интермеццо такая форма условия больше нигде не упоминается. Может создаться впечатление, что интермеццо дает объяснения
в форме, не имеющей объяснения. На данный момент мы полагаемся
на ваше интуитивное понимание if как сокращения для cond. Напишите правило, показывающее, как переформулировать
(if проверяемое-выражение выражение-тогда выражение-иначе)

в выражение cond. 

Определения констант
Программы включают не только определения функций, но и определения констант, но константы не были включены в нашу первую
грамматику. Итак, вот дополненная грамматика, включающая определения констант:
определение = ...
| (define имя выражение)

По своей форме определение константы подобно определению функции. Ключевое слово define отличает определения
констант от выражений, но не отличает от определений функций. Чтобы понять, что определяется – функция или константа, читатель кода должен взглянуть на вторую часть определения.
Теперь давайте разберемся с особенностями определения констант.
В определении константы с литералом в правой части, например

Как оказывается,
в DrRa­cket имеется другой способ определения
функций; см. главу 17.

(define RADIUS 5)

переменная – это просто сокращенное обозначение значения выражения. Когда в процессе вычислений среда программирования DrRa­
cket встретит ссылку на RADIUS, она заменит ее числом 5.
Если в правой части определения указано выражение, например
(define DIAMETER (* 2 RADIUS))

то это выражение должно быть вычислено немедленно. В таких выражениях допускается использовать любые предшествующие определения. То есть последовательность определений

Язык для начинающих студентов
(define RADIUS 5)
(define DIAMETER (* 2 RADIUS))

эквивалентна
(define RADIUS 5)
(define DIAMETER 10)

Это правило выполняется, даже если в определениях констант используются определения функций:
(define
(define
(define
(define

RADIUS 10)
DIAMETER (* 2 RADIUS))
(area r) (* 3.14 (* r r)))
AREA-OF-RADIUS (area RADIUS))

По мере оценки этой последовательности определений DrRa­cket
сначала выяснит, что RADIUS имеет значение 10, DIAMETER – 20, а area –
это имя функции. Наконец, будет вычислено выражение (area RADIUS)
и полученное значение 314 связано с именем AREA-OF-RADIUS.
Возможность смешивания определений констант и функций также
вводит новый вид ошибок времени выполнения. Взгляните на сле­
дую­щую программу:
(define
(define
(define
(define

RADIUS 10)
DIAMETER (* 2 RADIUS))
AREA-OF-RADIUS (area RADIUS))
(area r) (* 3.14 (* r r)))

Она похожа на предыдущую, но в ней два последних определения
поменялись местами. Оценка первых двух определений будет выполнена так же, как прежде. Но при попытке оценить третье определение
процесс пойдет по другому пути. Чтобы оценить третье определение,
необходимо вычислить выражение (area RADIUS). Определение RADIUS
предшествует этому выражению, но определение area еще не встречалось. При оценке программы в DrRa­cket вы получите сообщение
об ошибке, поясняющее, что «эта функция не определена». Поэтому
будьте внимательны и используйте функции в определениях констант, только если известно, что они уже определены.
Упражнение 124. Оцените следующую программу, шаг за шагом:
(define PRICE 5)
(define SALES-TAX (* 0.08 PRICE))
(define TOTAL (+ PRICE SALES-TAX))

Возникнет ли ошибка при оценке следующей программы?
(define COLD-F 32)
(define COLD-C (fahrenheit->celsius COLD-F))
(define (fahrenheit->celsius f)
(* 5/9 (- f 32)))

А такой?
(define LEFT -100)

225

226

Интермеццо 1
(define
(define
(define
(define

RIGHT 100)
(f x) (+ (* 5 (expt x 2)) 10))
f@LEFT (f LEFT))
f@RIGHT (f RIGHT))

Проверьте свои рассуждения с по­мощью движка пошаговых вычислений в DrRa­cket. 

Определения структур
Как вы уже знаете, define-struct – самая сложная конструкция в BSL.
Поэтому мы оставили ее объяснение напоследок. Вот ее грамматика:
определение = ...
| (define-struct имя [имя ...])

Определение структур – это третья форма определений. От определений констант и функций определение структур отличается ключевым словом.
Вот простой пример:
(define-struct point [x y z])

Здесь point, x, y и z являются переменными, а круглые скобки помещены в соответствии с шаблоном грамматики, это правильное
определение типа структуры. Напротив, следующие два предложения
в круглых скобках:
(define-struct [point x y z])
(define-struct point x y z)

не являются допустимыми определениями, потому что за define-struct
не следует одно имя переменной и последовательность имен переменных в скобках.
Синтаксис define-struct прост, но его трудно объяснить с по­мощью
правил оценки. Как уже упоминалось несколько раз, определение
define-struct определяет сразу несколько функций: конструктор, несколько селекторов и предикат. То есть определение
(define-struct c [s-1 ... s-n])

вводит в программу следующие функции:
1) make-c: конструктор;
2) c-s-1... c-s-n: последовательность селекторов;
3) c?: предикат.
Эти функции имеют тот же статус, что и +, – или *. Однако, прежде
чем переходить к правилам, управляющим созданием этих новых
функций, мы должны вернуться к определению значений. В конце
концов, одна из целей define-struct – представление класса значений,
отличных от всех существующих значений.

Язык для начинающих студентов

Проще говоря, define-struct расширяет вселенную значений, добавляя в нее структуры, которые объединяют несколько значений в одно.
Когда программа включает определение define-struct, оценка значений в ней изменяется:
Значение – это: число, логическое значение, строка, изображение
zz или значение структурного типа:

(make-c _value-1 ... _value-n)
где предполагается, что структура c определена.
Например, определение point добавляет значения, имеющие следующую форму:
(make-point 1 2 -1)
(make-point "one" "hello" "world")
(make-point 1 "one" (make-point 1 2 -1))
...

Теперь можно переходить к правилам оценки новых функций.
Если c-s-1 применяется к структуре c, то она возвращает первый компонент значения. Точно так же второй селектор извлекает второй
компонент, третий селектор – третий компонент и т. д. Связь между
конструктором новых данных и селекторами лучше всего охарактеризовать с по­мощью n уравнений, добавляемых к правилам BSL:
(c-s-1 (make-c V-1 ... V-n)) == V-1
(c-s-n (make-c V-1 ... V-n)) == V-n

Для нашего текущего примера получаем конкретные уравнения:
(point-x (make-point V U W)) == V
(point-y (make-point V U W)) == U
(point-z (make-point V U W)) == W

Встретив выражение (point-y (make-point 3 4 5)), DrRa­cket заменит
это выражение значением 4, а выражение (point-x (make-point (makepoint 1 2 3) 4 5)) будет оценено как (make-point 1 2 3).
Предикат c? может применяться к любому значению. Он вернет
#true, если значение является структурой c, и #false в противном случае. Оба этих условия можно преобразовать в два уравнения:
(c? (make-c V-1 ... V-n)) == #true
(c? V)
== #false

где V не является значением, сконструированным с по­мощью make-c.
И снова уравнения будет проще понять, если выразить их в терминах
нашего примера:
(point? (make-point U V W)) == #true
(point? X)
== #false

где X является значением, но не является экземпляром структуры
point.

227

228

Интермеццо 1

Упражнение 125. Определите, какие из следующих предложений
являются допустимыми:
1. (define-struct oops [])
2. (define-struct child [parents dob date])
3. (define-struct (child person) [dob date])
Объясните, почему то или иное предложение является допустимым или недопустимым. 
Упражнение 126. Определите значения следующих выражений,
исходя из предположения, что в области определений объявлены следующие структуры:
(define-struct point [x y z])
(define-struct none [])

1. (make-point 1 2 3)
2. (make-point (make-point 1 2 3) 4 5)
3. (make-point (+ 1 2) 3 4)
4. (make-none)
5. (make-point (point-x (make-point 1 2 3)) 4 5)
Объясните, почему то или иное выражение имеет или не имеет
значения. 
Упражнение 127. Предположим, что программа содержит следующее определение:
(define-struct ball [x y speed-x speed-y])

Предскажите результаты вычисления следующих выражений:
1. (number? (make-ball 1 2 3 4))
2. (ball-speed-y (make-ball (+ 1 2) (+ 3 3) 2 3))
3. (ball-y (make-ball (+ 1 2) (+ 3 3) 2 3))
4. (ball-x (make-posn 1 2))
5. (ball-speed-y 5)
Проверьте свои предсказания, вычислив эти выражения в области
взаимодействий и с использованием движка пошаговых вычислений. 

Тесты в BSL
Во врезке «Полная грамматика BSL» представлена полная грамматика языка BSL плюс несколько форм тестирования.
Общий смысл тестовых выражений легко объяснить. После щелчка
на кнопке RUN (Выполнить) DrRa­cket соберет все тестовые выражения и переместит их в конец программы, сохраняя порядок их определения. Затем будет выполнено содержимое области определений.
Каждый тест вычислит свои части, а затем сравнит их с ожидаемым
результатом с по­мощью некоторого предиката. Помимо этого, тесты

Язык для начинающих студентов

взаимодействуют с DrRa­cket, способствуя сбору некоторой статистики и информации об ошибках тестирования.
Упражнение 128. Скопируйте следующие тесты в область определений DrRa­cket:
(check-member-of "green"
(check-within (make-posn
(make-posn
(check-range #i0.9 #i0.6
(check-random (make-posn
(make-posn
(check-satisfied 4 odd?)

"red" "yellow" "grey")
#i1.0 #i1.1)
#i0.9 #i1.2) 0.01)
#i0.8)
(random 3) (random 9))
(random 9) (random 3)))

Убедитесь, что все они терпят неудачу, и объясните, почему. 

Полная грамматика BSL
определение-выражения = определение
| выражение
| тест
определение = (define (имя переменная переменная ...) выражение)
| (define имя выражение)
| (define-struct имя [имя ...])
выражение =
|
|
|
|
|
|
|
|
тест =
|
|
|
|
|
|

(имя выражение выражение ...)
(cond [выражение выражение] ... [выражение выражение])
(cond [выражение выражение] ... [else выражение])
(and выражение выражение выражение ...)
(or выражение выражение выражение ...)
имя
число
строка
изображение

(check-expect выражение выражение)
(check-within выражение выражение выражение)
(check-member-of выражение выражение ...)
(check-range выражение выражение выражение)
(check-error выражение)
(check-random выражение выражение)
(check-satisfied выражение имя)

Сообщения об ошибках в BSL
Программы на BSL могут сигнализировать о многих видах синтаксических ошибок. Мы разрабатывали BSL и его систему сообщений об

229

230

Интермеццо 1

ошибках специально для новичков, которые неизбежно допускают
ошибки, и все же сообщения об ошибках требуют некоторого привыкания.
Ниже приводятся примеры сообщений об ошибках, с которыми вы
можете столкнуться. Каждое сообщение состоит из трех частей:
zz фрагмент кода, где обнаружена ошибка;
zz текст сообщения об ошибке;
zz объяснение с предлагаемым вариантом исправления ошибки.

Рассмотрим пример наихудшего сообщения об ошибке, которое вы
когда-либо видели:
(define (absolute n)
(cond
[< 0 (- n)] = 0-to-9 5) здесь интерпретируется как условие; выражение ответа отсутствует.
(cond
[(>= 0-to-9 5)
"head" cond: expected a clause with a question
"tail"]) and an answer, but found a clause
with 3 parts
(cond: ожидается предложение с условием и ответом, а обнаружено предложение
с тремя частями)
В этом случае предложение cond состоит из трех частей, что
также является нарушением грамматики. Выражение (>= 0-to9 5) в этом примере явно предназначено для проверки условия, но за ним следуют два ответа: "head" и "tail". Чтобы исправить ошибку, выберите из двух строк нужную и оставьте
только ее.
(cond)
cond: expected a clause after cond,
but nothing's there
(cond: после cond ожидается предложение, но оно отсутствует)
Оператор cond должен сопровождаться, по меньшей мере, одним предложением с условием и ответом; на практике чаще
используется форма cond с двумя и более предложениями.

Сообщения об ошибках в определениях функций
Во всех следующих сценариях предполагается, что в область определений был введен следующий код и нажата кнопка RUN (Выполнить).

233

234

Интермеццо 1

(define f(x) x)
define: expected only one expression after
the variable name f, but found 1 extra part
(define: после имени переменной f ожидается только одно выражение, но обнаружена одна дополнительная часть)
Определение функции состоит из трех частей: ключевого слова define, последовательности имен переменных, заключенных
в круглые скобки, и выражения. Это определение состоит из четырех частей; вероятно, данное определение является попыткой использовать для заголовка стандартное обозначение из
курса алгебры – f (x) вместо (f x).
(define (f x x) x)
define: found a variable that is used
more than once: x
(define: обнаружена переменная, используемая более одного раза: x)
Последовательность параметров в определении функции не
должна содержать повторяющиеся имена переменных.
(define (g) x)
define: expected at least one variable after
the function name, but found none
(define: после имени функции ожидается,
по крайней мере, одна переменная, но не
обнаружено ни одной)
В языке BSL заголовок функции должен содержать, по крайней
мере, два имени. Первое – имя функции; остальные – имена переменных, которые являются параметрами, а в этом определении функции они отсутствуют.
(define (f (x)) x)
define: expected a variable, but found a part

(define: ожидается переменная, но найдено выражение)
Заголовок функции содержит выражение (x), которое не является именем переменной.

Язык для начинающих студентов

(define (h x y) x y)
define: expected only one expression for the
function body, but found 1 extra part

(define: ожидается только одно выражение
для тела функции, но обнаружена одна дополнительная часть)
В этом определении функции за заголовком следуют два выражения: x и y.

Сообщения об ошибках в определениях структур
Для исследования ошибок, описываемых далее, необходимо добавить
в область определений следующие определения структур и щелкнуть
на кнопке RUN (Выполнить).
(define-struct (x))
(define-struct (x y)) define-struct: expected the structure name
after define-struct, but found a part

(define-struct: после define-struct ожидается имя структуры, а обнаружено выражение)
Определение структуры состоит из трех частей: ключевого слова define-struct, имени структуры и последовательности имен
в круглых скобках. В этих примерах отсутствует имя структуры.
(define-struct x
[y y]) define-struct: found a field name that is used
more than once: y

(define-struct: встречено имя поля, использующееся более одного раза: y)
Последовательность имен полей в определении структуры не
должна содержать повторяющихся имен.
(define-struct x y)
(define-struct x y z) define-struct: expected at least one field name
(in parentheses) after the structure name,
but found something else

(define-struct: после имени структуры
ожидается хотя бы одно имя поля (в скобках), но встречено что-то иное)
В этих определениях структур отсутствуют последовательности
имен полей, заключенные в скобки.

235

II Д
 АННЫЕ
ПРОИЗВОЛЬНОГО
РА ЗМЕРА

Все определения данных в части I описывают данные фиксированного размера. Для нас логические значения, числа, строки и изображения являются атомарными; специалисты в информатике говорят, что
они имеют единичный размер. После знакомства со структурами вы
научились объединять фиксированное количество фрагментов данных. Даже при создании глубоко вложенных структур с использованием языка определения данных вы всегда знаете точное количество
элементарных фрагментов данных в любом конкретном экземпляре.
Однако многие задачи в программировании предполагают вычисления с неопределенным количеством фрагментов информации, которые должны обрабатываться как одно целое. Например, программе
может потребоваться вычислить среднее значение группы чисел или
следить за произвольным количеством объектов в интерактивной
игре. Однако с текущим багажом знаний невозможно сформулировать определение данных, с по­мощью которого можно было бы представить такую ​​информацию в виде данных.
В этой части мы вновь вернемся к языку определения данных
и рассмотрим его особенности, позволяющие описывать данные произвольного (но конечного) размера. Например, первая половина этой
части будет посвящена спискам – форме данных, которая присутствует в большинстве современных языков программирования. Помимо
дополнений в языке определения данных, в этой части мы обновим
рецепт проектирования, чтобы работать с такими определениями
данных. И в заключительных главах вы увидите, как эти определения
данных и обновленный рецепт проектирования применяются в различных контекстах.

8. Списки
Возможно, прежде вам не доводилось сталкиваться с определениями
данных, ссылающимися на самих себя. Ваши учителя родного языка
и литературы почти наверняка старались держаться подальше от подобных определений, так же как и учителя математики. Но программисты не могут позволить себе нечеткости и неопределенности. Их
работа требует точности. Определение данных, как правило, может
содержать несколько ссылок на самого себя, и в этой главе мы покажем примеры таких определений, начав со списка.
Введение списков также расширяет круг приложений, которые мы
можем изучать. В этой главе мы постараемся осветить все аспекты
с по­мощью наглядных примеров, а также объясним причины для
пересмотра рецепта проектирования в следующей главе, где рассказывается, как системно проектировать функции, которые работают
с определениями данных, ссылающимися на самих себя.

8.1. Создание списков
Все мы постоянно составляем списки. Перед тем как пойти в магазин,
мы составляем список продуктов, которые нужно приобрести. Некоторые каждое утро составляют список дел на день. В декабре многие
дети готовят новогодние списки желаний. Чтобы спланировать вечеринку, мы составляем список приглашенных. Организация информации в виде списков – неотъемлемая часть нашей жизни.
Так как часто информация поступает в виде списков, мы должны
научиться представлять такие списки в виде данных на языке BSL.
Поскольку списки так важны, в языке BSL имеется все необходимое
для создания списков и управления ими, по аналогии с точками на
декартовой плоскости (posn). Но, в отличие от точек, определение данных, представляющих списки, всегда остается за вами. Однако не будем забегать вперед и начнем с создания списков.
Формирование списка всегда начинается с пустого списка. В BSL
пустой список определяется так:
'()

Это определение пустого списка. Так же как #true или 5, '() – это
всего лишь константа. Добавляя элемент в список, мы создаем другой
список; для этой цели в BSL имеется операция cons. Например, выражение
(cons "Mercury" '())

создает новый список из списка '() и строки "Mercury". В табл. 7 этот
список представлен в той же наглядной форме, какую мы использовали для иллюстрации структур. В блоке с подписью cons имеются

239

Списки

два поля: first и rest. В этом конкретном примере поле first содержит
строку "Mercury", а поле rest – '().
Получив список с одним элементом, мы можем сконструировать
список с двумя элементами, вновь воспользовавшись операцией cons.
Например, так:
(cons "Venus" (cons "Mercury" '()))

или так:
(cons "Earth" (cons "Mercury" '()))

Таблица 7. Конструирование списка
Список

(cons "Mercury"
'())

Диаграмма
cons
rest

first

"Mercury" '()
(cons "Venus"
(cons "Mercury"
'()))

cons
first

"Venus"

rest
cons
rest

first

"Mercury" '()
(cons "Earth"
(cons "Venus"
(cons "Mercury"
'())))

cons
first

"Earth"

rest
cons
first

"Venus"

rest
first

cons
rest

"Mercury" '()

В средней строке в табл. 7 показано, как можно представить список с двумя элементами. Это тот же блок из двух полей, но на этот
раз поле rest содержит еще один похожий блок. На самом деле этот
вложенный блок является блоком из первой строки в той же таблице.
Наконец, сконструируем список из трех пунктов:
(cons "Earth" (cons "Venus" (cons "Mercury" '())))

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

240

Глава 8

Поскольку даже у хорошего художника возникнут проблемы
с рисованием глубоко вложенных структур, специалисты в информатике прибегают к диаграммам в виде прямоугольников и стрелок. В табл. 8 показано, как можно переставить последнюю строку
из табл. 7. Каж­дая структура cons изображается как отдельный блок.
Если поле rest настолько сложное, что его нельзя нарисовать внутри
блока, то вместо него изображается точка и линия со стрелкой, указывающей на содержащийся в нем блок. В зависимости от расположения блоков вы получаете два вида диаграмм. Первая, показанная
в верхней строке в табл. 8, перечисляет блоки в порядке их создания.
Вторая, показанная в нижней строке, перечисляет блоки в порядке
их включения с по­мощью cons. То есть вторая диаграмма сообщает,
что будет получено при применении first к списку, независимо от
длины этого списка. Именно поэтому программисты предпочитают
второй вариант.
Таблица 8. Изображение списка в виде диаграммы
Cписок

(cons "Earth"
(cons "Venus"
(cons "Mercury"
'())))

Диаграмма
first

cons
rest

first

cons
rest

first

cons
rest

first

cons
rest

first

cons
rest

first

cons
rest

"Mercury" '()

"Earth"

"Venus"

"Venus"

"Earth"

"Mercury" '()

Упражнение 129. Создайте списки, представляющие:
1) список небесных тел, скажем всех планет в нашей Солнечной
системе;
2) список блюд, например стейк, картофель фри, бобы, хлеб, вода,
сыр Бри и мороженое;
3) список цветов.
Нарисуйте диаграммы этих списков, как в табл. 7 и 8. Какой из рисунков вам нравится больше? 
Аналогично можно составлять списки чисел. Вот пример списка из
10 цифр:
(cons 0
(cons 1
(cons 2
(cons 3
(cons 4
(cons 5
(cons 6

241

Списки
(cons 7
(cons 8
(cons 9 '()))))))))))

Чтобы построить этот список, потребуется 10 операций конструи­
рования списков и один пустой список '(). Чтобы создать список
с тремя произвольными числами, например:
(cons pi
(cons e
(cons -22.3 '())))

потребуется использовать три операции cons.
Вообще говоря, списки необязательно должны состоять из значений одного типа, например:
(cons "Robbie Round"
(cons 3
(cons #true
'())))

Имейте в виду, что если
список должен представлять запись с фиксированным количеством
полей, то вместо списка
лучше использовать
структуру.

Первый элемент в этом списке – строка, второй – число,
а последний – логическое значение. Этот список можно рассматривать как запись в трудовой книжке с тремя элементами данных: имя сотрудника, количество лет, отработанных в компании, и наличие у сотрудника корпоративной
медицинской страховки. Также можно вообразить, что этот список
представляет виртуального игрока в какой-то игре. Без определения
данных невозможно понять, что это за данные.
Вот первое определение данных, включающее операцию cons:
; 3LON -- это список с тремя числами:
; (cons Number (cons Number (cons Number '())))
; интерпретация: координаты точки в 3-мерном пространстве

Это определение данных использует операцию cons, подобно тому,
как другие определения используют конструкторы для создания
экземпляров структур, и в некотором смысле cons – это всего лишь
специальный конструктор. Но такое определение данных не демонстрирует, как формировать списки произвольной длины: списки, которые могут ничего не содержать, содержат один элемент, два элемента, десять элементов или, может быть, даже 1 438 901 элемент.
Что ж, попробуем еще раз:
;
;
;
;

List-of-names -- это одно из значений:
-- '()
-- (cons String List-of-names)
интерпретация: список фамилий приглашенных

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

242

Глава 8

самих себя. Имеет ли такое определение смысл? В конце концов, если
бы вы сказали своему учителю родного языка, что «стол – это стол»,
то почти наверняка вы услышали бы в ответ: «Вздор!» – потому что
определение, ссылающееся на самого себя, не объясняет значения
слова.
В информатике и программировании, однако, определения, ссылающиеся на себя, используются очень широко, и при некоторой
осторожности такие определения действительно имеют смысл. Здесь
«осмысленность» означает, что определение данных можно использовать для описания предназначения, а именно для представления
примеров данных, которые принадлежат к определяемому классу,
или для проверки принадлежности некоторого заданного фрагмента данных к определенному классу. С этой точки зрения определение List-of-names (список имен) имеет смысл. Как минимум мы можем сгенерировать '() в качестве одного примера, используя первое
предложение в детализации. А опираясь на '(), как элемент списка
имен List-of-names, легко создать второй пример:
(cons "Findler" '())

Здесь мы используем строку и список List-of-names, чтобы получить набор данных в соответствии со вторым предложением в детализации. Следуя этому правилу, можно сгенерировать массу других
таких же списков:
(cons "Flatt" '())
(cons "Felleisen" '())
(cons "Krishnamurthi" '())

И хотя все эти списки содержат одно имя (представленное в виде
строки), мы можем использовать вторую строку из определения данных для создания списков с большим количеством имен:
(cons "Felleisen" (cons "Findler" '()))

Этот фрагмент данных принадлежит списку имен List-of-names,
потому что "Felleisen" является строкой, а (cons "Findler" '()) соответствует определению списка имен List-of-names.
Упражнение 130. Создайте элемент List-of-names, содержащий
пять строк. Нарисуйте представление списка, подобное изображенному в табл. 7.
Объясните, почему
(cons "1" (cons "2" '()))

является элементом List-of-names, а (cons 2 '()) – нет. 
Упражнение 131. Напишите определение данных для представления списков логических значений. Определение класса должно
соответствовать любым сколь угодно длинным спискам логических
значений. 

243

Списки

8.2. Что такое '(), что такое cons
Теперь приостановимся ненадолго и внимательнее рассмотрим '()
и cons. Как уже упоминалось, '() – это просто константа. Но в отличие
от таких констант, как 5 или "this is a string", она больше похожа на
имя функции или переменную; но если сравнить ее с #true и #false,
то легко увидеть, что в действительности это просто представление
пустого списка на языке BSL.
Что касается правил вычислений, то '() – это новый вид элементарного (и атомарного) значения, отличный от чисел, логических
значений, строк и т. д. Это не составное значение, как Posn. На самом
деле константа '() настолько уникальна, что сама по себе принадлежит к отдельному классу значений, который имеет предикат, распо­
знающий только '() и ничего больше:
; Любое значение -> логическое значение
; является ли заданное значение пустым списком '()
(define (empty? x) ...)

Подобно другим предикатам, empty? может применяться к любому
значению из вселенной значений в языке BSL. Он дает #true, только
если применяется к пустому списку '():
> (empty?
#true
> (empty?
#false
> (empty?
#false
> (empty?
#false
> (empty?
#false

'())
5)
"hello world")
(cons 1 '()))
(make-posn 0 0))

Теперь обратим внимание на cons. Исходя из примеров, что мы
видели до сих пор, можно заключить, что cons – это конструктор,
подобный конструкторам структур, которые мы видели выше. Если
говорить точнее, cons выглядит как конструктор структуры с двумя
полями, первое из которых может содержать любое значение, а второе – список любого вида. Вот как эта идея выражается на языке BSL:
(define-struct pair [left right])
; ConsPair -- это структура:
; (make-pair Any Any).
; Любое значение Любое значение -> ConsPair
(define (our-cons a-value a-list)
(make-pair a-value a-list))

Единственная загвоздка в том, что our-cons принимает любые возможные значения BSL во втором аргументе, а cons – нет, что подтверждает следующий эксперимент:

244

Глава 8
> (cons 1 2)
cons:second argument must be a list, but received 1 and 2

(cons:второй аргумент должен быть списком, а получены значения 1
и 2).
Иначе говоря, cons – это проверяющая функция, подобная той, что
обсуждается в главе 6, которая предлагает следующее уточнение:
;
;
;
;

ConsOrEmpty -- одно из значений:
-- '()
-- (make-pair Any ConsOrEmpty)
интерпретация: ConsOrEmpty -- это класс всех списков

; Any Any -> ConsOrEmpty
(define (our-cons a-value a-list)
(cond
[(empty? a-list) (make-pair a-value a-list)]
[(pair? a-list) (make-pair a-value a-list)]
[else (error "cons: second argument ...")]))

Если cons – это проверяющая функция-конструктор, то вам может
быть интересно узнать, как извлечь отдельные элементы из полученной структуры. В конце концов, в главе 5 говорится, что структуры
невозможно использовать без селекторов. Поскольку структура cons
имеет два поля, у нее есть два селектора: first и rest. Их легко определить в терминах нашей структуры pair:
; ConsOrEmpty -> Любое значение
; извлекает левый элемент из заданной пары
(define (our-first a-list)
(if (empty? a-list)
(error 'our-first "...")
(pair-left a-list)))

Стоп! Определите our-rest.
Если ваша программа имеет доступ к определению структуры pair,
то она легко сможет создавать экземпляры pair, не содержащие '()
или содержащие другие экземпляры pair в правом поле. Независимо от того, как были созданы такие «плохие» экземпляры, намеренно
или случайно, они наверняка приведут к неправильному и странному поведению функций и программ. Поэтому BSL скрывает фактическое определение структуры для cons, чтобы избежать этих проблем.
В разделе 16.2 показан один из способов сокрытия таких определений в программах, но на данный момент нам это не нужно.
Таблица 9. Примитивы, имеющие отношение к спискам
'()
empty?
cons
first
rest
cons?

специальное значение, представляющее пустой список
предикат, распознающий только '()
проверяющий конструктор для создания экземпляров с двумя полями
селектор для извлечения элемента, добавленного последним
селектор для извлечения расширяющего списка
предикат, распознающий экземпляры cons

245

Списки

Таблица 9 обобщает этот раздел. Важно запомнить, что '() – это
уникальное значение, а cons – проверяющий конструктор, который
создает списки. Кроме того, first, rest и cons? являются самыми обычными селекторами и предикатом. Подводя итог, можно сказать, что
в этой главе рассказывается не о новом способе создания данных,
а о новом способе формулирования определений данных.

8.3. Программирование со списками
Предположим, вы ведете список своих друзей и решили, что
список может оказаться настолько большим, что вам понадобится программа, с по­мощью которой можно узнать,
присутствует ли какое-то имя в списке. Чтобы конкретизировать эту идею, давайте сформулируем ее как упражнение:

Здесь слово "friend"
(друг) используется
в смысле, характерном
для социальных сетей,
а не для реальной жизни.

Задача. Вы работаете со списком контактов для нового сотового телефона. Владелец телефона постоянно обновляет этот
список и просматривает его. На данный момент вам поручено
разработать функцию, которая принимает этот список контактов и определяет, содержит ли он имя «Flatt».
Решив эту задачу, мы обобщим ее и напишем функцию, которая
сможет отыскивать любое имя в списке.
Для представления списка имен, в котором функция должна выполнять поиск, вполне подойдет определение данных List-of-names
из предыдущего раздела. Так как определение у нас уже есть, то перейдем сразу к заголовку функции:
; List-of-names -> Boolean
; определяет, присутствует ли имя "Flatt" в списке a-list-of-names
(define (contains-flatt? a-list-of-names)
#false)

Имя a-list-of-names вполне подходит для списка имен, который
принимает функция, но оно слишком длинное, поэтому сократим его
до alon.
Следуя общему рецепту проектирования, приведем несколько
примеров, иллюстрирующих назначение функции. Сначала определим результат для простейшего случая: '(). Поскольку этот список не
содержит ничего, он определенно не содержит "Flatt":
(check-expect (contains-flatt? '()) #false)

Затем рассмотрим варианты списков с одним элементом. Вот два
примера:
(check-expect (contains-flatt? (cons "Find" '()))
#false)
(check-expect (contains-flatt? (cons "Flatt" '()))
#true)

246

Глава 8

В первом случае правильный ответ #false, потому что единственный элемент в списке не "Flatt"; во втором случае единственным
элементом является "Flatt", поэтому ответ – #true. И наконец, более
общий пример:
(check-expect
(contains-flatt?
(cons "A" (cons "Flatt" (cons "C" '()))))
#true)

И снова правильный ответ #true, потому что список содержит
"Flatt".
Стоп! Приведите общий пример, правильным ответом на который
будет #false.
Сделайте глубокий вдох и запустите программу. Заголовок – это
определение-«пустышка»; в нашей программе есть несколько примеров, которые автоматически преобразуются в тесты, и, что самое интересное, некоторые из них выполняются успешно. Они успешно выполняются по ошибке. Если причина вам понятна, то читайте дальше.
Четвертый шаг – проектирование макета функции, соответствующего определению данных. Поскольку определение данных для спис­
ков строк состоит из двух предложений, тело функции должно быть
выражением cond с двумя ветками. Два условия определяют, какой из
двух типов списков получила функция:
(define (contains-flatt? alon)
(cond
[(empty? alon) ...]
[(cons? alon) ...]))

Вместо (cons? alon) во втором предложении можно использовать
else.
Добавим в макет еще одну подсказку. Как вы наверняка помните,
рецепт проектирования предлагает аннотировать каждое предложение в выражении cond выражениями с селекторами, если класс входных данных представлен составными экземплярами. В данном случае
мы знаем, что '() – это элементарное значение, не имеющее никаких
компонентов, составляющих его. В любых других случаях список состоит из строки и другого списка строк, и мы напомним себе об этом
факте, добавив в макет (first alon) и (rest alon):
(define (contains-flatt? alon)
(cond
[(empty? alon) ...]
[(cons? alon)
(... (first alon) ... (rest alon) ...)]))

Теперь перейдем к пятому шагу в нашем рецепте проектирования – непосредственной реализации функции. Для начала рассмот­
рим каждое условие в cond отдельно. Если (empty? alon) истинно, значит, функция получила пустой список, и результат должен быть #false.

Списки

Во втором случае, если (cons? alon) истинно, аннотации в макете напоминают нам, что мы имеем строку в первом элементе и остальную
часть списка. Рассмотрим пример, соответствующий этому условию:
(cons "A"
(cons ...
... '()))

Функция должна сравнить первый элемент с искомой строкой
"Flatt". В этом примере первый элемент содержит строку "A", не совпадающую с "Flatt", поэтому операция сравнения дает #false. Если
взять другой пример, скажем
(cons "Flatt"
(cons ...
... '()))

то функция определила бы, что первый элемент соответствует искомой строке "Flatt", и ответила бы #true. Из всего этого следует, что
второе предложение в выражении cond должно содержать выражение,
сравнивающее строку в первом элементе со строкой "Flatt":
(define (contains-flatt? alon)
(cond
[(empty? alon) #false]
[(cons? alon)
(... (string=? (first alon) "Flatt")
... (rest alon) ...)]))

Если сравнение дает #true, то результат функции должен быть #true.
Если сравнение дает #false, то необходимо рассмотреть остальную
часть списка: (rest alon). Очевидно, что в этом случае функция не может знать окончательного ответа, потому что ответ зависит от того,
что скрыто в «...». Иначе говоря, если первый элемент не совпадает
со строкой "Flatt", то нам нужно проверить, содержит ли остальная
часть списка строку "Flatt".
К счастью, у нас есть функция contains-flatt?, отвечающая всем требованиям. В соответствии с назначением она определяет, содержит
ли список строку "Flatt". А это значит, что (contains-flatt? l) сообщает,
содержит ли список l искомую строку "Flatt". И, следуя той же логике,
(contains-flatt? (rest alon)) определяет, содержит ли (rest alon) строку
"Flatt", что нам и требуется.
Проще говоря, последняя строка должна содержать выражение
(contains-flatt? (rest alon)):
; List-of-names -> Логическое значение
(define (contains-flatt? alon)
(cond
[(empty? alon) #false]
[(cons? alon)
(... (string=? (first alon) "Flatt") ...
... (contains-flatt? (rest alon)) ...)]))

247

248

Глава 8

Вся хитрость в том, чтобы объединить значения двух выражений.
Как уже упоминалось, если первое сравнение дает #true, то нет необходимости выполнять поиск в остальной части списка; но если оно
дает #false, то второе выражение, в свою очередь, может дать #true,
что означает, что имя "Flatt" находится в остальной части списка.
То есть результат (contains-flatt? alon) будет #true, когда либо первое,
либо второе выражение в последней строчке даст #true.
Листинг 24. Поиск в списке
; List-of-names -> Логическое значение
; определяет, присутствует ли имя "Flatt" в списке alon
(check-expect
(contains-flatt? (cons "X" (cons "Y" (cons "Z" '()))))
#false)
(check-expect
(contains-flatt? (cons "A" (cons "Flatt" (cons "C" '()))))
#true)
(define (contains-flatt? alon)
(cond
[(empty? alon) #false]
[(cons? alon)
(or (string=? (first alon) "Flatt")
(contains-flatt? (rest alon)))]))

В листинге 24 показано полное определение функции. В целом оно
мало отличается от определений в первой части книги. Оно состоит из сигнатуры, назначения, двух примеров и определения. Единственное отличие этого определения функции от всего, что вы видели
раньше, – это ссылка функции на саму себя, то есть ссылка на contains-flatt? в теле define. С другой стороны, определение данных тоже
содержит ссылку на самого себя, поэтому ссылка функции на саму
себя не должна вызывать особого удивления.
Упражнение 132. С помощью DrRa­cket проверьте работу функции
contains-flatt? со следующим примером:
(cons "Fagan"
(cons "Findler"
(cons "Fisler"
(cons "Flanagan"
(cons "Flatt"
(cons "Felleisen"
(cons "Friedman" '())))))))

Какой результат, по вашему мнению, должна вернуть функция? 
Упражнение 133. Вот другой способ сформулировать второе предложение cond в функции contains-flatt?:
... (cond
[(string=? (first alon) "Flatt") #true]
[else (contains-flatt? (rest alon))]) ...

Объясните, почему это выражение дает те же результаты, что и выражение в версии в листинге 24. Какая версия понятнее вам? Поясните почему. 

249

Списки

Упражнение 134. Разработайте функцию contains?, которая определяет наличие заданной строки в заданном списке строк.
Примечание. В языке BSL имеется встроенная функция member?,
которая принимает два значения и проверяет, встречается ли первое
во втором, которое интерпретируется как список:
> (member? "Flatt" (cons "b" (cons "Flatt" '())))
#true

Не используйте member? в определении функции contains?. 
Листинг 25. Вычисления со списками, шаг 1
(contains-flatt? (cons "Flatt" (cons "C" '())))
==
(cond
[(empty? (cons "Flatt" (cons "C" '()))) #false]
[(cons? (cons "Flatt" (cons "C" '())))
(or
(string=? (first (cons "Flatt" (cons "C" '()))) "Flatt")
(contains-flatt? (rest (cons "Flatt" (cons "C" '())))))])

8.4. Вычисления со списками
Мы все еще используем язык BSL, поэтому правила алгебры – см. интермеццо 1 – говорят нам, как определять значение таких выражений, как
(contains-flatt? (cons "Flatt" (cons "C" '())))

без привлечения DrRa­cket. Программисты должны четко представлять, как выполняются подобные вычисления, поэтому мы по шагам
разберем один простой пример.
В листинге 25 показан первый шаг, где для определения результата
применения функции используется обычное правило подстановки.
В результате получается условное выражение, потому что, как сказал
бы учитель алгебры, функция задана кусочно.
Листинг 26. Вычисления со списками, шаг 2
...
==
(cond
[#false #false]
[(cons? (cons "Flatt" (cons "C" '())))
(or (string=? (first (cons "Flatt" (cons
(contains-flatt? (rest (cons "Flatt"
==
(cond
[(cons? (cons "Flatt" (cons "C" '())))
(or (string=? (first (cons "Flatt" (cons
(contains-flatt? (rest (cons "Flatt"
==
(cond

"C" '()))) "Flatt")
(cons "C" '())))))])

"C" '()))) "Flatt")
(cons "C" '())))))])

250

Глава 8
[#true
(or (string=? (first (cons "Flatt" (cons "C" '()))) "Flatt")
(contains-flatt? (rest (cons "Flatt" (cons "C" '())))))])
==
(or (string=? (first (cons "Flatt" (cons "C" '()))) "Flatt")
(contains-flatt? (rest (cons "Flatt" (cons "C" '())))))

В листинге 26 показано продолжение вычислений. Чтобы найти
правильную ветвь в выражении cond, мы должны определить значения условий одно за другим. Поскольку список не пустой, результат
первого условия – #false, и поэтому мы исключаем первую ветвь из
дальнейшего рассмотрения. Условие во втором предложении вычисляется в #true, потому что для cons-списка условие cons? выполняется.
Листинг 27. Вычисления со списками, шаг 3
...
==
(or (string=? "Flatt" "Flatt")
(contains-flatt? (rest (cons "Flatt" (cons "C" '())))))
== (or #true (contains-flatt? ...))
== #true

От этого момента осталось сделать всего три шага, чтобы получить
окончательный результат. Эти три шага показаны в листинге 27. Первый вычисляет (first (cons "Flatt" ...)) и получает строку "Flatt". Второй обнаруживает, что "Flatt" совпадает с искомой строкой "Flatt".
Третий сообщает, что (or #true X) имеет значение #true независимо от
значения X.
Упражнение 135. Используйте движок пошаговых вычислений
в DrRa­cket, чтобы проверить порядок вычислений в
(contains-flatt? (cons "Flatt" (cons "C" '())))

Также с по­мощью движка пошаговых вычислений определите результат
(contains-flatt?
(cons "A" (cons "Flatt" (cons "C" '()))))

Что случится, если строку "Flatt" заменить на "B"? 
Упражнение 136. С помощью движка пошаговых вычислений
в DrRa­cket проверьте
(our-first (our-cons "a" '())) == "a"
(our-rest (our-cons "a" '())) == '()

Определения этих функций вы найдете в разделе 8.2. 

9. П
 роектирование
с определениями данных,
ссылающимися на самих себя
На первый взгляд, определения данных, ссылающиеся на самих себя,
кажутся гораздо более сложными, чем определения смешанных данных. Но, как показал пример contains-flatt?, шесть шагов рецепта проектирования остаются действительными и для них. Тем не менее
в этой главе мы обобщим рецепт проектирования, чтобы он еще лучше подходил для случаев использования определений данных, ссылающихся на самих себя. Новые правила касаются процесса выявления,
когда действительно необходимо использовать самоссылающиеся
определения данных, создания макета и определения тела функции:
1. Если постановка задачи предполагает ис- Числа тоже кажутся
пользование информации произвольного произвольно большими.
Для неточных чисел это
размера, то определение данных, представ- иллюзия. Для точных
ляющих ее, должно ссылаться на само себя. целых чисел это
Выше вы видели только один такой класс – действительно так.
список имен List-of-names. Слева на рис. 11 Поэтому работа с целыми числами является
показано, как подобным же образом опреде- частью этой главы.
лить список строк List-of-strings. Остальные
списки атомарных данных определяются точно так же.
Чтобы определение данных, ссылающееся на самого себя,
было допустимым, оно должно удовлетворять двум условиям. Во-первых, определение должно содержать как минимум
два предложения. Во-вторых, хотя бы одно из предложений не
должно ссылаться на определяемый класс данных. Хорошей
практикой считается явное обозначение ссылок на себя с по­
мощью стрелок от ссылок в определении данных на само это
определение; пример такого обозначения см. на рис. 11.

;; A List-of-strings is one of
;; -- '()
;; -- (cons String List-of-strings)

(define (fun-for-los a-list-of-strings)
(cond
[(empty? a-list-of-strings)
[else ; (cons? a-list-of-strings)
(… (f irst a-list-of-strings)
(… (rest a-list-of-strings) …)]))

Рис. 11. Стрелками показаны ссылки определений данных на самих себя

Допустимость определений данных со ссылками на самих себя
необходимо проверять путем создания примеров данных. Начните с предложения, которое не ссылается на определение данных; перейдите к другому предложению и представьте вариант,
использующий предыдущий пример и ссылку на само опреде-

252

Глава 9

ление. Следуя таким путем, для определения данных на рис. 11
вы получите списки, подобные следующим:
'()
для первого предложения
(cons "a" '()) для второго предложения, с использованием предыдущего примера
(cons "b"
для второго предложения, с использованием предыдущего примера
(cons "a"
'()))

Если из определения данных не получается сгенерировать примеры, то это говорит о недопустимости такого определения.
Если создать первые примеры получилось, но вы не понимаете,
как создать более крупные примеры, то это говорит о том, что
такое определение, вероятно, не соответствует его интерпретации.
2. Правила определения заголовка остаются прежними: заголовок
все так же должен включать сигнатуру, описание назначения
и фиктивное определение. Формулируя описание назначения,
сосредоточьтесь на том, что функция вычисляет, а не как она
это делает и, в частности, не на том, как она выполняет обход
экземпляров входных данных.
Вот пример, конкретизирующий этот рецепт проектирования:
; List-of-strings -> Number
; подсчитывает количество строк в списке alos
(define (how-many alos)
0)

В описании назначения четко указано, что функция просто
подсчитывает строки в заданном списке; и нет необходимости заранее думать о том, как сформулировать эту идею в виде
функции на языке BSL.
3. Когда дело доходит до примеров применения функции, обязательно представьте примеры входных данных, в которых
несколько раз используется предложение со ссылкой на само
определение данных. Это лучший способ позже сформулировать тесты, которые охватывают все определение функции.
В нашем текущем примере описание назначения почти само
подсказывает примеры применения функции:
дано
'()
(cons "a" '())
(cons "b" (cons "a" '()))

ожидается
0
1
2

Первая строка соответствует пустому списку, и мы знаем, что
пустой список ничего не содержит. Вторая строка – это список
с одной строкой, поэтому ожидаемый результат 1. Последняя
строка представляет список с двумя строками.

Проектирование с определениями данных, ссылающимися на самих себя

4. По сути, определение данных, ссылающееся на само себя, выглядит так же, как определение смешанных данных. Поэтому
разработка макета может продолжаться в соответствии с рецептом в главе 6. В частности, необходимо сформулировать
выражение cond, в котором количество предложений совпадает
с количеством предложений в определении данных. Каждое условие должно соответствовать своему предложению в определении данных и содержать соответствующие селекторы.
Таблица 10. Как преобразовать определение данных в макет
Вопрос
Различаются ли в определении
данных разные подклассы
данных?
Чем подклассы отличаются
друг от друга?
Имеются ли предложения,
включающие
структурированные значения?
Используются ли
в определении данных ссылки
на само определение?
Имеются ли в определении
данных ссылки на какие-то
другие определения данных?

Ответ
Макет должен содержать столько
предложений cond, сколько имеется
различных подклассов в определении данных
Используйте различия, чтобы
сформулировать условие для каждого
предложения
Если да, добавьте в предложение
соответствующие селекторы
Сформулируйте «естественные рекурсии»
в макете, соответствующие ссылкам
в определении данных на само определение
Специализируйте макет с учетом этого
другого определения данных. Обратитесь
к соответствующему макету. См. раздел 6.1,
шаги 4 и 5 рецепта проектирования

В табл. 10 эта идея выражена в виде викторины с вопросами
и ответами. В левом столбце задаются вопросы об определении
данных, а в правом объясняется, что необходимо предпринять
при построении макета.
Если проигнорировать последнюю строку и применить первые
три вопроса к любой функции, принимающей список строк
List-of-strings, то вы получите следующий макет:
(define (fun-for-los alos)
(cond
[(empty? alos) ...]
[else
(... (first alos) ... (rest alos) ...)]))

Напомним, однако, что цель макета – выразить определение
данных в виде заготовки функции. То есть макет выражает
в форме кода то, что в определении данных выражается как
смесь слов на естественном языке и на языке BSL. Проще говоря, каждой важной части в определении данных должен соответствовать свой аналог в макете, и это правило должно неукос­
нительно соблюдаться, когда определение данных содержит

253

254

Глава 9

Интересно отметить,
что рецепт проектирования произвольно
больших данных
соответствует так
называемым
«доказательствам
по индукции» в математике, а «прыжок веры»
представляет использование гипотезы индукции в индуктивном шаге
такого доказательства.
Логика доказывает
справедливость этого
метода с по­мощью
теоремы индукции.

стрелку, указывающую изнутри определения на само определение. В частности, если определение данных ссылается на само
себя в i-м предложении и k-м поле упомянутой там структуры,
то макет должен включать ссылку в i-м предложении cond и содержать селектор для k-го поля. Для каждого такого селектора
добавьте стрелку, указывающую обратно на параметр функции.
В конечном итоге в макете должно быть столько же стрелок,
сколько имеется в определении данных.
Рисунок 11 иллюстрирует эту идею на примере макетов функций, которые принимают список строк. Оба примера включают
одну стрелку, которая берет начало во втором предложении –
поле и селектор rest соответственно – и указывает на начало
соответствующего определения.
Поскольку код на BSL, как и в большинстве языков программирования, записывается в виде текста, вы должны использовать альтернативу стрелке – рекурсивное применение функции
к соответствующему селектору:
(define (fun-for-los alos)
(cond
[(empty? alos) ...]
[else
(... (first alos) ...
... (fun-for-los (rest alos)) ...)]))

В первых четырех частях этой книги мы будем называть такое
рекурсивное применение функций естественной рекурсией.
5. Тело функции должно начинаться с предложений в cond, не
имеющих рекурсивных вызовов. Такие предложения называются базовыми вариантами. Соответствующие ответы обычно
легко формулируются или уже даны в примерах.
Далее следуют рекурсивные варианты. Для начала напомним,
что вычисляет каждое из выражений в строке макета. Для ес­
тест­венной рекурсии предполагается, что функция уже работает, как указано в описании назначения. Этот последний шаг
называют «прыжком веры», но, как вы увидите, он всегда дает
верный результат.
Все остальное связано с объединением различных значений.
В табл. 11 сформулированы первые четыре вопроса и ответы
для этого шага. Воспользуемся ею, чтобы завершить определение функции how-many. Переименование макета fun-for-los
в how-many дает нам следующее:
; List-of-strings -> Число
; подсчитывает строки в списке alos
(define (how-many alos)
(cond
[(empty? alos) ...]
[else

255

Проектирование с определениями данных, ссылающимися на самих себя
(... (first alos) ...
... (how-many (rest alos)) ...)]))

Таблица 11. Как преобразовать определение данных в макет
Вопрос
Что должны возвращать
нерекурсивные
предложения в cond?
Что вычисляют селекторы
в рекурсивных
предложениях?

Ответ
Примеры должны подсказать вам, какие значения должны возвращаться. Если нет, то сформулируйте соответствующие примеры и тесты
Определения должны подсказать вам, какие
данные извлекают эти селекторы, а описания
интерпретации в определениях данных
сообщат, какую информацию эти данные
представляют
Какой результат
Используйте описание назначения, чтобы
возвращает естественная
определить значение, возвращаемое рекурсиврекурсия?
ным вызовом, без учета того, как это значение
вычисляется. Если описание назначения не дает
ответа, улучшите его формулировку
Как функция может
Найдите в BSL функцию, которая объединяет
объединить значения,
значения. Или, если такой функции нет,
чтобы получить желаемый добавьте в список желаний вспомогательную
результат?
функцию. В большинстве случаев этот
последний шаг реализуется просто. Назначение,
примеры и макет сообщат вам, какая функция
или выражение поможет объединить
полученные значения в правильный результат.
Мы называем такие функции и выражения
комбинаторами, слегка злоупотребляя
существующей терминологией

Как показывают примеры применения функции, ответом для
базового случая является число 0. Два выражения во втором
предложении извлекают элемент first и вычисляют количество
строк в (rest alos). Чтобы вычислить количество строк во всем
списке alos, достаточно просто прибавить 1 к значению последнего выражения:
(define (how-many alos)
(cond
[(empty? alos) 0]
[else (+ (how-many (rest alos)) 1)]))

Этот табличный способ поиска комбинатора
предложил Феликс Клок
(Felix Klock).

Определить правильный способ объединения значений в же­
лае­мый результат не всегда просто. Начинающие программис­
ты часто застревают на этом шаге. Как показано в табл. 12, сначала необходимо поместить в таблицу все примеры применения функции, а также значения выражений в макете. В табл. 13
показано, как выглядит подобная таблица для нашего примера
how-many. В крайнем левом столбце перечислены примеры входных данных, а в крайнем правом – желаемые результаты для

256

Глава 9

этих входных данных. Три столбца между ними показывают
значения выражений в макете: (first alos), (rest alos) и (how-many (rest alos)), последний из которых является естественной
рекурсией. Если рассматривать эту таблицу достаточно долго,
можно заметить, что значения в столбце с результатами всегда
на единицу больше значений в столбце, соответствующем ес­
тественной рекурсии. Таким образом, легко догадаться, что
(+ (how-many (rest alos)) 1)

является тем самым выражением, которое вычисляет желае­
мый результат. Поскольку среда разработки DrRa­cket помогает быстро проверить такие предположения, используйте ее
и щелк­ните на кнопке RUN (Выполнить). Если примеры, преобразованные в тесты, выполнятся успешно, мысленно пройдитесь по выражению, чтобы убедиться, что оно будет правильно
работать для всех списков; в противном случае продолжайте
добавлять в таблицу все новые и новые строки с примерами,
пока у вас не появится другая идея.
Таблица 12. Преобразование макета в функцию, табличный метод
Вопрос
Ответ
Итак, если вы застряли... ...оформите примеры из третьего шага в виде
таблицы. Поместите входное значение в первый
столбец, а желаемый результат – в последний.
В промежуточные столбцы впишите значения,
возвращаемые селекторами и рекурсивными
вызовами. Добавляйте примеры в таблицу, пока
не заметите общую закономерность,
определяющую комбинатор
Обратитесь к описанию назначения другой
Если макет ссылается на
макет вспомогательной
функции и примерам, чтобы определить, что
она вычисляет, и исходите из того, что вы уже
функции, то как
можете использовать результат, даже если
определить, что
вычисляет эта функция?
проектирование этой вспомогательной функции
еще не закончено

Таблица 13. Аргументы, промежуточные значения и результаты
alos
(cons "a"
'())
(cons "b"
(cons "a"
'()))
(cons "x"
(cons "b"
(cons "a"
'())))

(first
alos)
"a"

(rest
alos)
'()

(how-many
(how-many
(rest alos))
alos)
0
1

"b"

(cons "a"
'())

1

2

"x"

(cons "b"
(cons "a"
'()))

2

3

Проектирование с определениями данных, ссылающимися на самих себя

257

В таблице также видно, что некоторые селекторы в макете не
имеют отношения к вычислению фактического результата.
В данном случае нет необходимости вычислять (first alos), что
сильно отличает эту функцию от contains-flatt?, где используются оба выражения из макета.
Продолжая читать эту книгу, имейте в виду, что во многих случаях этап комбинирования можно выразить с по­мощью элементарных операций языка BSL, таких как +, and или cons. Но
иногда вам, возможно, придется добавить вспомогательную
функцию. Кроме того, в некоторых случаях могут понадобиться
вложенные условия.
6. Наконец, преобразуйте все примеры в тесты и убедитесь, что
все они выполняются успешно и охватывают все части функции.
Вот наши примеры для how-many, преобразованные в тесты:
(check-expect (how-many '()) 0)
(check-expect (how-many (cons "a" '())) 1)
(check-expect
(how-many (cons "b" (cons "a" '()))) 2)

Примеры лучше сразу формулировать в виде тестов, и язык BSL
позволяет это. Такой подход поможет также, если на предыдущем шаге вам придется прибегнуть к табличному методу поиска комбинатора.
В табл. 13 приводится обобщенная версия рецепта Вы можете скопировать
13 на одну сторопроектирования, представленного в этом разделе. табл.
ну картонной карточки
В первом столбце перечислены шаги рецепта, а во для записей, записать
втором – ожидаемые результаты после выполнения свои варианты вопросов
каждого шага. В третьем столбце описываются дей- и ответов для этого
рецепта проектироваствия, которые помогут сделать тот или иной шаг. Ре- ния на другой стороне
цепт учитывает определения списков, ссылающиеся и затем всегда держать
на самих себя, которые мы использовали в этой главе. эту карточку под рукой.
Как всегда, для овладения любым процессом нужна
практика, поэтому мы настоятельно рекомендуем выполнить
следующие упражнения, в которых предлагается применить
рецепт для решения нескольких задач.
Таблица 14. Проектирование функций для обработки данных,
ссылающихся на самих себя
Шаги
Результат
Анализ задачи Определение
данных

Действия
Разработка структуры данных, представляющих информацию; создание примеров конкретных элементов информации
и описание интерпретации данных; идентификация ссылок на себя в определении
данных

258

Глава 9

Таблица 14 (окончание)
Шаги
Определение
заголовка

Результат
Сигнатура;
описание
назначения;
фиктивное
определение

Действия
Определение сигнатуры с использованием
известных типов данных; формулирование краткого описания назначения функции; создание фиктивного определения
функции, которое возвращает постоянное
значение из указанного диапазона
Примеры
Примеры
Создание нескольких примеров, не менее
и тесты
одного для каждого предложения
в определении данных
Макет
Макет функции Преобразование определения данных
в макет: по одному условию в выражении
cond для каждого предложения в определении данных; селекторы, с по­мощью которых условия идентифицируют структуру;
одна естественная рекурсия для каждой
ссылки на само определение
Определение Законченное
Выбор способа комбинирования
определение
в ожидаемый результат значений
функции
выражений в предложениях cond
Тестирование Проверка
Преобразование всех примеров в тесты
check-expect и их выполнение
выпол­нения
тестов

9.1. Практические упражнения: списки
Упражнение 137. Сравните макеты функций contains-flatt? и how-many. Кроме имен функций, они совершенно одинаковые. Объясните это
сходство. 
Упражнение 138. Вот определение данных для представления последовательности денежных сумм:
; List-of-amounts -- одно из значений:
; -- '()
; -- (cons PositiveNumber List-of-amounts)

Создайте несколько примеров, чтобы проверить себя, насколько
правильно вы понимаете это определение данных. Добавьте стрелки,
обозначающие ссылки на само определение.
Спроектируйте функцию sum, которая принимает список List-ofamounts и подсчитывает сумму всех денежных средств в списке. Используйте движок пошаговых вычислений в DrRa­cket и посмотрите,
как применение (sum l) обрабатывает короткий список l типа List-ofamounts. 
Упражнение 139. Взгляните на следующее определение данных:
; List-of-numbers -- одно из значений:
; -- '()
; -- (cons Number List-of-numbers)

Проектирование с определениями данных, ссылающимися на самих себя

Некоторые экземпляры этого класса данных можно передать на
вход функции sum из предыдущего упражнения 138, а некоторые – нет.
Спроектируйте функцию pos?, которая принимает список чисел
List-of-numbers и определяет, являются ли все числа в этом списке
положительными. Иначе говоря, если (pos? l) возвращает #true, то
это означает, что l является экземпляром списка денежных сумм
List-of-amounts. Используйте движок пошаговых вычислений
в DrRa­cket, чтобы выяснить, как pos? обрабатывает списки (cons 5 '())
и (cons -1' ()).
Также спроектируйте функцию checked-sum. Функция должна принимать список чисел List-of-numbers и возвращать их сумму, если
этот список является экземпляром списка денежных сумм List-ofamounts; в противном случае checked-sum должна сообщать об ошибке.
Подсказка. Используйте для этого check-error.
Что вычислит функция sum, получив список чисел List-of-numbers? 
Упражнение 140. Спроектируйте функцию all-true, которая принимает список логических значений и определяет, все ли они равны
#true. Иначе говоря, если в списке есть хотя бы одно значение #false,
то функция должна вернуть #false.
Затем спроектируйте функцию one-true, которая принимает список
логических значений и определяет, имеется ли в этом списке хотя бы
одно значение #true. 
Используйте табличный подход к проектированию. Это поможет
определить базовый случай. Используйте движок пошаговых вычислений в DrRa­cket, чтобы увидеть, как эти функции обрабатывают
спис­ки (cons #true '()), (cons #false' ()) и (cons #true (cons #false '())).
Упражнение 141. Представьте, что вам предложили спроектировать функцию cat, которая принимает список строк и объединяет все
его элементы в одну длинную строку. Вы неизбежно придете к сле­
дующему частичному определению:
; List-of-string -> String
; объединяет все элементы из l в одну длинную строку
(check-expect (cat '()) "")
(check-expect (cat (cons "a" (cons "b" '()))) "ab")
(check-expect
(cat (cons "ab" (cons "cd" (cons "ef" '()))))
"abcdef")
(define (cat l)
(cond
[(empty? l) ""]
[else (... (first l) ... (cat (rest l)) ...)]))

Заполните табл. 15. Определите функцию, которая может скомбинировать желаемый результат из значений, вычисленных с по­мощью
подвыражений.
Используйте движок пошаговых вычислений в DrRa­cket, чтобы
проверить порядок вычисления выражения (cat (cons "a" '())). 

259

260

Глава 9

Таблица 15. Таблица для функции cat
l
(first l)
(cons "a"
???
(cons "b"
'()))
(cons
???
"ab"
(cons "cd"
(cons "ef"
'())))

(rest l)
???

(cat (rest l)) (cat l)
???
"ab"

???

???

"abcdef"

Упражнение 142. Спроектируйте функцию ill-sized?, которая
принимает список изображений loi и положительное число n. Она
должна отыскать первое изображение в loi, ширина и высота которого не равны n; если такого изображения нет, то функция должна вернуть #false.
Подсказка. Используйте следующее неполное определение:
; ImageOrFalse -- одно из значений:
; -- Image
; -- #false 

9.2. Непустые списки
Теперь вы знаете достаточно, чтобы уверенно создавать определения
данных, представляющие списки. Решив упражнения (хотя бы некоторые) в конце предыдущего раздела, вы научились обрабатывать
списки различных видов чисел, логических значений, изображений
и т. д. В этом разделе мы продолжим знакомство со списками и приемами их обработки.
Начнем с простой на вид задачи вычисления среднего значения по
списку температур. Для простоты сразу приведем определение данных:
; List-of-temperatures -- одно из значеий:
; -- '()
; -- (cons CTemperature List-of-temperatures)
; CTemperature -- число больше -272.

Вообще говоря, температуры можно представить простыми числами, но второе определение данных напоминает, что не все числа
являются температурами, и вы должны иметь это в виду.
Заголовок выглядит просто:
; List-of-temperatures -> Number
; вычисляет среднюю температуру
(define (average alot) 0)

Придумать примеры для этой задачи тоже несложно, поэтому
сформулируем только один тест:

Проектирование с определениями данных, ссылающимися на самих себя
(check-expect
(average (cons 1 (cons 2 (cons 3 '())))) 2)

Ожидаемый результат – это, как нетрудно догадаться, сумма температур, деленная на их количество.
Немного подумав, можно заметить, что макет функции average должен напоминать макеты, виденные нами до сих пор:
(define (average alot)
(cond
[(empty? alot) ...]
[(cons? alot)
(... (first alot) ...
... (average (rest alot)) ...)]))

Два условия в выражении cond соответствуют двум предложениям
в определении данных; вопросы отличают пустые списки от непус­
тых; и из-за ссылки на само определение данных необходима естест­
венная рекурсия.
Однако превратить этот макет в определение функции слишком
непросто. Первое условие в cond должно возвращать число, представляющее среднее значение для пустого списка температур, но такого числа нет. Точно так же второе условие предполагает применение
функции, которая объединит текущую температуру и среднее значение для остальной части списка температур в новое среднее значение.
Это возможно, но такой способ вычисления среднего значения выглядит неестественным.
Среднее значение температур получается делением их суммы на
количество. Мы отметили это, когда формулировали тривиальный
пример. Наша формулировка предполагает, что average решает три
задачи: суммирование, подсчет и деление. Рецепт, представленный
в первой части, требует для каждой задачи написать отдельную функцию, и если поступить именно так, то организация вычисления среднего станет очевидна:
; List-of-temperatures -> Number
; вычисляет среднюю температуру
(define (average alot)
(/ (sum alot) (how-many alot)))
; List-of-temperatures -> Number
; суммирует температуры в заданном списке
(define (sum alot) 0)
; List-of-temperatures -> Number
; посчитывает количество температур в заданном списке
(define (how-many alot) 0)

Определения последних двух функций, конечно же, являются
элементами списка желаний, и для них нужно спроектировать полные определения. Сделать это легко, потому что how-many одинаково
обрабатывает и списки строк List-of-strings, и списки температур

261

262

Глава 9

List-of-temperature (почему?), а также потому, что конструирование
sum производится с использованием уже известной процедуры:
; List-of-temperatures -> Number
; суммирует температуры в заданном списке
(define (sum alot)
(cond
[(empty? alot) 0]
[else (+ (first alot) (sum (rest alot)))]))

Стоп! Используя пример проектирования функции average, спроектируйте функцию sum и убедитесь, что тест выполняется правильно.
Затем запустите тесты для average.
На данный момент правильность определения average не вызывает
сомнений просто потому, что оно прямо соответствует процедуре вычисления среднего значения, о которой рассказывается в школе. Однако мы пишем программы не только для себя, но и для других.
В частности, другие должны иметь возможность прочитать сигнатуру
функции, применить ее и получить информативный ответ. Но наше
определение average не предполагает обработку пустых списков температур.
Упражнение 143. Определите с по­мощью DrRa­cket, как
Выражаясь языком
математики, мы бы поведет себя функция average, если ей передать пустой списказали, что функция сок. Затем спроектируйте функцию checked-average, которая
average в упражнении выводит информативное сообщение об ошибке при приме143 – это частичная
нении к '(). 
функция, потому что
Альтернативное решение – сообщить об ограничениона вызывает ошибку,
если ей передать '(). ях в сигнатуре, указав, что average не может применяться
к пус­тым спискам. Для этого нужно определить представление данных, исключающее '(), например:
; NEList-of-temperatures -- одно из значений:
; -- ???
; -- (cons CTemperature NEList-of-temperatures)

Вопрос в том, чем заменить «???», чтобы показать, что пустой список '() не является допустимым экземпляром данных. Как вариант
можно показать пример кратчайшего из допустимых списков, который длиннее пустого списка. То есть первое предложение в определении данных должно описывать все возможные списки с единственной температурой:
;
;
;
;

NEList-of-temperatures -- одно из значений:
-- (cons CTemperature '())
-- (cons CTemperature NEList-of-temperatures)
интерпретация: непустые списки температур в шкале Цельсия

Это определение отличается от предыдущих определений списков:
оно отделяет важные элементы: предложение со ссылкой на само
определение от предложения без такой ссылки. Строгое соблюдение
рецепта проектирования требует составить несколько примеров не-

Проектирование с определениями данных, ссылающимися на самих себя

263

пустого списка температур NEList-of-temperatures, чтобы придать
смысл этому определению. Как всегда, начинать следует с базового
предложения, то есть пример должен выглядеть так:
(cons c '())

где c обозначает температуру CTemperature, например (cons -273 '()).
Кроме того, такое определение ясно показывает, что все непустые
элементы списка температур List-of-temperatures тоже являются элементами нового класса данных: (cons 1 (cons 2 (cons 3 '()))) соответствует всем требованиям, если им соответствует (cons 2 (cons 3 '())),
а (cons 2 (cons 3' ())) является списком температур NEList-of-temperatures, потому что (cons 3 '()), в свою очередь, является экземпляром
класса NEList-of-temperatures, как было подтверждено выше. Убедитесь сами, что размер списка температур NEList-of-temperatures не
имеет ограничений.
Теперь вернемся к задаче проектирования average и по- Этот альтернативный
кажем, что эта функция предназначена исключительно для подход показывает, что
данном случае можно
работы с непустыми списками. Определив список темпера- всузить
область опредетур NEList-of-temperatures, мы можем правильно сформу- ления average и создать
тотальную функцию.
лировать сигнатуру:
; NEList-of-temperatures -> Number
; вычисляет среднюю температуру
(check-expect (average (cons 1 (cons 2 (cons 3 '()))))
2)
(define (average ne-l)
(/ (sum ne-l)
(how-many ne-l)))

Естественно, все остальное остается без изменений: описание назначения, тесты и определение функции. В конце концов, сама идея
вычисления среднего предполагает обработку непустого набора чисел, и в этом весь смысл нашего обсуждения.
Упражнение 144. Будут ли sum и how-many правильно обрабатывать
списки NEList-of-temperature, несмотря на то что изначально они
проектировались для работы с списками List-of-temperature? Если вы
уверены, что они не будут правильно обрабатывать списки NEList-­oftemperature, то приведите контрпримеры. Если вы уверены в обратном, то объясните почему. 
Как бы то ни было, определение данных поднимает вопрос, как
спроектировать sum и how-many с учетом того, что теперь они должны
применяться к экземплярам NEList-of-temperature. Вот очевидные результаты выполнения первых трех шагов из рецепта проектирования:
; NEList-of-temperatures -> Number
; суммирует температуры в заданном списке
(check-expect
(sum (cons 1 (cons 2 (cons 3 '())))) 6)
(define (sum ne-l) 0)

264

Глава 9

Этот пример является измененной версией примера average; фиктивное определение возвращает число, но ошибочное для данного
теста.
Четвертый шаг – это самая интересная часть проектирования функции sum для обработки непустых списков NEList-of-temperatures. Во
всех предыдущих примерах требовалось использовать макет, отличающий пустые списки от непустых из-за особой формы определения
данных. Это не относится к списку температур NEList-of-temperatures.
Здесь в обоих предложениях упоминаются непустые списки. Однако
эти два предложения различаются в части rest списков. В частности,
в первом предложении в поле rest используется значение '(), а во
втором вместо него используется непустой список. Чтобы отличить
экземпляры данных первого вида от данных второго вида, следует
извлечь поле rest и проверить его с по­мощью empty?:
; NEList-of-temperatures -> Number
(define (sum ne-l)
(cond
[(empty? (rest ne-l)) ...]
[else ...]))

Здесь else заменяет (cons? (rest ne-l)).
Далее нужно определить, должны ли оба этих предложения или
одно из них обрабатывать ne-l как структуру. Это тот случай, когда
требуется безусловное использование поля rest в ne-l. Иначе говоря,
нужно добавить соответствующие селекторы в два предложения:
(define (sum ne-l)
(cond
[(empty? (rest ne-l)) (... (first ne-l) ...)]
[else (... (first ne-l) ... (rest ne-l) ...)]))

Прежде чем продолжить чтение, объясните, почему выражение,
возвращающее ответ в первом условии, не содержит обращения к селектору (rest ne-l).
Наконец, нужно решить вопрос, касающийся ссылок определения
данных на само себя. Мы знаем, что NEList-of-temperature содержит
одну такую ссылку, поэтому макет sum должен включать одно рекурсивное применение:
(define (sum ne-l)
(cond
[(empty? (rest ne-l)) (... (first ne-l) ...)]
[else
(... (first ne-l) ... (sum (rest ne-l)) ...)]))

В данном случае sum применяется к (rest ne-l) во втором предложении, потому что в этой точке определение данных ссылается на само
себя.
Приступая к пятому шагу проектирования, разберемся с тем, что
у нас уже есть. Поскольку первое предложение в выражении cond
выглядит значительно проще второго с его рекурсивным вызовом,

Проектирование с определениями данных, ссылающимися на самих себя

начнем с него. В этом конкретном случае, согласно условию, sum применяется к списку, содержащему ровно одно значение температуры
(first ne-l). Очевидно, что это значение температуры является суммой всех температур в данном списке:
(define (sum ne-l)
(cond
[(empty? (rest ne-l)) (first ne-l)]
[else
(... (first ne-l) ... (sum (rest ne-l)) ...)]))

Во втором случае список включает, по меньшей мере, две температуры; выражение (first ne-l) извлекает первую из них, а (rest ne-l) –
остальные. Кроме того, макет предлагает использование результата
выражения (sum (rest ne-l)). Но sum – это определяемая нами функция,
и мы пока не можем знать, как она обработает (rest ne-l). Однако у нас
есть описание назначения, в котором говорится, что sum вычисляет
сумму всех температур, имеющихся в указанном списке, то есть в (rest
ne-l). Если это описание верно, то (sum (rest ne-l)) сложит вместе все
числа в ne-l, кроме одного. Чтобы получить окончательный результат,
достаточно просто прибавить первую температуру к этой сумме:
(define (sum ne-l)
(cond
[(empty? (rest ne-l)) (first ne-l)]
[else (+ (first ne-l) (sum (rest ne-l)))]))

Если сейчас запустить тест для данной функции, вы увидите, что
наш прыжок веры был оправдан. Действительно, по причинам, которые не обсуждаются в этой книге, такой прыжок веры оправдан всегда,
поэтому он является неотъемлемой частью рецепта проектирования.
Упражнение 145. Создайте предикат sorted>?, который принимает непустой список температур NEList-of-temperature и возвращает
#true, если температуры в списке отсортированы в порядке убывания.
То есть если второе значение температуры меньше первого, третье
меньше второго и т. д. В противном случае предикат должен возвращать #false.
Подсказка. Это еще одна задача, которая легко решается с по­
мощью табличного метода определения комбинатора. В табл. 16
приводится неполная таблица с несколькими примерами. Заполните
ячейки таблицы недостающими значениями. Затем попробуйте создать выражение, вычисляющее результат по частям. 
Упражнение 146. Спроектируйте функцию how-many для NEList-­oftemperature. Это последняя функция, необходимая для завершения
average, поэтому убедитесь, что average тоже успешно проходит все
свои тесты. 
Упражнение 147. Разработайте определение данных для NEList-­
of-Booleans, представляющее непустые списки логических значений.
Затем повторно спроектируйте функции all-true и one-true из упражнения 140. 

265

266

Глава 9

Таблица 16. Таблица для предиката sorted>?
l

(first l)

(rest l)
???

(sorted>?
(rest l))
#true

(sorted>?
l)
#false

(cons 1
(cons 2
'()))
(cons 3
(cons 2
'()))
(cons 0
(cons 3
(cons 2
'())))

1

3

(cons 2 '())

???

#true

0

(cons 3
(cons 2
'()))

???

???

Упражнение 148. Сравните определения функций из этого раздела (sum, how-many, all-true, one-true) с определениями соответствующих
функций из предыдущих разделов. С какими определениями данных
проще работать, которые допускают пустые списки или которые не
допускают этого? Объясните почему. 

9.3. Натуральные числа
Язык программирования BSL предлагает множество функций, которые принимают списки, и несколько функций, которые их создают.
Среди них make-list, которая принимает число n и некоторое значение v и создает список с n элементами, равными v. Вот некоторые
примеры:
> (make-list 2 "hello")
(cons "hello" (cons "hello" '()))
> (make-list 3 #true)
(cons #true (cons #true (cons #true '())))
> (make-list 0 17)
'()

Проще говоря, несмотря на то что эта функция принимает атомарные данные, она может производить фрагменты данных произвольно большого размера. Возникает вопрос: как такое возможно?
Ответ заключается в том, что make-list принимает на входе не прос­
то число, а число особого вида. В детском саду вы называли эти числа
«счетными числами», то есть эти числа используются для подсчета
предметов. В информатике эти числа называют натуральными числами. В отличие от обычных чисел, натуральные числа имеют такое
определение данных:
;
;
;
;

N -- одно из значений:
-- 0
-- (add1 N)
интерпретация: представляет счетные числа

Проектирование с определениями данных, ссылающимися на самих себя

Первое предложение утверждает, что 0 – это натуральное число и используется, чтобы показать, что нет ни одного объекта для
подсчета. Второе предложение сообщает, что если n – натуральное
число, то и n + 1 тоже является натуральным числом, потому что
add1 – это функция, которая прибавляет 1 к любому заданному числу.
Второе предложение можно было бы записать как (+ n 1), но применение add1 должно сигнализировать о том, что это сложение является
особенным.
Особенность add1 заключается в том, что она действует скорее как
конструктор из некоторого определения структуры, чем как обычная
функция. По этой причине в BSL имеется также функция sub1, которая
является «селектором», соответствующим add1. К любому натуральному числу m, не равному 0, можно применить sub1, чтобы получить
число, использовавшееся при создании m. Другими словами, add1 похожа на cons, а sub1 на first и rest.
Теперь у многих из вас может возникнуть вопрос: какие предикаты
отличают 0 от других натуральных чисел, не равных 0. По аналогии со
списками имеются два предиката: zero?, который определяет – равно
ли указанное число нулю, и positive?, который определяет, больше ли
указанное число нуля.
Теперь вы сможете самостоятельно спроектировать функции для
натуральных чисел, такие как make-list. Определение данных уже
имеется, поэтому просто добавим заголовок:
; N String -> List-of-strings
; создает список, содержащий n копий строки s
(check-expect (copier 0 "hello") '())
(check-expect (copier 2 "hello")
(cons "hello" (cons "hello" '())))
(define (copier n s)
'())

Следующий шаг – разработка макета. В соответствии с определением данных тело макета copier должно состоять из выражения cond
с двумя условиями: одно для значения 0 и одно для положительных
чисел. Кроме того, 0 считается атомарным значением, а положительные числа – структурированными значениями, то есть второе предложение в макете должно содержать выражение с селектором. И последнее, но не менее важное: определение данных для N во втором
предложении ссылается на само себя, поэтому второе предложение
в макете должно осуществлять рекурсивное применение copier к соответствующему выражению с селектором:
(define (copier n s)
(cond
[(zero? n) ...]
[(positive? n) (... (copier (sub1 n) s) ...)]))

267

268

Глава 9

Листинг 28. Создание списка копий
; N String -> List-of-strings
; создает список, содержащий n копий строки s
(check-expect (copier 0 "hello") '())
(check-expect (copier 2 "hello")
(cons "hello" (cons "hello" '())))
(define (copier n s)
(cond
[(zero? n) '()]
[(positive? n) (cons s (copier (sub1 n) s))]))

Листинг 28 содержит полное определение функции copier, полученное на основе ее макета. Давайте восстановим все детали этого
процесса. Как всегда, начнем с предложения в выражении cond, которое не имеет рекурсивных вызовов. Условие в этом предложении
говорит, что (это важно) входное значение равно 0, то есть функция
должна создать пустой список без элементов. Работая над вторым
примером, мы уже прояснили этот случай. Теперь перейдем ко второму предложению в cond и вспомним, что вычисляют выражения в нем:
1) (sub1 n) возвращает натуральное число, которое использовалось при конструировании заданного натурального числа n, которое, как мы знаем, больше 0;
2) (copier (sub1 n) s) создает список, содержащий (sub1 n) копий
строки s, как указано в описании назначения.
Но функция получила число n и должна создать список с n строками s. Если строк в списке на одну меньше, то нетрудно догадаться,
что функция должна просто прибавить строку s к результату (copier
(sub1 n) s) с по­мощью cons. Именно это и делает второе предложение.
Теперь можно запустить тесты, чтобы убедиться, что функция copier выполняется без ошибок хотя бы в двух представленных примерах.
При желании можете создать еще несколько примеров входных данных и опробовать их.
Упражнение 149. Правильно ли работает функция copier, если
вместо строки применить ее к натуральному числу, логическому значению или изображению? Или нужно спроектировать другую функцию? Ответ на этот вопрос вы найдете в части III.
Вот альтернативное определение copier с использованием предложения else:
(define (copier.v2 n s)
(cond
[(zero? n) '()]
[else (cons s (copier.v2 (sub1 n) s))]))

Как поведут себя copier и copier.v2 при применении к 0.1 и "x"?
Объясните. Используйте движок пошаговых вычислений в DrRa­cket,
чтобы подтвердить свое объяснение. 

Проектирование с определениями данных, ссылающимися на самих себя

Упражнение 150. Спроектируйте функцию add-to-pi. Она принимает натуральное число n и прибавляет его к pi без использования
элементарной операции +. Вот начало:
; N -> Number
; вычисляет (+ n pi) без использования +
(check-within (add-to-pi 3) (+ 3 pi) 0.001)
(define (add-to-pi n)
pi)

Закончив определение, обобщите функцию до версии add, которая прибавляет натуральное число n к некоторому произвольному
числу x без использования +. Почему в тесте используется проверка
check-within? 
Упражнение 151. Спроектируйте функцию multiply. Она принимает натуральное число n и умножает его на число x без использования *.
С помощью движка пошаговых вычислений в DrRa­cket вычислите
(multiply 3 x) с произвольным значением x по своему выбору. Как multiply соотносится со знаниями арифметики, полученными в начальной школе? 
Упражнение 152. Спроектируйте две функции: col и row.
Функция col принимает натуральное число n и изображение img
и создает столбец – изображение с n копиями img, расположенными
вертикально.
Функция row принимает натуральное число n и изображение img
и создает строку – изображение с n копиями img, расположенными горизонтально. 
Упражнение 153. Цель этого упражнения – визуализировать результаты студенческого протеста в стиле 1968 года. Вот примерная
идея: небольшая группа студентов наполняет воздушные шарики
краской, входит в какую-нибудь аудиторию и беспорядочно разбрасывает шарики. Программа должна показать точки в аудитории, куда
попали шарики.
Используйте две функции из упражнения 152, чтобы создать прямоугольник размером 8×18 квадратов, каждый из которых имеет размер 10×10 пикселей. Добавьте его в пустую сцену того же размера.
Это – ваша аудитория.
Спроектируйте функцию add-balloons. Она должна принимать
список Posn с координатами, попадающими в границы аудитории,
и создавать изображение аудитории с красными точками, координаты которых соответствуют координатам в заданном списке структур
Posn.
На рис. 12 показан результат нашего решения этого упражнения.
Слева изображена чистая аудитория, в середине – после броска двух
первых шариков, а справа – крайне маловероятное распределение
10 попаданий. Но где 10-я точка? 

269

270

Глава 9

Рис. 12. Случайные атаки

9.4. Русская матрешка
В Википедии дается такое определение русской матрешки: «Матрешка – русская деревянная игрушка в виде расписной куклы, внутри которой находятся подобные ей куклы меньшего размера» – и иллюст­
рируется следующим изображением:

На этом рисунке матрешки расставлены в ряд, чтобы
зритель мог увидеть их все.
Теперь рассмотрим задачу представления таких матрешек в виде данных на BSL. Включив фантазию, можно
представить, что мастер может создать произвольное количество матрешек, вкладываемых друг в друга, потому что
для любой матрешки можно создать матрешку большего
размера, вмещающую ее. С другой стороны, как известно,
глубоко внутри находится цельная матрешка, внутри которой ничего нет.
На каждом следующем уровне в сторону увеличения нас
может интересовать множество разных характеристик: размер (хотя
он связан с уровнем вложенности); цвет; орнамент на поверхности

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

Проектирование с определениями данных, ссылающимися на самих себя

матрешки и т. д. В этой задаче мы выберем только одну характеристику – цвет, который будем представлять в виде строки. С учетом этого
каждая следующая матрешка имеет два свойства: цвет и матрешку,
находящуюся внутри. Для представления информации с двумя свойствами мы всегда определяем структуры:
(define-struct layer [color doll])

А теперь добавим определение данных:
; RD (сокращенно от Russian Doll – русская матрешка) -- это одно из значений:
; -- Строка
; -- (make-layer String RD)

Естественно, первое предложение в этом определении данных
представляет самую внутреннюю матрешку, а точнее ее цвет. Второе
предложение описывает каждую следующую матрешку, в которую
вложена данная матрешка. Это предложение представляет ее в виде
экземпляра layer, который, как следует из определения, содержит цвет
и еще одно поле: матрешку, которая находится внутри этой матрешки.
Взгляните на следующее схематическое изображение:

Здесь изображены три матрешки. Красная – самая внутренняя,
зеленая – в середине и желтая – текущая внешняя матрешка. Чтобы
представить эту матрешку экземпляром RD, можно начать с любого конца. Давайте пойдем изнутри. Красную матрешку легко представить в виде RD. Поскольку внутри ничего нет и матрешка имеет
красный цвет, для ее представления достаточно строки "red". Вторую
матрешку можно представить так:
(make-layer "green" "red")

Это представление говорит, что зеленая (полая) матрешка содержит красную матрешку. Наконец, чтобы получить самую внешнюю
матрешку, просто заключим эту матрешку в еще одну:
(make-layer "yellow" (make-layer "green" "red"))

Этот процесс дает хорошее представление о том, как перейти от
любого набора цветных русских матрешек к представлению данных.

271

272

Глава 9

Но имейте в виду, что программист должен уметь делать и обратное,
то есть переходить от конкретных данных к информации. Чтобы проверить это свое умение, нарисуйте схематичную матрешку для следующего экземпляра RD:
(make-layer "pink" (make-layer "black" "white"))

При желании можете даже попробовать реализовать это на языке
BSL.
Теперь, когда мы создали определение данных и понимаем, как
представлять настоящие куклы и как интерпретировать экземпляры
RD, мы готовы приступить к проектированию функций, принимающих RD. В частности, спроектируем функцию, которая подсчитывает,
сколько матрешек содержится в заданном наборе. Это описание является прекрасным описанием назначения и одновременно определяет сигнатуру:
; RD -> Number
; сколько матрешек в данном наборе an-rd

Сначала определим пример данных и начнем с (make-layer "yellow"
(make-layer "green" "red")). Изображение выше говорит нам, что ожидаемый ответ – 3, потому что имеется три матрешки: красная, зеленая и желтая. Этот пример также говорит нам, что когда входные данные представляют следующую матрешку

то ожидаемый ответ – 1.
Четвертый шаг требует создания макета. Следуя стандартной процедуре для этого шага, получаем следующий макет:
; RD -> Number
; сколько матрешек в данном наборе an-rd
(define (depth an-rd)
(cond
[(string? an-rd) ...]
[(layer? an-rd)
(... (layer-color an-rd) ...
... (depth (layer-doll an-rd)) ...)]))

Количество предложений в cond задается количеством предложений в определении RD. В каждом предложении конкретно указывается, о каких данных идет речь и что мы должны использовать предикаты string? и layer?. Строки не являются составными данными,
но экземпляры layer содержат два значения. Если функции нужны
эти значения, она должна использовать селекторы: (layer-color anrd) и (layer-doll an-rd). Наконец, второе предложение в определении

Проектирование с определениями данных, ссылающимися на самих себя

данных содержит ссылку на само определение в поле doll структуры
layer. Следовательно, нам нужно рекурсивно применить функцию ко
второму выражению с селектором.
Примеры и макет диктуют определение функции. Для нерекурсивного предложения в cond ответ, очевидно, равен 1. Рекурсивное предложение в макете вычисляет следующие результаты:
zz (layer-color an-rd) извлекает строку, описывающую цвет теку-

щей матрешки;

zz (layer-doll an-rd) извлекает матрешку, содержащуюся в теку-

щей;
zz (depth (layer-doll an-rd)) определяет, сколько матрешек содержится в (layer-doll an-rd), в соответствии с описанием назначения функции depth.
Это последнее число близко к желаемому ответу, но не является им,
потому что разница между an-rd и (layer-doll an-rd) составляет один
уровень вложенности, то есть одну дополнительную матрешку. Иначе
говоря, функция должна прибавить 1 к результату рекурсивного применения, чтобы получить фактический ответ:
; RD -> Number
; сколько матрешек в данном наборе an-rd
(define (depth an-rd)
(cond
[(string? an-rd) 1]
[else (+ (depth (layer-doll an-rd)) 1)]))

Обратите внимание, что определение функции не использует
(layer-color an-rd) во втором предложении. И снова мы видим, что
макет отражает схему организации данных, но при этом нам могут
понадобиться не все части фактического определения.
Теперь преобразуем примеры в тесты:
(check-expect (depth "red") 1)
(check-expect
(depth
(make-layer "yellow" (make-layer "green" "red")))
3)

Если выполнить этот код в DrRa­cket, то можно увидеть, что при его
выполнении затрагиваются все части определения depth.
Упражнение 154. Спроектируйте функцию colors. Он должна принимать матрешку и возвращать строку, перечисляющую все цвета
этой и всех вложенных матрешек через запятую и пробел. Для нашего
конкретного примера в результате должна получиться строка:
"yellow, green, red" 

Упражнение 155. Спроектируйте функцию inner, которая принимает RD и возвращает самую внутреннюю матрешку (ее цвет). Ис-

273

274

Глава 9

пользуйте движок пошаговых вычислений в DrRa­cket, чтобы вычислить (inner rd) для значения rd по вашему выбору. 

9.5. Списки в интерактивных программах
Применение списков и определений данных, которые ссылаются на
самих себя, позволяет проектировать и создавать гораздо более интересные интерактивные программы, чем те, что используют конечное
Если вы забыли, как количество данных. Только представьте, что вы можете сопроектируются и созда- здать версию программы космических захватчиков из глаются интерактивные вы 6, которая позволяет игроку произвести столько выстрепрограммы, вернитесь лов из танка, сколько он пожелает. Начнем с упрощенной
к разделу 3.6.
версии этой задачи.
Задача. Спроектируйте интерактивную программу, имитирующую стрельбу. Каждый раз, когда «игрок» нажимает клавишу
пробела, программа должна производить выстрел из нижней
части холста. Выстреливаемые снаряды должны подниматься
вертикально вверх со скоростью один пиксель за такт часов.
Проектирование интерактивной программы начинается с разделения информации на константы и элементы постоянно меняющегося состояния мира. В первом случае мы определим физические и графические константы; а для второго случая разработаем представление данных, описывающее состояние мира. Несмотря на некоторую
расплывчатость постановки задачи, она явно предполагает наличие
прямоугольной сцены со снарядами, перемещающимися вертикально вверх. Очевидно, что местоположение снарядов будет меняться
с каждым тактом часов, но размер сцены и координата X пуска снарядов остаются неизменными:
(define HEIGHT 80) ; расстояния в пикселях
(define WIDTH 100)
(define XSHOTS (/ WIDTH 2))
; графические константы
(define BACKGROUND (empty-scene WIDTH HEIGHT))
(define SHOT (triangle 3 "solid" "red"))

Эти конкретные значения не указываются в постановке задачи, но
мы легко можем исправить ситуацию в будущем, изменив пару определений, поэтому можно считать, что мы достигли нашей цели.
В постановке задачи упоминаются два аспекта «мира», которые
меняются с течением времени. Во-первых, нажатие клавиши пробела
производит выстрел и добавляет в сцену новый снаряд. Во-вторых,
все снаряды летят вертикально вверх со скоростью один пиксель за
такт часов. Мы не можем заранее предсказать, сколько выстрелов
сделает игрок, поэтому используем список для их представления:

Проектирование с определениями данных, ссылающимися на самих себя
;
;
;
;

List-of-shots (список выпущенных снарядов) -- одно из значений:
-- '()
-- (cons Shot List-of-shots)
интерпретация: коллекция летящих снарядов

Остается решить еще один вопрос: как представить каждый отдельный снаряд. Мы уже знаем, что все они имеют одну и ту же координату X, которая никогда не меняется. Кроме того, все снаряды
выглядят одинаково. Единственное, чем они отличаются друг от друга, – это координата Y. Поэтому каждый снаряд можно представить
в виде числа:
; Shot (снаряд) -- это Number (число).
; интерпретация: представляет координату Y снаряда

Мы могли бы ограничить представление снарядов интервалом
чисел меньше HEIGHT, потому что знаем, что все снаряды выстреливаются из нижней части холста и затем перемещаются вертикально
вверх, то есть их координата Y непрерывно уменьшается.
Соответственно, для представления мира можно использовать такое определение данных:
; ShotWorld -- список чисел List-of-numbers.
; интерпретация: каждое число в этом списке
; представляет координату Y снаряда

Итак, два определения данных выше описывают списки чисел.
У нас уже есть определение списков чисел, а имя ShotWorld сообщает
назначение этого класса данных.
Следующая наша задача после определения констант и представления данных, описывающего состояния мира, состоит в том, чтобы
выбрать обработчики событий и адаптировать их сигнатуры к данной
проблеме. В текущем примере упоминаются такты часов и клавиша
пробела; все это подталкивает нас к тому, чтобы добавить в список
желаний три функции:
zz функцию, превращающую состояние мира в изображение:
; ShotWorld -> Image
; для каждого значения y в списке w добавляет в сцену
; изображение снаряда в позиции (MID,y}
(define (to-image w) BACKGROUND)

потому что постановка задачи требует визуального отображения снарядов;
zz функцию, обрабатывающую каждый такт часов:
; ShotWorld -> ShotWorld
; перемещает каждый снаряд из списка w вверх на один пиксель
(define (tock w) w)

zz и функцию для обработки событий клавиатуры:

275

276

Глава 9
; ShotWorld KeyEvent -> ShotWorld
; добавляет новый снаряд в сцену,
; если игрок нажал клавишу пробела
(define (keyh w ke) w)

Не забывайте, что кроме функций в списке желаний нам также
нужно определить основную функцию, которая подготавливает мир
и устанавливает обработчики. Листинг 29 включает эту функцию, которая единственная не была разработана нами, потому что она является лишь модификацией стандартной схемы.
Начнем с проектирования функции to-image. У нас уже есть ее сигнатура, описание назначения и заголовок, поэтому теперь мы должны определить примеры. Поскольку определение данных включает два предложения, нужно создать как минимум два примера: '()
и список координат, скажем (cons 9 '()). Ожидаемым результатом для
'(), очевидно, является первоначальная сцена BACKGROUND, а при наличии координаты Y в списке функция должна поместить изображение
снаряда в позицию MID с указанной координатой Y:
(check-expect (to-image (cons 9 '()))
(place-image SHOT XSHOTS 9 BACKGROUND))

Прежде чем продолжить чтение, исследуйте пример, который применяет to-image к списку с двумя снарядами. Это поможет понять, как
работает функция.
Четвертый шаг – преобразование определения данных в макет:
; ShotWorld -> Image
(define (to-image w)
(cond
[(empty? w) ...]
[else
(... (first w) ... (to-image (rest w)) ...)]))

Макет функции, соответствующий определению списка данных,
теперь настолько знаком, что не требует подробных объяснений.
Если у вас есть сомнения, прочитайте вопросы в табл. 10 и создайте
макет самостоятельно.
На основе макета легко определить функцию. Основная идея состоит в том, чтобы объединить примеры с макетом и ответить на вопросы из табл. 11. Следуя установленному порядку, начнем с базового случая – пустого списка снарядов. Как мы уже знаем из примеров,
ожидаемый результат – пустая сцена BACKGROUND. Затем определим,
что должны вычислять выражения во втором условии в cond:
zz (first w) извлекает первую координату из списка;
zz (rest w) – остальные координаты;
zz (to-image (rest w)) добавляет в сцену снаряды из оставшейся

час­ти списка, согласно описанию назначения to-image.

Иначе говоря, (to-image (rest w)) добавляет в сцену оставшиеся снаряды в списке и, соответственно, выполняет почти всю рабо-

Проектирование с определениями данных, ссылающимися на самих себя

ту. Единст­венное, чего не хватает, – отображения первого снаряда
(first w). Если теперь применить описание назначения к этим двум
выражениям, мы получим желаемое выражение для второго предложения cond:
(place-image SHOT XSHOTS (first w)
(to-image (rest w)))

Добавленный значок – это стандартное изображение снаряда; две
координаты прописаны в описании назначения, а последний аргумент place-image – это изображение, созданное из остальной части
списка.
В листинге 29 приводится полное определение функции to-image,
а также остальная часть программы. Функция tock проектируется
точно так же, как to-image, и мы оставляем вам этот процесс как самостоятельное упражнение. Но на обработчике keyh мы остановимся подробнее. Сигнатура этой функции ставит интересный вопрос.
Она указывает, что обработчик принимает два аргумента с нетривиальными определениями данных. С одной стороны, ShotWorld – это
определение данных, ссылающееся само на себя. С другой стороны,
KeyEvents – это фактически перечисление. Пока мы можем только
«догадываться», какой из двух аргументов определяет структуру макета, но позднее мы подробно изучим такие ситуации.
Обработчик событий, такой как keyh, обрабатывает нажатия клавиш. Следовательно, будем считать, что его основным аргументом
является событие от клавиатуры, и будем строить макет на его основе. В частности, следуя определению данных для событий клавиатуры
KeyEvent из раздела 4.3, функция должна включать выражение cond со
множеством предложений, подобных следующим:
(define (keyh w
(cond
[(key=? ke
[(key=? ke
...
[(key=? ke
...
[(key=? ke
...
[(key=? ke

ke)
"left") ...]
"right") ...]
" ") ...]
"a") ...]
"z") ...]))

Листинг 29. Интерактивная программа с состоянием в виде списка
; ShotWorld -> ShotWorld
(define (main w0)
(big-bang w0
[on-tick tock]
[on-key keyh]
[to-draw to-image]))
; ShotWorld -> ShotWorld
; перемещает каждый снаряд вверх на один пиксель
(define (tock w)

277

278

Глава 9
(cond
[(empty? w) '()]
[else (cons (sub1 (first w)) (tock (rest w)))]))
; ShotWorld KeyEvent -> ShotWorld
; добавляет новый снаряд в сцену,
; если игрок нажал клавишу пробела
(define (keyh w ke)
(if (key=? ke " ") (cons HEIGHT w) w))
; ShotWorld -> Image
; для каждого значения y в списке w добавляет в сцену BACKGROUND
; изображение снаряда в позицию (XSHOTS,y}
(define (to-image w)
(cond
[(empty? w) BACKGROUND]
[else (place-image SHOT XSHOTS (first w)
(to-image (rest w)))]))

Конечно, подобно функциям, принимающим все возможные значения, обработчику событий клавиатуры обычно не нужно проверять
все возможные варианты. В данном случае мы знаем, что обработчик
реагирует только на клавишу пробела, а все остальные игнорируются.
Поэтому мы можем объединить все условия cond в предложение else
и отдельно обрабатывать только пробел " ".
Упражнение 156. Добавьте тесты для программы в листинге 29
и убедитесь, что все они выполняются успешно. Объясните, что делает функция main. Затем запустите программу, вызвав функцию main. 
Упражнение 157. Поэкспериментируйте и определите, легко ли
изменить принятые нами решения относительно констант. Например, определите, приводит ли изменение одной константы к желаемому результату:
zz увеличьте высоту холста на 220 пикселей;
zz увеличьте ширину холста на 30 пикселей;
zz измените координату X линии выстрелов на «где-нибудь слева

от середины»;
zz измените фон сцены на зеленый;
zz измените отображение снарядов в виде красного вытянутого
прямоугольника.
Также проверьте, можно ли увеличить размер снаряда вдвое, ничего не меняя, или изменить его цвет на черный. 
Упражнение 158. Запустите функцию main, нажмите клавишу пробела (произведите выстрел) и подождите некоторое время, пока снаряд не исчезнет за верхним краем холста. Даже притом что снаряд
покинул сцену, он все еще будет содержаться в списке, описывающем
состояние мира.
Спроектируйте альтернативную функцию tock, которая не просто
перемещает снаряды на один пиксель с каждым тактом часов, но
также удаляет снаряды, пересекшие верхнюю границу сцены. Под-

Проектирование с определениями данных, ссылающимися на самих себя

сказка. Для этого можно спроектировать вспомогательную функцию
с рекурсивным предложением в cond. 
Упражнение 159. Превратите решение упражнения 153 в интер­
активную программу. Основная функция этой программы, с именем
riot (бунт), должна принимать количество разбрасываемых шариков
и отображать сцену с опускающимися шариками, которые появляются из-за верхнего края по одному в секунду. Функция должна создать
список структур Posn с координатами точек падения шариков.
Подсказки. (1) Вот одно из возможных представлений данных:
(define-struct pair [balloon# lob])
; Pair -- это структура (make-pair N List-of-posns)
; List-of-posns -- одно из значений:
; -- '()
; -- (cons Posn List-of-posns)
; интерпретация: (make-pair n lob) означает n шариков,
; которые нужно бросить

(2) Выражение big-bang – это самое обычное выражение. Его можно
вложить в другое выражение.
(3) Напомним, что случайные числа можно получить с по­мощью
функции random. 

9.6. Замечания о списках и множествах
Эта книга опирается на интуитивное понимание множеств как коллекций значений BSL. В разделе 5.7 конкретно говорится, что определение данных вводит имя для множества значений BSL. В этой книге
постоянно задается один и тот же вопрос о множествах: входит ли какой-либо элемент в некоторое множество. Например, 4 входит в множество чисел, а строка «четыре» – нет. В книге также показано, как
использовать определения данных для проверки, является ли какое-­
либо значение членом некоторого именованного множества, и как
использовать некоторые определения данных для создания выборки
из множества, но эти две процедуры касаются определений данных,
а не множеств как таковых.
В то же время списки представляют коллекции значений. Следовательно, вам может быть интересно узнать, отличаются ли списки от
множеств или это различие несущественное. Если да, то этот раздел
для вас.
На данный момент основное различие между множествами и спис­
ками состоит в том, что множество – это идея, которую мы используем для обсуждения этапов проектирования кода, а список – одна из
многих форм данных в BSL – выбранном нами языке программирования. Эти два понятия используются в наших разговорах на довольно
разных уровнях. Однако, учитывая, что определение данных вводит
представление фактической информации на языке BSL и множества

279

280

Глава 9

Большинство
полноценных языков
напрямую поддерживают представления
данных в виде списков
и множеств.

являются коллекциями информации, вы можете спросить:
как внутри BSL представить множества в виде данных.
В отличие от списков, множества не имеют особого статуса
в BSL, но в то же время множества чем-то напоминают спис­
ки. Ключевое различие заключается в том, какие функции
используют эти формы данных. В BSL имеется несколько констант
и функций для работы со списками, например empty, empty?, cons, cons?,
first, rest. Также есть возможность определить свои функции, например member?, length, remove, reverse и т. д. Вот пример функции, которая
отсутствует в языке BSL, но которую можно определить самостоятельно:
; List-of-string String -> N
; подсчитывает количество вхождений строки s в список los
(define (count los s)
0)

Стоп! Завершите проектирование этой функции.
Продолжим наши рассуждения и без лишних усложнений скажем,
что множества – это фактически списки. А чтобы еще больше упрос­
тить рассуждения, сосредоточимся на списках чисел. Если согласиться с тем, что для нас имеет значение, является ли число частью множества или нет, то почти сразу становится ясно, что списки можно
использовать для представления множеств двумя разными спосо­
бами.
В табл. 17 показаны два определения данных. Оба говорят, что
множество представлено списком чисел. Разница в том, что определение справа имеет ограничение, согласно которому никакое число
не может присутствовать в списке более одного раза. В конце концов,
ключевой вопрос, который мы задаем о множестве: присутствует ли
какое-то число в множестве или нет, и не имеет значения, входит ли
оно в набор один, два или три раза.
Таблица 17. Два представления множеств
;
;
;
;
;
;

Son.L -- одно из значений:
-- пустой список
-- (cons Number Son.L)
Son используется, когда
применяется к Son.L и Son.R

;
;
;
;
;
;

Son.R -- одно из значений:
-- пустой список
-- (cons Number Son.R)
Ограничение: если s -- это Son.R, то никакое
число не может присутствовать дважды в s

Независимо от выбранного варианта, мы можем определить два
важных понятия:
; Son (множество чисел)
(define es '())
; Number Son -> Boolean
; присутствует ли x в s

Проектирование с определениями данных, ссылающимися на самих себя
(define (in? x s)
(member? x s))

Первое – это пустое множество, которое в обоих случаях представлено пустым списком. Второе – проверка на членство.
Один из способов создать большое множество – использовать cons
и приведенные выше определения. Допустим, мы решили создать
представление множества, содержащего числа 1, 2 и 3. Вот одно из таких представлений:
(cons 1 (cons 2 (cons 3 '())))

Оно остается верным для обоих представлений данных. Но можно
ли сказать, что следующее представление
(cons 2 (cons 1 (cons 3 '())))

определяет то же самое множество? А такое представление?
(cons 1 (cons 2 (cons 1 (cons 3 '()))))

В первом случае ответ утвердительный, потому что главное – входит ли число в множество или нет. Но во втором случае ситуация меняется: даже притом что порядок элементов в списке не имеет значения, ограничение в правом определении данных не позволяет отнести последний список к множеству Son.R, потому что он содержит
два экземпляра 1.
Разница между двумя определениями данных проявляется при
проектировании функций. Представьте, что нам нужна функция,
которая удаляет число из множества. Вот соответствующая запись
в списке желаний, которая относится к обоим представлениям:
; Number Son -> Son
; вычитает x из s
(define (set- x s)
s)

В описании назначения используется слово «вычитает», потому
что именно так логики и математики обозначают операцию удаления
элемента из множества.
В табл. 18 показаны результаты проектирования. Они имеют два
отличия:
1) в тесте слева используется список, содержащий два числа 1,
а в тесте справа то же множество определяется с по­мощью
единственного оператора cons;
2) из-за этих различий cлева должна использоваться функция remove-all, а справа – remove.
Стоп! Скопируйте код в область определений DrRa­cket и убедитесь,
что тесты выполняются успешно. Затем читайте далее и продолжайте
экспериментировать с кодом вместе с нами.

281

282

Глава 9

Таблица 18. Функции для двух представлений множеств
; Number Son.L -> Son.L
; удаляет x из s
(define s1.L
(cons 1 (cons 1 '())))

; Number Son.R -> Son.R
; удаляет x из s
(define s1.R
(cons 1 '()))

(check-expect
(set-.L 1 s1.L) es)

(check-expect
(set-.R 1 s1.R) es)

(define (set-.L x s)
(remove-all x s))

(define (set-.R x s)
(remove x s))

Неприятный аспект табл. 18 заключается в том, что тесты используют в качестве ожидаемого результата простой список es. На первый взгляд эта проблема может показаться несущественной. Однако
взгляните на следующий пример:
(set- 1 set123)

где set123 представляет множество чисел 1, 2 и 3 одним из двух способов:
(define set123-version1
(cons 1 (cons 2 (cons 3 '()))))
(define set123-version2
(cons 1 (cons 3 (cons 2 '()))))

Независимо от выбора представления, выражение (set- 1 set123)
возвращает один из двух списков:
(define set23-version1
(cons 2 (cons 3 '())))
(define set23-version2
(cons 3 (cons 2 '())))

Но мы не можем предсказать, какое из этих двух множеств вернет
set-.
Для простого случая с двумя альтернативами можно использовать
такой тест check-member-of:
(check-member-of (set-.v1 1 set123.v1)
set23-version1
set23-version2)

Если ожидаемое множество содержит три элемента, то число возможных вариантов увеличивается до шести, если не считать представлений с повторяющимися элементами, которые допускаются
определением данных слева.
Для решения этой проблемы необходимо объединить две идеи.
Во-первых, давайте вспомним, что на самом деле set- гарантирует
отсутствие указанного элемента в результате. Наш способ преобра-

Проектирование с определениями данных, ссылающимися на самих себя

зования примеров в тесты не реализует эту идею. Во-вторых, эту
идею можно точно сформулировать на языке BSL с по­мощью теста
check-satisfied.
В интермеццо 1 мы уже упоминали, что тест check-satisfied определяет, удовлетворяет ли выражение определенному свойству. Свойство – это функция, принимающая некоторое значение и возвращающая логическое значение. В данном случае мы хотим заявить, что 1
не является членом некоторого множества:
; Son -> Boolean
; #true, если 1 является членом s; иначе #false
(define (not-member-1? s)
(not (in? 1 s)))

С помощью not-member-1? мы можем сформулировать тест, как показано ниже:
(check-satisfied (set- 1 set123) not-member-1?)

и в этом варианте четко указано, что именно должна сделать функция. Более того, эта формулировка не зависит от того, как представлено входное или выходное множество.
Подводя итог, можно сказать, что списки и множества оба являются коллекциями значений, но имеют существенные отличия:
Свойство
членство
порядок

Списки
одно из многих
имеет критическое
значение
количество вхождений имеет значение
размер
конечный,
но произвольный

Множества
имеет критическое значение
не имеет значения
не имеет значения
конечный или бесконечный

Последняя строка в этой таблице представляет хоть и новую, но все
же очевидную идею. Многие из множеств, упоминаемых в данной
книге, бесконечно велики, например число, строка или список строк
List-of-strings. Список, напротив, всегда конечен, хотя может содержать сколь угодно большое количество элементов.
В этом разделе объяснялись существенные различия между множествами и списками, а также два способа представления конечных
множеств с по­мощью конечных списков. Язык BSL недостаточно
выразителен для представления бесконечных множеств; упражнение 299 познакомит вас с совершенно другим представлением множеств, которое также может выражать бесконечные множества. Однако вопрос о том, как реальные языки программирования представляют множества, выходит за рамки этой книги.
Упражнение 160. Спроектируйте функции set+.L и set+.R, создающие множество, добавляя число x в некоторое заданное множество s,
для левого и правого определений данных соответственно. 

283

10. Еще о списках
Списки – универсальная форма данных, доступная практически во
всех языках программирования. Программисты используют их при
создании больших приложений, искусственного интеллекта, распределенных систем и многого другого. В этой главе мы познакомимся
с некоторыми идеями из мира списков, включая функции, создающие списки, представления данных со структурами внутри списков
и способы представления текстовых файлов в виде списков.

10.1. Функции, создающие списки
Вот пример функции, вычисляющей сумму заработной платы на основе почасовой ставки:
; Number -> Number
; вычисляет заработную плату за h рабочих часов
(define (wage h)
(* 12 h))

Он принимает количество отработанных часов и вычисляет сумму
заработной платы за это время. Однако компании, заказавшей программное обеспечение для расчета заработной платы, не интересна
эта функции. Ей нужна функция, вычисляющая заработную плату
для всех ее сотрудников.
Назовем эту новую функцию wage*. Ее задача – обработать все часы,
отработанные сотрудниками, и вычислить заработную плату, причитающуюся каждому из них. Для простоты предположим, что функции
передается список чисел, каждое из которых представляет количест­
во часов, отработанное одним сотрудником в течение недели, и на
выходе должен получиться список с причитающимися сумами заработной платы, также являющийся списком чисел.
Поскольку у нас уже есть определение данных, описывающее входные и выходные данные, мы можем сразу перейти ко второму шагу
проектирования:
; List-of-numbers -> List-of-numbers
; вычисляет недельную зарплату сотрудников с учетом почасовой ставки
(define (wage* whrs)
'())

Далее нужно составить несколько примеров входных данных
и ожидаемых результатов. Итак, сконструируем несколько коротких
списков чисел, представляющих часы, отработанные за неделю:
Дано

'()
(cons 28 '())
(cons 4 (cons 2 '()))

Ожидается

'()
(cons 336 '())
(cons 48 (cons 24 '()))

Еще о списках

Чтобы вычислить результат, мы определяем недельную заработную плату для каждого числа в данном входном списке. В первом
примере во входном списке нет чисел, поэтому на выходе получится
'(). Убедитесь, что понимаете, почему второй и третий ожидаемые
результаты – это то, что нам нужно.
Учитывая, что wage* получает те же данные, что и некоторые другие
функции из главы 8, и макет зависит только от формы определения
данных, мы можем повторно использовать следующий макет:
(define (wage* whrs)
(cond
[(empty? whrs) ...]
[else (... (first whrs) ...
... (wage* (rest whrs)) ...)]))

Если вы захотите попрактиковаться в разработке шаблонов, используйте вопросы из табл. 10.
Пришло время для самого творческого шага в процессе проектирования. Следуя рецепту, рассмотрим каждое предложение cond отдельно. Нерекурсивный случай, когда выполняется условие (empty? whrs),
означает, что функция получила пустой список '(). Согласно примерам выше, в такой ситуации функция должна вернуть '().
Во втором случае рецепт проектирования требует указать, что вычисляет каждое выражение в макете:
zz (first whrs) возвращает первое число из списка whrs, то есть пер-

вое количество отработанных часов;

zz (rest whrs) – оставшаяся часть данного списка;
zz (wage * (rest whrs)) вызывает функцию, которую мы сейчас про-

ектируем, для обработки оставшейся части списка. Как обычно,
используем ее сигнатуру и описание назначения, чтобы определить результат этого выражения. Сигнатура говорит нам, что
эта функция принимает список чисел, а описание назначения
объясняет, что в результате возвращается список с суммами заработной платы, соответствующими отработанным часам, указанным во входном списке.

Ключ к успеху состоит в том, чтобы довериться этим фактам при
формулировании выражения, вычисляющего результат в этом случае, даже притом что функция еще не определена.
Поскольку у нас уже есть список сумм заработной платы для всех,
кроме первого элемента в whrs, функция должна выполнить два действия, чтобы получить ожидаемый результат для всего списка whrs:
вычислить недельную заработную плату для (first whrs) и сконструировать список, представляющий недельные заработные платы для
исходного списка whrs. В первом действии мы повторно используем
wage. Во втором – объединяем две части информации в один список:
(cons (wage (first whrs)) (wage* (rest whrs)))

285

286

Глава 10

Законченное определение функции приводится в листинге 30.
Листинг 30. Вычисление заработной платы для всех сотрудников
; List-of-numbers -> List-of-numbers
; вычисляет недельную зарплату сотрудников с учетом почасовой ставки
(define (wage* whrs)
(cond
[(empty? whrs) '()]
[else (cons (wage (first whrs)) (wage* (rest whrs)))]))
; Number -> Number
; вычисляет заработную плату за h рабочих часов
(define (wage h)
(* 12 h))

Упражнение 161. Преобразуйте примеры в тесты и убедитесь, что
все они выполняются успешно. Затем измените функцию в листинге 30 так, чтобы сотрудники получали по 14 долларов за час работы.
После этого реорганизуйте программу так, чтобы впоследствии можно было изменять величину заработной платы изменением единст­
венного значения в программе. 
Упражнение 162. Никакие сотрудники не могут работать
Покажите результаты,
получаемые на разных более 100 часов в неделю. Чтобы защитить компанию от мошагах рецепта шенничества, функция должна проверить каждый элемент
проектирования. Если
списка, который получает функция wage*, чтобы он не превы застопорились
на каком-то шаге, вышал 100. Если обнаружится хотя бы один такой элемент,
покажите кому-нибудь, функция должна немедленно сигнализировать об ошибке.
как далеко вы продви- Как следует изменить функцию в листинге 30, чтобы выполнулись, следуя рецепту
проектирования. нить эту простую проверку? 
Упражнение 163. Спроектируйте функцию convertFC. Она
Рецепт – это не просто
инструмент проекти- должна принимать список температур по шкале Фаренгейрования, которым вы та и возвращать список соответствующих температур по
можете пользоваться;
это также система шкале Цельсия. 
Упражнение 164. Спроектируйте функцию convert-euro,
диагностики, которая
позволит другим помочь которая принимает список с денежными суммами в долласебе.
рах США и возвращает список соответствующих денежных
сумм в евро. Посмотрите текущий обменный курс в интернете.
Обобщите функцию convert-euro до функции convert-euro*, которая
принимает обменный курс и список с денежными суммами в долларах
США и возвращает список соответствующих денежных сумм в евро. 
Упражнение 165. Спроектируйте функцию subst-robot, которая
принимает список описаний игрушек (каждое описание – это строка
с единственным словом) и заменяет все вхождения слова "robot" на
"r2d2"; все остальные описания должны оставаться неизменными.
Обобщите функцию subst-robot до функции substitute, которая
принимает две строки – new и old и список описаний для обработки. Последний потребляет две строки, называемые новой и старой,
и список строк. Она должна вернуть новый список строк, заменив все
вхождения old на new. 

287

Еще о списках

10.2. Структуры в списках
Представление рабочей недели в виде числа – не лучший выбор, потому что для печати зарплатной ведомости требуется больше информации, чем простое количество часов, отработанных за неделю. Кроме
того, разные сотрудники имеют разную почасовую ставку. К счастью,
элементы списка могут быть не только атомарными, но и любыми
другими значениями, включая структуры.
Наш текущий пример требует именно такого представления данных. Вместо чисел мы используем структуры, представляющие сотрудников, плюс отработанные ими часы работы и почасовые ставки:
(define-struct work [employee rate hours])
; Work -- это структура:
; (make-work String Number Number)
; интерпретация: (make-work n r h) объединяет имя n,
; почасовую ставку r и количество отработанных часов h

Это довольно упрощенное представление, но, несмотря на простоту, оно порождает дополнительную проблему, заставляя нас сформулировать определение данных для списков, содержащих структуры:
;
;
;
;
;

Low (List of works -- список работ) -- это одно из значений:
-- '()
-- (cons Work Low)
интерпретация: экземпляр Low представляет
часы, отработанные сотрудниками компании

Вот три примера списка Low:
'()
(cons (make-work "Robby" 11.95 39)
'())
(cons (make-work "Matthew" 12.95 45)
(cons (make-work "Robby" 11.95 39)
'()))

Используя это определение данных, объясните, почему
эти примеры данных относятся к классу Low.
Стоп! Создайте еще пару примеров данных, соответствующих этому определению.
Теперь, прочувствовав определение Low, можно приступить к переделке функции wage*, чтобы вместо списков чисел она принимала списки Low:

При работе над реальными проектами вместо
подобных суффиксов вы
будете использовать
инструменты управления разными версиями
кода.

; Low -> List-of-numbers
; вычисляет недельную зарплату по заданному списку записей
(define (wage*.v2 an-low)
'())

Суффикс «.v2» в конце имени функции сообщает читателям кода,
что это вторая, переделанная версия функции. В данном случае пере-

288

Глава 10

делка начинается с определения новой сигнатуры и адаптации описания назначения. Заголовок при этом не изменился.
Третий шаг рецепта проектирования – проработка примеров. Начнем со второго списка из представленных выше. Он содержит одну
запись Work, а именно (make-work "Robby" 11.95 39). Она интерпретируется так: "Robby" проработал 39 часов и за каждый час получает
11,95 доллара. То есть его зарплата за неделю составляет 466,05 доллара, или (* 11,95 39). Следовательно, wage*.v2 должна вернуть (cons
466.05 '()). Естественно, если бы входной список содержал две записи
Work, функция выполнила бы два подобных вычисления и вернула
в результате список с двумя числами.
Стоп! Определите ожидаемый результат для третьего примера из
представленных выше.
ЗАМЕЧАНИЕ О ЧИСЛАХ. Имейте в виду, что BSL, в отличие от большинства других языков программирования, воспринимает десятичные числа точно так же, как и вы, а именно как точные дроби. Но подобное выражение на другом языке, таком как Java, вернуло бы для
первой записи значение 466,04999999999995. Поскольку заранее нельзя
предсказать, когда операции с десятичными числами поведут себя
таким странным образом, подобные примеры лучше записывать так:
(check-expect
(wage*.v2
(cons (make-work "Robby" 11.95 39) '()))
(cons (* 11.95 39) '()))

просто чтобы подготовиться к странностям в других языках программирования. Кроме того, оформление примеров в таком стиле также
показывает, что вы действительно поняли, как вычисляется заработная плата. КОНЕЦ.
Теперь перейдем к разработке макета. Воспользовавшись шаблонными вопросами, вы быстро получите следующее:
(define (wage*.v2 an-low)
(cond
[(empty? an-low) ...]
[(cons? an-low)
(... (first an-low) ...
... (wage*.v2 (rest an-low)) ...)]))

потому что определение данных состоит из двух предложений, в первом из которых используется пустой список '(), а во втором список
структур. Но мы также знаем о входных данных больше, чем выражено в этом макете. Например, мы знаем, что (first an-low) извлекает из
данного списка структуру с тремя полями. Похоже, что мы должны
добавить в макет еще три выражения:
(define (wage*.v2 an-low)
(cond
[(empty? an-low) ...]

Еще о списках
[(cons? an-low)
(... (first an-low) ...
... ... (work-employee (first an-low)) ...
... ... (work-rate (first an-low)) ...
... ... (work-hours (first an-low)) ...
(wage*.v2 (rest an-low)) ...)]))

В этом шаблоне перечислены все данные, которые потенциально
могут нас заинтересовать.
Здесь мы используем другую стратегию. В частности, мы должны
создать и использовать отдельный макет функции всякий раз,
когда разрабатываем макет для определения данных, который ссылается на другие определения данных:
(define (wage*.v2 an-low)
(cond
[(empty? an-low) ...]
[(cons? an-low)
(... (for-work (first an-low))
... (wage*.v2 (rest an-low)) ...)]))
; Work -> ???
; макет для обработки экземпляров Work
(define (for-work w)
(... (work-employee w) ...
... (work-rate w) ...
... (work-hours w) ...))

Разделение на отдельные макеты приводит к естественному разделению работы на функции и между функциями; ни одна из них не
становится слишком большой, и все они относятся к конкретному
определению данных.
Наконец, мы готовы приступить к программированию. Как всегда, начнем с самого простого случая, которым здесь является первая
строка. Если wage*.v2 применяется к пустому списку '(), она должна
вернуть '(). Затем переходим ко второй строке и вспоминаем, что
вычисляют выражения в ней.
1. (first an-low) извлекает первую структуру Work из списка.
2. (for-work ...) говорит о том, что мы должны спроектировать
функцию, которая обрабатывает структуры Work.
3. (rest an-low) извлекает оставшуюся часть данного списка.
4. (wage*.v2 (rest an-low)) возвращает список с заработными платами для всех записей Work, кроме первой, в соответствии
с описанием назначения функции.
Если вы застопорились здесь, воспользуйтесь табличным методом
в табл. 12.
Если вам все понятно, то вы увидите, что достаточно сложить два
выражения вместе:
... (cons (for-work (first an-low))
(wage*.v2 (rest an-low))) ...

289

290

Глава 10

предположив, что for-work вычисляет заработную плату для первой
записи Work. Так мы завершили функцию, добавив еще одну функцию в список желаний.
Поскольку for-work – это просто имя, выбранное навскидку, и не совсем подходит для этой функции, выберем другое имя, wage.v2, и добавим ее определение в списке желаний:
; Work -> Number
; вычисляет заработную плату для данной записи w типа work
(define (wage.v2 w)
0)

Проектирование подобных функций подробно описано в части I,
поэтому мы не будем приводить здесь дополнительных пояснений.
В листинге 31 представлен окончательный результат разработки
функций wage и wage*.v2.
Упражнение 166. Функция wage*.v2 принимает список записей
Work и создает список чисел. Однако функции могут также создавать
списки структур.
Спроектируйте представление данных, описывающее зарплатный
чек. Предположим, что зарплатный чек содержит имя сотрудника
и сумму. Затем спроектируйте функцию wage*.v3. Она должна принимать список записей Work и возвращать список зарплатных чеков, по
одному для каждой записи в исходном списке.
Листинг 31. Вычисление зарплат для списка записей Work
; Low -> List-of-numbers
; вычисляет недельную зарплату по заданному списку записей Work
(check-expect
(wage*.v2 (cons (make-work "Robby" 11.95 39) '()))
(cons (* 11.95 39) '()))
(define (wage*.v2 an-low)
(cond
[(empty? an-low) '()]
[(cons? an-low) (cons (wage.v2 (first an-low))
(wage*.v2 (rest an-low)))]))
; Work -> Number
; вычисляет заработную плату для данной записи w типа work
(define (wage.v2 w)
(* (work-rate w) (work-hours w)))

Настоящий зарплатный чек содержит также табельный номер сотрудника. Разработайте представление данных для информации о сотрудниках и измените определение данных для записей Work, чтобы
в них использовалась эта дополнительная информация о сотруднике, а не только строка с его именем. Также измените представление
данных для зарплатного чека, добавив в него имя и табельный номер сотрудника. Наконец, спроектируйте функцию wage*.v4, которая

Еще о списках

291

принимает список обновленных записей Work и возвращает список
зарплатных чеков.
Замечание об итеративном уточнении. Это упражнение демонстрирует прием итеративного уточнения задачи. Мы начали с упрощенного представления зарплаты и постепенно сделали его более
реалистичным. Для такой простой программы, как эта, итеративное
уточнение – избыточный прием, потому что можно сразу начать с полного представления данных, но позже мы столкнемся с ситуация­ми,
когда итеративное уточнение превращается в необходимость. 
Упражнение 167. Спроектируйте функцию sum, которая принимает список структур Posn и возвращает сумму координат X всех его
элементов. 
Упражнение 168. Спроектируйте функцию translate, которая принимает и создает списки со структурами Posn. Для каждой структуры (make-posn x y) во входном списке она должна создавать структуру
(make-posn x (+ y 1)) в выходном списке. Мы заимствовали слово «translate» (перемещение) из геометрии, где сдвиг точки на постоянное
расстояние по прямой называется перемещением. 
Упражнение 169. Спроектируйте функцию legal. Так же как translate из упражнения 168, функция legal должна принимать и возвращать списки со структурами Posn. Выходной список должен содержать все экземпляры Posn из входного списка, чьи координаты X находятся в диапазоне от 0 до 100, а координаты Y – в диапазоне от 0 до
200. 
Упражнение 170. Вот один из способов представления номера телефона:
(define-struct phone [area switch four])
; Phone -- это структура:
; (make-phone Three Three Four)
; Three -- это число в диапазоне от 100 до 999.
; Four -- это число в диапазоне от 1000 до 9999.

Спроектируйте функцию replace. Она должна принимать и возвращать списки структур Phone, замещая все вхождения кода города
(area) 713 на 281. 

10.3. Списки в списках, файлы
В главе 2 мы познакомились с функцией read-file, которая Добавьте (require
читает указанный текстовый файл и возвращает его со- 2htdp/batch-io)
держимое в виде строки. Судя по всему, создатель read-file в область определений.
выбрал на роль представления текстовых файлов простую
строку, и эта функция создает представление данных для конкретного файла (заданного именем). Однако текстовые файлы – это не
просто длинные последовательности символов. Они могут быть организованы в строки и слова, строки и ячейки и множеством других

292

Глава 10

способов. Проще говоря, представление содержимого файла в виде
простой строки может быть пригодно в редких случаях, но часто это
плохой выбор.
Чтобы не быть голословными, рассмотрим пример файла в листинге 32. Он содержит стихотворение Пита Хайна (Piet Hein) и состоит из
множества строк и слов. Если прочитать этот файл с по­мощью выражения
(read-file "ttt.txt")
Как вы наверняка
догадались, многоточия
не являются частью
результата.

то вы получите такую строку:
"TTT\n \nPut up in a place\nwhere ...."

где "\n" внутри строки соответствует разрывам строк.

Листинг 32. Стихотворение «Things take time»
ttt.txt
TTT
Put up in a place
where it's easy to see
the cryptic admonishment
T.T.T.
When you feel how depressingly
slowly you climb,
it's well to remember that
Things Take Time.
Piet Hein

Эту строку можно разбить на части, применив ряд элементарных
операций со строками, например explode, но большинство языков
программирования, включая BSL, поддерживают множество других
представлений файлов и функций, которые создают такие представления из существующих файлов:
zz один из способов представления таких файлов – список строк,

где каждая строка в файле представлена одним строковым элементом в списке:
(cons "TTT"
(cons ""
(cons "Put up in a place"
(cons ...
'()))))

Здесь вторым элементом списка является пустая строка, потому что файл содержит пустую строку;
zz другой способ – список слов, в котором каждое слово представ-

лено строкой:

Еще о списках
(cons "TTT"
(cons "Put"
(cons "up"
(cons "in"
(cons ...
'())))))

Обратите внимание, что в этом представлении исчезла вторая
пустая строка, потому что в пустой строке нет слов;
zz и третье представление основано на списках списков слов:
(cons (cons "TTT" '())
(cons '()
(cons (cons "Put"
(cons "up"
(cons ... '())))
(cons ...
'()))))

Это представление имеет преимущество перед вторым в том,
что оно сохраняет организацию файла, включая вторую пустую
строку. Но проблема в том, что в таком представлении списки
содержат... списки.
При первом знакомстве идея списков, содержащих списки, может
показаться пугающей, но не нужно беспокоиться. Рецепт проектирования поможет справиться и с этими сложностями.
Прежде чем продолжить, рассмотрите листинг 33. В нем показано
несколько примеров функций чтения файлов. Это не исчерпывающий набор примеров: существует множество других способов чтения
текстовых файлов, и вам потребуется узнать еще много нового, чтобы
научиться обрабатывать все возможные виды текстовых файлов. Но
для нашей цели – обучения и изучения принципов систематического проектирования программ – их вполне достаточно, и они помогут
вам разрабатывать достаточно интересные программы.
В листинге 33 используются имена двух определений данных, с которыми мы пока незнакомы, включая списки списков. Как всегда,
начнем с определения данных, но на этот раз мы предлагаем вам сделать это самостоятельно. Поэтому, прежде чем продолжить чтение,
выполните следующие упражнения. Это необходимо, чтобы понять
смысл функций в листинге, а кроме того, не решив эти упражнения,
вам будет трудно понять остальную часть данного раздела.
Упражнение 171. Вы уже знаете, как выглядит определение данных для списка строк List-of-strings. Расскажите о нем вслух. Проверьте себя – сможете ли вы представить стихотворение Пита Хайна (Piet
Hein) в виде такого списка, в котором каждая строка содержит одну
текстовую строку из стихотворения, а также в виде списка, в котором
каждая строка содержит одно слово. Используйте функции read-lines
и read-words, чтобы подтвердить свои рассуждения.

293

294

Глава 10

Затем разработайте определение данных для списка списков строк
List-of-list-of-strings и снова представьте стихотворение Пита Хайна
в виде такого списка, в котором каждая текстовая строка из стихотворения представлена списком строк, по одной на слово, а все стихотворение – списком таких списков. Используйте функцию read-words/
line, чтобы подтвердить свои рассуждения. 
Листинг 33. Функции чтения файлов
; String -> String
; возвращает содержимое файла f в виде строки
(define (read-file f) ...)
; String -> List-of-string
; возвращает содержимое файла f в виде списка строк,
; по одной на каждую строку текста
(define (read-lines f) ...)
; String -> List-of-string
; возвращает содержимое файла f в виде списка строк,
; по одной на каждое слово
(define (read-words f) ...)
; String -> List-of-list-of-string
; возвращает содержимое файла f в виде списка списков строк,
; по одному списку на каждую строку текста и по одной строке на слово
(define (read-words/line f) ...)
; Функции принимают имя файла в виде строки.
; Если файл с указанным именем отсутствует в папке,
; где находится программа, то функции сигнализируют об ошибке.

Как вы, наверное, знаете, в операционных системах имеются программы для измерения файлов. Одни подсчитывают количество
строк, другие определяют, сколько слов присутствует в строке. Начнем с последней программы, чтобы на ее примере посмотреть, как
рецепт проектирования помогает справиться с созданием сложных
функций.
Первый шаг – убедиться в наличии всех необходимых определений данных. Если вы выполнили упражнение 171, как мы просили,
то у вас должно быть определение данных для представления входной информации в проектируемой функции, а в предыдущем разделе определяется список чисел List-of-numbers, описывающий все
возможные выходные результаты. Для краткости обозначим класс
списков списков строк как LLS (List-of-list-of-string – список списков
строк) и используем это имя для оформления заголовка для желаемой функции:
; LLS -> List-of-numbers
; подсчитывает количество слов в каждой текстовой строке
(define (words-on-line lls) '())

Мы дали функции имя words-on-line, потому что оно выражает ее
назначение в одной фразе.

Еще о списках

Далее необходимо создать набор примеров данных:
(define line0 (cons "hello" (cons "world" '())))
(define line1 '())
(define lls0 '())
(define lls1 (cons line0 (cons line1 '())))

Первые два определения представляют два примера текстовых
строк: первая содержит два слова, а вторая не содержит ни одного.
Последние два определения показывают, как создать экземпляры LLS
из этих примеров строк. Определите ожидаемый результат применения функции к этим двум примерам.
Имея примеры данных, легко сформулировать примеры применения функции; просто представьте, как применить функцию к каж­
дому из примеров данных. Когда words-on-line применяется к lls0,
она должна вернуть пустой список, потому что в нем нет строк. Когда
words-on-line применяется к lls1, она должна вернуть список с двумя
числами, потому что lls1 содержит две строки. Эти два числа равны
2 и 0 соответственно, потому что первая строка в lls1 содержит два
слова, а вторая – ни одного.
Вот как можно перевести все это в тестовые примеры:
(check-expect (words-on-line lls0) '())
(check-expect (words-on-line lls1)
(cons 2 (cons 0 '())))

Выполнив этот второй шаг, вы получите законченную программу,
которая, впрочем, не дает никаких результатов в некоторых тестовых
примерах.
Следующий интересный шаг – разработка макета для этой демонстрационной задачи. Ответив на вопросы из табл. 10, вы без труда получите типичный макет функции, обрабатывающей список:
(define (words-on-line lls)
(cond
[(empty? lls) ...]
[else
(... (first lls) ; список строк
... (words-on-line (rest lls)) ...)]))

Как и в предыдущем разделе, мы знаем, что выражение (first lls)
извлекает список строк List-of-strings, который тоже имеет сложную
организацию. Возникает соблазн вставить вложенный макет для выражения этих знаний, но, как вы наверняка помните, лучше разработать
второй вспомогательный макет и изменить первую строку во втором
условии так, чтобы она ссылалась на этот вспомогательный макет.
Поскольку вспомогательный макет определяет функцию, которая
принимает список, он почти не отличается от предыдущего:
(define (line-processor ln)
(cond

295

296

Глава 10
[(empty? lls) ...]
[else
(... (first ln) ; a string
... (line-processor (rest ln)) ...)]))

Основное отличие заключается в том, что (first ln) извлекает строку из списка, а мы рассматриваем строки как атомарные значения.
Получив этот макет, можно изменить первую строку во втором условии в words-on-line, как показано ниже:
... (line-processor (first lls)) ...

Это напомнит нам на пятом шаге о том, что определение words-online требует спроектировать вспомогательную функцию.
Теперь приступим к программированию. Как всегда, воспользуемся вопросами из табл. 11. Первое условие, обрабатывающее случай,
когда функция получает пустой список строк, – самый простой. Наши
примеры говорят нам, что в этом случае функция должна вернуть '(),
то есть пустой список чисел. Второе условие, обрабатывающее случай, когда функция получает непустой список, содержит несколько
выражений. Давайте вспомним, что они вычисляют:
zz (first lls) извлекает первую строку из непустого списка строк;
zz (line-processor (first lls)) напоминает, что мы должны спроек-

тировать вспомогательную функцию для обработки строки;

zz (rest lls) – оставшаяся часть списка строк;
zz (words-on-line (rest lls)) подсчитывает слова в строках в остав-

шейся части списка. Откуда мы это знаем? Именно это указано
в сигнатуре и в описании назначения функции words-on-line.

Если допустить, что у нас уже есть вспомогательная функция, которая принимает строку и подсчитывает слова в ней (назовем ее words#),
то мы с легкостью можем закончить второе условие:
(cons (words# (first lls))
(words-on-line (rest lls)))

Это выражение объединяет количество слов в первой строке lls со
списком чисел слов, соответствующих строкам в оставшейся части
списка lls.
Осталось спроектировать функцию words#. В макете мы обозначили
ее именем line-processor. Ее цель – подсчитать количество слов в текстовой строке, которая представлена простым списком строк. Итак,
вот запись в нашем списке желаний:
; List-of-strings -> Number
; подсчитывает слова в los
(define (words# los) 0)

Здесь уместно вспомнить пример, использованный в главе 9 для
иллюстрации рецепта проектирования определений данных, ссы­

297

Еще о списках

лаю­щихся на самих себя. Функция в том примере называется how-many, и она тоже подсчитывает количество строк в списке строк. Ее отличие в том, что она обрабатывает список имен, но в данном случае
это не имеет никакого значения; она подсчитывает количество строк
в спис­ке строк, а значит, решает нашу проблему.
Повторное использование функций считается хорошей практикой,
поэтому мы можем определить words# так:
(define (words# los)
(how-many los))

На самом деле в языках программирования уже есть
функции, которые решают такие задачи. В BSL эта функция
называется length, она подсчитывает количество значений
в любом списке, независимо от типов этих значений.
В листинге 34 представлено законченное решение нашей
текущей задачи. Листинг включает два тестовых примера.
Кроме того, вместо отдельной функции words# определение words-on-line вызывает функцию length, которая имеется в языке BSL. Поэкспериментируйте с этим решением
в DrRa­cket и убедитесь, что два тестовых примера охватывают все определение функции.

Возможно, у вас
появится желание
познакомиться
со списком функций,
доступных в BSL.
Назначение некоторых
из них может показаться туманным, но многие
из них пригодятся
для решения будущих
заданий. Использование
таких функций
экономит ваше время,
а не наше.

Листинг 34. Подсчет слов в текстовой строке
;
;
;
;
;

LLS -- одно из значений:
-- '()
-- (cons Los LLS)
интерпретация: список текстовых строк, каждая из которых
представлена списком строк

(define line0 (cons "hello" (cons "world" '())))
(define line1 '())
(define lls0 '())
(define lls1 (cons line0 (cons line1 '())))
; LLS -> List-of-numbers
; подсчитывает количество слов в каждой текстовой строке
(check-expect (words-on-line lls0) '())
(check-expect (words-on-line lls1) (cons 2 (cons 0 '())))
(define (words-on-line lls)
(cond
[(empty? lls) '()]
[else (cons (length (first lls))
(words-on-line (rest lls)))]))

Сделав еще один небольшой шаг, вы получили возможность создать свою первую файловую утилиту:
; String -> List-of-numbers
; подсчитывает слова во всех строках в заданном файле

298

Глава 10
(define (file-statistic file-name)
(words-on-line
(read-words/line file-name)))

Она просто объединяет библиотечную функцию с words-on-line.
Первая читает файл и возвращает его содержимое в виде списка
спис­ков строк, который затем передается второй функции.
Идея объединения встроенной функции со вновь спроектированной широко используется в программировании. Естественно, люди
проектируют функции не случайным образом и стараются использовать функции, уже имеющиеся в выбранном языке программирования. Разработчики программ тщательно планируют свои действия
и проектируют функции в соответствии с данными, которые возвращают имеющиеся функции. Как уже отмечалось выше, решение
обычно воспринимается как композиция двух вычислений и сводится к разработке соответствующего набора данных, с по­мощью которого можно передать результат одного вычисления второму, где каж­
дое вычисление реализуется с по­мощью функции.
Упражнение 172. Спроектируйте функцию collapse, которая преобразует список текстовых строк в строку. При объединении слова
в текстовых строках следует разделять пробелами (" "), а сами текстовые строки – символом перевода строки ("\n").
Листинг 35. Кодирование строк
; 1String -> String
; преобразует данный символ (1String) в трехбуквенную числовую строку
(check-expect (encode-letter "z") (code1 "z"))
(check-expect (encode-letter "\t")
(string-append "00" (code1 "\t")))
(check-expect (encode-letter "a")
(string-append "0" (code1 "a")))
(define (encode-letter s)
(cond
[(>= (string->int s) 100) (code1 s)]
[(< (string->int s) 10)
(string-append "00" (code1 s))]
[(< (string->int s) 100)
(string-append "0" (code1 s))]))
; 1String -> String
; преобразует данный символ (1String) в строку
(check-expect (code1 "z") "122")
(define (code1 c)
(number->string (string->int c)))

Усложненное задание. Когда вы закончите, опробуйте программу, как показано ниже:
(write-file "ttt.dat"
(collapse (read-words/line "ttt.txt")))

Еще о списках

и убедитесь в идентичности файлов «ttt.dat» и «ttt.txt», удалив все
лишние пробелы в вашей версии стихотворения T.T.T. 
Упражнение 173. Спроектируйте программу, удаляющую все артикли из текстового файла. Программа должна принимать имя файла
n, читать файл, удалять артикли и записывать результат в файл с именем, которое является результатом объединения строки «no-article-»
с именем файла n. В этом упражнении под артиклями понимаются
следующие три слова: «a», «an» и «the».
Используйте read-words/line, чтобы сохранить организацию исходного текста при преобразовании в строки и слова. Завершив разработку программы, примените ее к стихотворению Пита Хейна. 
Упражнение 174. Спроектируйте программу, которая кодирует
текстовые файлы числами. Каждая буква в слове должна кодироваться числовой трехбуквенной строкой со значением от 0 до 256. В лис­
тинге 35 показана наша версия функции кодирования букв. Прежде
чем начать, объясните, как работает эта функция.
Подсказки. (1). Используйте read-words/line, чтобы сохранить организацию исходного текста при преобразовании в строки и слова.
(2) Прочитайте описание функции explode. 
Упражнение 175. Спроектируйте программу, имитирующую коман­
ду wc в Unix. Эта команда подсчитывает символы, слова и строки в данном файле. То есть она принимает имя файла и выводит три числа. 
Листинг 36. Транспонирование матрицы
; Matrix -> Matrix
; транспонирует заданную матрицу
(define wor1 (cons 11 (cons 21 '())))
(define wor2 (cons 12 (cons 22 '())))
(define tam1 (cons wor1 (cons wor2 '())))
(check-expect (transpose mat1) tam1)
(define (transpose lln)
(cond
[(empty? (first lln)) '()]
[else (cons (first* lln) (transpose (rest* lln)))]))

Упражнение 176. Возможно, учителя математики уже познакомили вас с матричными вычислениями. Матрица – это прямоугольная
таблица чисел. Вот одно из возможных представлений данных для
матриц:
;
;
;
;

Matrix -- это одно из значений:
-- (cons Row '())
-- (cons Row Matrix)
ограничение: все строки в матрице имеют одинаковую длину

; Row (строка в матрице) -- это одно из значений:
; -- '()
; -- (cons Number Row)

299

300

Глава 10

Обратите внимание на ограничение. Изучите определение данных
и преобразуйте матрицу два на два, состоящую из чисел 11, 12, 21
и 22, в это представление данных. Стоп! Не продолжайте чтения, пока
не разберетесь с примерами данных.
Вот решение этой простой задачи:
(define row1 (cons 11 (cons 12 '())))
(define row2 (cons 21 (cons 22 '())))
(define mat1 (cons row1 (cons row2 '())))

Если к этому решению вы пришли не сами, то внимательно изучите его.
Функция в листинге 36 реализует важную математическую операцию – транспонирование матрицы. Транспонировать означает отра­
зить элементы относительно диагонали, то есть относительно линии,
соединяющей верхний левый угол с правым нижним.
Стоп! Транспонируйте матрицу mat1 вручную, затем прочитайте
листинг 36. Что делает выражение (empty? (first lln)) в функции transpose?
Определение предполагает использование двух вспомогательных
функций:
zz first*, принимает матрицу и возвращает первый столбец в виде

списка чисел;
zz rest*, принимает матрицу и возвращает матрицу без первого
столбца.

Даже не имея определений этих функций, вы должны понимать,
как работает транспонирование. Вы также должны понимать, что эту
функцию нельзя спроектировать с по­мощью рецептов проектирования, которые мы использовали до сих пор. Объясните почему.
Спроектируйте две функции из списка желаний. Затем завершите
проектирование transpose, добавив несколько тестовых примеров. 

10.4. И снова о графическом редакторе
В разделе 5.10 мы спроектировали интерактивный графический однострочный редактор. Там предлагалось два разных способа представления состояния редактора и настоятельно рекомендовалось
изучить оба: структуру, содержащую пару строк, и структуру, объединяющую строку с индексом, соответствующим текущей позиции курсора (см. упражнение 87).
Третья альтернатива – использовать структуры, объединяющие два
списка символов (1String):
(define-struct editor [pre post])
; Editor -- это структура:
; (make-editor Lo1S Lo1S)

301

Еще о списках
; Lo1S -- это одно из значений:
; -- '()
; -- (cons 1String Lo1S)

Прежде чем у вас появятся вопросы, рассмотрим два примера данных:
(define good
(cons "g" (cons "o" (cons "o" (cons "d" '())))))
(define all
(cons "a" (cons "l" (cons "l" '()))))
(define lla
(cons "l" (cons "l" (cons "a" '()))))
; пример данных 1:
(make-editor all good)
; пример данных 2:
(make-editor lla good)

Эти два примера демонстрируют, насколько важно указывать в заголовке интерпретацию. Два поля в структуре Editor четко представляют буквы слева и справа от курсора, однако примеры выше демонстрируют, что существует как минимум два способа интерпретации
структуры:
1) (make-editor pre post) может означать, что буквы в pre предшест­
вуют курсору, а буквы в post следуют за ним и что текст отображается в редакторе как
(string-append (implode pre) (implode post))

Напомним, что implode преобразует список символов (1String)
в строку;

2) (make-editor pre post) также может означать, что буквы в pre

предшествуют курсору в обратном порядке. Если это так, то
текст в редакторе отображается следующим образом:
(string-append (implode (rev pre))
(implode post))

Функция rev должна принимать список символов и переставлять их в обратном порядке.
Даже не имея полного определения функции rev, нетрудно представить, как она работает. Используйте это знание, чтобы убедиться,
что понимаете, что преобразование первого примера данных в информацию в соответствии с первой интерпретацией и преобразование второго примера данных в соответствии со второй интерпретацией приводят к одному и тому же отображению в редакторе:

allgood

302

Глава 10

Обе интерпретации имеют право на жизнь, но, как оказывается, вторая значительно упрощает проект программы. Остальная часть этого
раздела иллюстрирует этот момент, параллельно демонстрируя использование списков внутри структур. Чтобы оценить данный урок по
достоинству, вы должны были выполнить упражнения из раздела 5.10.
Начнем с rev, потому что нам определенно нужна эта функция, чтобы понять смысл определения данных. Она имеет простой заголовок:
; Lo1s -> Lo1s
; возвращает копию входного списка, в которой элементы следуют в обратном порядке
(check-expect
(rev (cons "a" (cons "b" (cons "c" '()))))
(cons "c" (cons "b" (cons "a" '()))))
(define (rev l) l)

Для удобства мы добавили один «очевидный» пример. Вы можете
добавить свои дополнительные примеры, чтобы убедиться, что понимаете, что вам нужно.
Макет функции rev – типичный для функций, обрабатывающих
списки:
(define (rev l)
(cond
[(empty? l) ...]
[else (... (first l) ...
... (rev (rest l)) ...)]))

Здесь мы имеем два предложения, и во втором из них используется
несколько селекторов и ссылок на само определение.
Заполнить первое предложение в макете легко: копией пустого
списка с переупорядоченными элементами является сам пустой список. Для заполнения второго предложения снова используем вопросы:
zz (first l) – это первый элемент списка символов;
zz (rest l) – это остальная часть списка;
zz (rev (rest l)) – это копия остальной части списка с переупоря-

доченными элементами.

Стоп! Попробуйте завершить проектирование rev, используя следующие подсказки.
Таблица 19. Табличный метод для rev
l
(cons "a"
'())
(cons "a"
(cons "b"
(cons "c"
'())))

(first
l)
"a"

(rest
l)
'()

(rev
(rest l))
'()

"a"

(cons "b"
(cons "c"
'()))

(cons "c"
(cons "b"
'()))

(rev l)
(cons "a"
'())
(cons "c"
(cons "b"
(cons "a"
'())))

Еще о списках

Если этих подсказок окажется недостаточно, то создайте таблицу
из примеров. В табл. 19 показаны два примера: (cons "a" '()) и (cons "a"
(cons "b" (cons "c" '()))). Второй пример особенно показателен. Взгляните на предпоследний столбец, он показывает, что основную работу
выполняет применение (rev (rest l)), возвращающее (cons "c" (cons
"b" '())). Так как желаемым результатом является (cons "c" (cons "b"
(cons "a" '()))), то rev должна каким-то образом добавить "a" в конец
результата, полученного рекурсивным вызовом. На самом деле, поскольку (rev (rest l)) возвращает уже преобразованную копию оставшейся части списка, то достаточно будет просто добавить результат
выражения (first l) в его конец. У нас нет функции, которая добавляет
элементы в конец списка, но мы можем добавить ее в список желаний
и завершить определение функции:
(define (rev l)
(cond
[(empty? l) '()]
[else (add-at-end (rev (rest l)) (first l))]))

Вот расширенная запись в списке желаний для функции add-at-end:
; Lo1s 1String -> Lo1s
; создает новый список, добавляя s в конец l
(check-expect
(add-at-end (cons "c" (cons "b" '())) "a")
(cons "c" (cons "b" (cons "a" '()))))
(define (add-at-end l s)
l)

Мы назвали запись «расширенной», потому что она содержит пример, сформулированный в виде теста. Пример заимствован из примеров для rev, и именно он определяет необходимость добавления
новой записи в список желаний. Прежде чем продолжить чтение,
придумайте пример, когда add-at-end получает пустой список.
Поскольку add-at-end тоже является функцией, обрабатывающей
списки, ее макет покажется вам хорошо знакомым:
(define (add-at-end l s)
(cond
[(empty? l) ...]
[else (... (first l) ...
... (add-at-end (rest l) s) ...)]))

Чтобы превратить его в определение функции, вновь воспользуемся вопросами для шага 5 рецепта. Наш первый вопрос: как сформулировать ответ для «базового» случая, то есть первого предложения
в этом макете? Если вы неукоснительно выполняли все предлагаемые
упражнения, то знаете, что его результатом всегда будет выражение
(add-at-end '() s)

303

304

Глава 10

то есть (cons s '()). В конце концов, результат должен быть списком,
а список должен содержать данный символ (1String).
Следующие два вопроса касаются «сложного» случая со ссылкой на
само определение. Мы знаем, во что вычисляются выражения во втором предложении в cond: первое извлекает первый символ из данного
списка, а второе «создает новый список, добавляя s в конец (rest l)».
Описание назначения четко определяет, что должна сделать функция
в этом случае – она должна прибавить (first l) к результату рекурсивного вызова:
(define (add-at-end l s)
(cond
[(empty? l) (cons s '())]
[else
(cons (first l) (add-at-end (rest l) s))]))

Запустите тестовые примеры, чтобы убедиться, что эта функция
работает правильно и, следовательно, правильно работает rev. Вас не
должно удивлять, если вы узнаете, что в BSL уже имеется функция,