Минимизация структур управления
----------------------------------------------------------------
Структуры управления в Форте играют меньшую роль, нежели в других языках. Форт-программисты стараются описывать очень сложные приложения в терминах коротких слов и без особого акцента на конструкции IF THEN. Имеется несколько приемов для минимизации структур управления. Они включают в себя:
* вычисления или подсчет * упрятывание условных операторов при рефакторизации * использование структурированных выходов * перепроектирование.
В этой главе мы исследуем эти приемы с целью упрощения и удаления структур управления из Вашего кода.
ЧТО ЖЕ ТАКОГО ПЛОХОГО В СТРУКТУРАХ УПРАВЛЕНИЯ? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Перед тем, как мы станем разворачивать свой список советов, давайте приостановимся для того, чтобы понять, зачем следует в первую очередь избегать условных операторов. Использование структур условного управления добавляет сложность в Ваш код. Чем он сложнее, тем сложнее Вам будет его читать и поддерживать. Чем из большего числа деталей состоит машина, тем больше шансов у нее сломаться. И тем труднее будет ее чинить.
----------------------------------------------------------------
Мур рассказывает историю:
Недавно я вновь связывался с компанией, для которой мы сделали некую работу несколько лет назад. Они меня вызвали - 238 -
потому, что их программе теперь стукнуло пять лет и она стала очень сложной. Их программисты влезали туда и многое меняли, добавляя переменные состояния и операторы условных переходов. Каждое выражение, которое я держал за простое пять лет назад, теперь стало очень сложным. "Если это, иначе если то, иначе если се" ...и уже только потом простое выражение. Теперь, читая эти фразы, я не могу выделить, что же происходит и почему. Я должен был бы помнить, что означает каждая из переменных, почему она применена именно в этом случае и затем: что происходит, когда данная последовательность происходит - или не происходит. Начиналось все самым невинным образом.
У них был особый случай, о котором надо было позаботиться. Для обеспечения этого они ввели условный оператор в одном месте. Затем они обнаружили, что такой же нужен здесь и здесь. А затем - еще несколько. Каждый шаг по возрастающей лишь добавлял небольшой сумбур в программу. Будучи программистами, они каждый раз превосходили это. Общий эффект был разрушительным. В конце концов у них образовалось с полдюжины флагов. Проверить этот, сбросить его, установить тот и т.д. В качестве результата одного условия нужно было проверять следующие возникающие условия. Они создали логический эквивалент макаронообразного кода вместо того, чтобы использовать возможности структурирования программы. Сложность зашла гораздо дальше того уровня, который они предполагали. Однако они обрекли себя на этот путь, пропустив простое решение, которое все это могло бы сделать ненужным - иметь два слова вместо одного. Вы говорите либо РАБОТАТЬ, либо ДЕЛАТЬ-ВИД. В большинстве приложений имеется очень немного мест, в которых Вам нужно проверять условие. К примеру, в видеоигре Вы в действительности не говорите: "Если нажимают Кнопку А, то сделать это; если нажимают Кнопку Б, то сделать что-нибудь другое". Вы с подобной логикой не связываетесь. Если кнопку нажимают, то Вы что-то исполняете. Это что-то связано именно с кнопкой, а не с логикой. Условные операторы сами по себе не плохи - это конструкции нужные. Но программа с большим количеством таких конструкций запутана и нечитаема. Все, что Вам нужно делать - это по каждой из них задать себе вопрос. Любой условный оператор должен заставить Вас спросить: "Что я делаю неправильно?" С условными операторами можно пытаться совладать различными способами. Длинные вложенные последовательности иного рода лучше длинных вложенных последовательностей условных переходов.
---------------------------------------------------------------- - 239 -
Перед тем, как мы предложим некоторые конкретные приемы, давайте взглянем на три подхода к использованию условных операторов на конкретном примере.
Рисунки 8-1, 8-2 и 8- 3 иллюстрируют три версии проекта автоматической банковской машины. Первый пример вышел прямо из Школы Структурированных Программистов. Логика задачи зависит от правильной вложенности операторов IF.
Рис.8-1. Структурированный подход.
БАНКОМАТ --------
IF карточка правильная DO IF владелец карточки правильный DO IF запрос на оплату DO IF код пароля правильный DO запрос количества IF запрос =< текущего баланса DO IF расход =< допустимого расхода DO оплатить запрос подсчитать дебет ELSE предупреждение конец сеанса ELSE предупреждение конец сеанса ELSE предупреждение конец сеанса ELSE IF код пароля правильный DO запрос количества получить конверт через щель подсчитать кредит ELSE предупреждение конец сеанса ELSE съесть карточку ELSE предупреждение КОНЕЦ - 240 -
Легко читается? Скажите мне, при каком условии пользовательская карточка будет съедена. Для ответа Вам придется либо подсчитать ELSEы от конца и приложить их к тому же количеству IFов от начала, либо использовать линейку. Во второй версии на рис. 8-2 показано улучшение за счет использования множества мелких именованных процедур для удобочитаемости. Карточка пользователя будет съедена, если владелец не тот.
Рис.8-2. Вложенные условные операторы внутри именованных процедур.
БАНКОМАТ --------
PROCEDURE ЧИТАТЬ-КАРТУ IF карточка читается THEN ПРОВЕРИТЬ-ВЛАДЕЛЬЦА ELSE выбросить карточку КОНЕЦ
PROCEDURE ПРОВЕРИТЬ-ВЛАДЕЛЬЦА IF владелец правильный THEN ПРОВЕРИТЬ-КОД ELSE съесть карточку КОНЕЦ
PROCEDURE ПРОВЕРИТЬ-КОД IF введенный код соответствует владельцу THEN ТРАНЗАКТ ELSE предупреждение, конец сеанса КОНЕЦ
PROCEDURE ТРАНЗАКТ IF запрос на оплату THEN РАСХОД ELSE ПРИХОД КОНЕЦ
PROCEDURE РАСХОД запрос IF запрос =< текущий баланс THEN ОПЛАТИТЬ КОНЕЦ
PROCEDURE ОПЛАТИТЬ IF сумма =< допустимый расход THEN оплатить счет подсчитать дебет ELSE предупреждение КОНЕЦ
PROCEDURE ПРИХОД принять конверт подсчитать кредит - 241 -
Однако даже после такого улучшения структура каждого слова полностью зависит от `последовательности`, в которой производятся проверки.
Процедура предположительно "высшего" уровня обременена проверкой на наихудший, самый тривиальный тип события. И каждая из проверок становится ответственной за вызов следующей. Третья версия ближе всего подходит к тому, что может дать Форт. Слово самого высокого уровня точно выражает, что происходит на уровне концептуальном, показывая лишь главный путь. Каждое их подчиненных слов имеет свой собственный выход по ошибке, не засоряющий читабельность главного слова. Одна проверка не обязана вызывать следующую проверку.
Рис.8-3. Перефакторизация и/или устранение условных операторов.
БАНКОМАТ --------
: ПУСК ЧИТАТЬ-КАРТУ ТЕСТ-ВЛАДЕЛЬЦА ТЕСТ-КОДА ТРАНЗАКТ ;
: ЧИТАТЬ-КАРТУ правильная послед-ть кодов НЕ читается IF выбросить карточку QUIT THEN ;
: ТЕСТ-ВЛАДЕЛЬЦА владелец НЕ правильный IF съесть карточку QUIT THEN ;
: ТЕСТ-КОДА введенный код НЕ соответствует коду владельца IF предупреждение QUIT THEN ;
: ЧИТАТЬ-КНОПКУ ( -- адрес-функции-кнопки ) ( аппаратно-зависимый примитив ) ;
: ТРАНЗАКТ ЧИТАТЬ-КНОПКУ EXECUTE ;
1 КНОПКА РАСХОД
2 КНОПКА ПРИХОД
: РАСХОД запрос запрос =< текущего баланса IF ОПЛАТА THEN ; - 242 -
: ОПЛАТА оплата =< допустимого расхода IF оплатить подсчитать дебет ELSE предупреждение THEN ;
: ПРИХОД принять конверт подсчитать кредит ;
Слово ТРАНЗАКТ, кроме того, спроектировано вокруг того факта, что пользователь делает запросы, нажимая кнопки на клавиатуре. Нет необходимости ни в каких условных переходах. Одна кнопка начинает запрос на оплату, а другая - на размещение. Этот подход готов к восприятию позднейших изменений, таких, как добавление возможности делать переводы средств. (И этот подход поэтому становится `не` зависимым от аппаратуры. Детали интерфейса с клавиатурой могут быть сокрыты в лексиконе клавиатуры ЧИТАТЬ-КНОПКУ и КНОПКА.) Разумеется, Форт позволит Вам выбрать любой из трех методов. Который из них Вы предпочитаете?
КАК УСТРАНЯТЬ СТРУКТУРЫ УПРАВЛЕНИЯ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ В этом разделе мы изучим различные способы упрощения или избегания условных операторов.
Большинство из них будет давать более читабельный, лучше приспособленный к сопровождению и более эффективный код. Некоторые из приемов дают более эффективный, но не всегда столь же хорошо читабельный код. Помните поэтому: Не все советы применимы во всех ситуациях.
ИСПОЛЬЗОВАНИЕ СЛОВАРЯ.
------------------------------------------------------------ СОВЕТ Давайте всякой функции свое определение. ------------------------------------------------------------
Правильным образом используя словарь Форта, мы в действительности не сокращаем число условных операторов; мы просто вычленяем их из кода нашего приложения. Словарь Форта - это гигантский оператор CASE со строковыми аргументами. Функции сравнения и исполнения скрыты внутри Форт-системы. - 243 -
----------------------------------------------------------------
Мур:
В моем приходно-расходном пакете, когда Вы получаете от кого-нибудь чек, то пишете сумму, номер чека, слово ОТ и имя этого кого-то:
200.00 127 ОТ АЛИ-БАБА
Слово ОТ заботится о данной ситуации. Если Вы хотите выставить кому-нибудь счет, Вы пишете сумму, номер вызова, слово СЧЕТ и имя:
1000.00 280 СЧЕТ ТЕХНИТЕКС
... По одному слову на каждый случай. Решения принимает словарь.
----------------------------------------------------------------
Это замечание характеризует весь Форт. Для сложения пары чисел одинарной длины мы используем команду +. Для сложения пары чисел двойной длины - слово D+. Менее эффективный, более сложный подход использовал бы единственную команду, которая как-то "разбирала" бы, какие из типов чисел должны быть сложены. Форт эффективен потому, что все эти слова - ОТ и СЧЕТ и + и D+ - могут быть реализованы безо всякой необходимости в проверках и условных переходах.
------------------------------------------------------------ СОВЕТ Используйте "тупые" слова. ------------------------------------------------------------
Это вовсе не рекомендация для телевизионных сценаристов. Это - еще одна ипостась использования словаря. "Тупое" слово - это такое слово, которое не зависит от ситуации, но имеет одно и то же действие все время ("очевидно при ссылках").
В тупом слове нет неопределенности, а потому оно заслуживает большего доверия. Несколько обычных слов в Форте к выходу этой книги являлись источником противостояний. Одним из таких слов является .", которое печатает строку. В простейшем виде оно допустимо только внутри определения через двоеточие:
: ПРОВЕРКА ." ЭТО - СТРОКА. " ; - 244 -
В действительности эта версия слова `не` печатает строку. Она `компилирует` строку вместе с адресом другого определения, которое ее и печатает во время исполнения. Это тупая версия слова. Если Вы используете ее вне определения через двоеточие, оно бессмысленно скомпилирует строку, что совсем не соответствует тому, что ожидает получить новичок. Для решения этой проблемы FIG-Форт добавил проверку внутрь слова .", которая определяла, находится ли система в режиме компиляции или интерпретации. В первом случае оно компилировало строку и адрес примитива, во втром - печатало ее TYPEом. Слово ." стало состоящим из двух совершенно разных слов, поселенных вместе в одном определении внутри структуры IF THEN ELSE. Флаг, определяющий, компилирует или интерпретирует Форт, называется STATE. Поскольку ." зависит от STATE, то говорят, что это слово "STATE-зависимо". Команда `вела себя` одинаково внутри и снаружи определения через двоеточие. Эта двойственность оправдала себя для послеобеденных вводных курсов по Форту, но серьезный студент скоро понял, что это еще не все. Допустим, некая студентка желает написать новое слово по имени Я." (т.е. "яркая-точка-кавычка") для отображения выделенной яркостью строки символов на своем дисплее, с использованием в виде:
." Вставьте диск в " Я." левый " ." привод "
Она могла бы расчитывать определить это так:
: Я." ЯРКО ." НОРМА ;
то есть поменять видеорежим на повышенную яркость, напечатать строку, а затем восстановить режим на нормальный. Она пытается. И немедленно - иллюзия разрушена; обман разоблачен; определение не может работать.
Для решения ее проблемы программистке придется изучить определение слова (.") в своей собственной системе. Я не собираюсь здесь производить отступления по поводу того, как работает (.") - на мой взгляд, красотой оно не выделяется. Между прочим, имеется и другой синтаксический подход к проблеме нашей студентки, а именно тот, который не требует наличия двух раздельных слов ." и Я." для печати строк. Измените системное слово (.") так, чтобы оно всегда устанавливало нормальный режим после печати, даже если он и так бедет нормальным большую часть времени. Имея такой синтаксис, программистке нужно будет просто предварить выделяемую строку простым словом ЯРКО.
." Вставьте диск в " ЯРКО ." левый " ." привод " - 245 -
Стандарт-83 ныне специфицирует тупое определение .", а для случаев, когда необходима интерпретирующая версия добавлено новое слово .(. К счастью, в этом стандарте мы используем словарь для принятия решения с помощью двух раздельных слов. Слово ' (штрих) имеет подобную же историю. В Фиг-Форте оно было STATE-зависимым, но теперь в Стандарте-83 оно тупое. И штрих, и точка-кавычка характерны тем, что программист может захотеть повторно использовать одно из этих слов в высокоуровневом определении и при этом ожидать, что они будут вести себя так же, как и в обычных условиях.
------------------------------------------------------------ СОВЕТ Слова не должны зависеть от переменной STATE, если программист собирается когда-либо использовать их для вызова из высокоуровневого определения и при этом ожидает получить от них такое же поведение, как и при интерпретации. ------------------------------------------------------------
В качестве STATE-зависимого определения хорошо работает слово ASCII, равно как и слово MAKE. (См. приложение В.)
ВЛОЖЕННОСТЬ И КОМБИНАЦИИ УСЛОВНЫХ ОПЕРАТОРОВ.
------------------------------------------------------------ СОВЕТ Не делайте проверок на то, что уже было исключено. ------------------------------------------------------------
Пожалуйста, возьмите такой пример:
: ОБРАБОТАТЬ-КЛАВИШУ KEY DUP СТРЕЛКА-ВЛЕВО = IF КУРСОР-ВЛЕВО THEN DUP СТРЕЛКА-ВПРАВО = IF КУРСОР-ВПРАВО THEN DUP СТРЕЛКА-ВВЕРХ = IF КУРСОР-ВВЕРХ THEN СТРЕЛКА-ВНИЗ = IF КУРСОР-ВНИЗ THEN ;
Эта версия неэффективна, поскольку все проверки должны делаться независимо от исхода любой из них. Если была бы нажата, скажем, клавиша-влево, то не было бы необходимости в проверке ее на другие совпадения. Вместо этого можно было бы сделать вложенные проверки, типа: - 246 -
: ОБРАБОТАТЬ-КЛАВИШУ KEY DUP СТРЕЛКА-ВЛЕВО = IF КУРСОР-ВЛЕВО ELSE DUP СТРЕЛКА-ВПРАВО = IF КУРСОР-ВПРАВО ELSE DUP СТРЕЛКА-ВВЕРХ = IF КУРСОР-ВВЕРХ ELSE КУРСОР-ВНИЗ THEN THEN THEN DROP ;
------------------------------------------------------------ СОВЕТ Компануйте вместе булевские значения одинакового веса. ------------------------------------------------------------
Многие структуры IF THEN двойной вложенности могут быть упрощены с помощью комбинаций флагов и логических операторов перед принятием решения. Вот тестирование с двойной вложенностью:
: ?ГУЛЯТЬ СУББОТА? IF ДЕЛО СДЕЛАНО? IF СМЕЛО ПОЙТИ ГУЛЯТЬ THEN THEN ;
Вышеприведенный код использует вложенные IFы для того, чтобы удостовериться в том, что одновременно и суббота наступила, и дело закончено перед гулянием. Вместо этого давайте скомбиниркем условия логически и произведем единственную проверку:
: ?ГУЛЯТЬ СУББОТА? ДЕЛО СДЕЛАНО? AND IF СМЕЛО ПОЙТИ ГУЛЯТЬ THEN ;
Это проще и лучше читается. При ситуации логического "или" реализация через IF THENы еще ухабистей:
: ?ВСТАВАТЬ ТЕЛЕФОН ЗВОНИТ? IF ВСТАТЬ THEN БУДИЛЬНИК ЗВОНИТ? IF ВСТАТЬ THEN ;
Это куда более элегантно записывается в виде
: ?ВСТАВАТЬ ТЕЛЕФОН ЗВОНИТ? БУДИЛЬНИК ЗВОНИТ? OR IF ВСТАТЬ THEN ;
Из этого правила имеется одно исключение - а именно тогда, когда слишком велик проигрыш в скорости при выполнении проверок. Мы могли бы написать:
: ?ПОХЛЕБКА СТРУЧКИ-БОБОВ? ПОХЛЕБКА РЕЦЕПТ? AND IF ПОХЛЕБКА ПРИГОТОВИТЬ THEN ; - 247 -
Однако предположим, что у нас может занять много времени нашаривание в нашем рецептурном файле чего-нибудь насчет похлебок.
Очевидно, что нет смысла предпринимать поиск, если у нас бобов в запасе нет. Было бы более эффективно написать
: ?ПОХЛЕБКА СТРУЧКИ-БОБОВ? IF ПОХЛЕБКА РЕЦЕПТ? IF ПОХЛЕБКА ПРИГОТОВИТЬ THEN THEN ;
Мы не обеспокоены поисками рецептов при отсутствии бобов. Другое исключение возникает, когда какое-нибудь условие не выпоняется скорее всего. Устраняя его в первую очередь, Вы избегаете необходимости проверять остальные.
------------------------------------------------------------ СОВЕТ Когда многочисленные условия имеют различный вес (в смысле предпочтительности или времени на вычисление), вкладывайте условные операторы так, чтобы на первом уровне был тот, который выполняется наименее вероятно или является самым легким в вычислении. ------------------------------------------------------------
Сложнее пытаться улучшить подобным образом конструкции с OR. К примеру, в определении
: ?ВСТАВАТЬ ТЕЛЕФОН ЗВОНИТ? БУДИЛЬНИК ЗВОНИТ? OR IF ВСТАТЬ THEN ;
мы проверяем и телефон, и будильник, хотя лишь одного из них достаточно, чтобы нас поднять. Теперь предположим, что определять, что звонит будильник, очень трудоемко. Мы могли бы написать
: ?ВСТАВАТЬ ТЕЛЕФОН ЗВОНИТ? IF ВСТАТЬ ELSE БУДИЛЬНИК ЗВОНИТ? IF ВСТАТЬ THEN THEN ;
Если верно первое условие, нам больше не нужно проверять второе. Мы в любом случае вынуждены вставать, чтобы взять трубку. Повторение ВСТАТЬ выглядит уродливо и даже близко не подходит по читабельности решению через OR - однако в некоторых случаях оно предпочтительно. - 248 -
ВЫБОР СТРУКТУР УПРАВЛЕНИЯ ~~~~~~~~~~~~~~~~~~~~~~~~~ ------------------------------------------------------------ СОВЕТ Самый элегантный код - тот, который наиболее точно отражает проблему. Выбирайте структуру управления так, чтобы она наилучшим образом подходила к проблеме передачи управления. ------------------------------------------------------------
ОПЕРАТОРЫ CASE.
Определенный класс задач требует выбора нескольких возможных путей исполнения в соответстсвии с числовым аргументом. К примеру, мы желаем написать слово .МАСТЬ для приема числа, представляющего масть игральной карты, от 0 до 3, и печати имени этой масти.
Можно было бы написать это слово с использованием вложенных операторов IF ELSE THEN, типа:
: .МАСТЬ ( масть -- ) DUP 0= IF ." ЧЕРВИ " ELSE DUP 1 = IF ." ПИКИ " ELSE DUP 2 = IF ." КРЕСТИ " ELSE ." БУБИ " ELSE THEN THEN THEN DROP ;
Мы можем решить эту задачу элегантнее при использовании "оператора CASE". Вот то же самое определение, переписанное с использованием формата "Экеровсокого оператора CASE", названного так по имени Др. Чарльза Э. Экера, джентльмена, его предложившего [1].
: .МАСТЬ ( масть -- ) CASE 0 OF ." ЧЕРВИ " ENDOF 1 OF ." ПИКИ " ENDOF 2 OF ." КРЕСТИ " ENDOF 3 OF ." БУБИ " ENDOF ENDCASE ;
Достоинство оператора CASE состоит исключительно в удобстве его чтения и написания. Он не дает улучшения эффективности ни по занимаемой памяти, ни по скорости исполнения. На самом деле такой оператор компилирует во многом тот же код, что и вложенные операторы IF THEN. Оператор CASE - хороший пример факторизации во время компиляции. Должна ли всякая Форт-система включать в себя этот оператор? Это - палка о двух концах. Во-первых, случаи, когда - 249 -
такая конструкция действительно нужна, редки - достаточно редки, чтобы поставить под вопрос ее ценность. Если встречаются всего несколько таких случаев, конструкция IF ELSE THEN тоже будет неплохо работать, хотя и, быть может, при худшей читабельности. Если таких случаев много, более гибкой является таблица решений. Во-вторых, многие задачи подобного типа не совсем подходят для структуры CASE. Экеровский оператор предполагает, что Вы проверяете на равенство число на стеке. В примере со словом .МАСТЬ у нач имеется непрерывный ряд чисел от 0 до 3. Эффективней было бы использовать целое для вычисления смещения и непосредственного перехода на нужный код. В случае Крошечного Редактора (позже в этой главе) у нас имеются не одно, а два измерения возможностей. Оператор CASE для такой проблемы тоже не подходит. Лично я считаю оператор CASE элегантным решением для неправильно поставленной задачи: попыткой алгоритмического выражения того, что более точно описывается таблицей решений.
Оператору CASE следует быть частью задачи тогда, когда он бывает полезен, но не частью системы.
ЦИКЛИЧЕСКИЕ СТРУКТУРЫ.
Правильно выбранная структура цикла может устранить дополнительные условные операторы.
----------------------------------------------------------------
Мур:
Во многих случаях условные структуры используются для выхода из циклов. Эту конкретную ситуацию можно избежать, строя циклы с многочисленными точками выхода. Это тема весьма жизненна, поскольку множественные конструкции WHILE имеются в polyFORTHе, хотя и не дошли до Форта '83. Они являются простым способом определения многих WHILEов при одном REPEATе. Также и Дин Сендерсон (из фирмы FORTH, Inc.) изобрел новую конструкцию, которая дает две точки выхода для цикла DO LOOP. Имея такую конструкцию, Вы делаете меньше проверок. Очень часто я держу на стеке значение "истина", а если покидаю цикл рано, то меняю это значение, чтобы знать, что я покинул его рано. Затем позже я использую IF для проверки того, когда я покинул цикл, и это очень неуклюже. Если однажды решение принято, не следует принимать его опять. Имея подходящие конструкции для циклов, Вам не придется помнить, откуда Вы пришли, и большее количество проверок будет устранено. - 250 -
Это не совсем популярно, потому что слегка неструктурировано. Или, может быть, чрезмерно структурировано. Ценно то, что программы получаются проще. И это ничего не стоит.
----------------------------------------------------------------
Разумеется, проблема эта жизненна. Здесь, наверное, слишком рано было бы предлагать какие-то определенные новые конструкции циклов. Проверьте документацию на Вашу систему на предмет того, что в ней предлагается из экзотических цикловых структур. Или, по нуждам Ваших задач, добавьте свои собственные структуры. Это не так уж сложно в Форте. Я даже не вполне уверен в том, насколько использование множественных выходов не противоречит доктринам структурированного программирования. В цикле BEGIN WHILE REPEAT с многими WHILEами все выходы приводят к одной точке продолжения: REPEAT.
Однако в конструкции Сендерсона можно выходить из цикла, перепрыгивая `через` конец цикла и продолжая исполнение после ELSE. Имеются две возможные точки продолжения. Это "хуже структурировано", если можно так выразиться. И в то же время определение всегда закончится точкой с запятой и вернет управление вызвавшему его слову. В этом смысле оно хорошо структурировано; модуль имеет одну точку входа и одну точку выхода. Когда Вы хотите выполнить некий код только если `не` покинули цикл преждевременно, использовать такой подход кажется самым естественным. (Мы рассмотрим пример этому в последующем разделе "Использование структурированных выходов".)
------------------------------------------------------------ СОВЕТ Предпочитайте счетчики перед проверками на окончание. ------------------------------------------------------------
Форт поддерживает формат строк, содержащих длину в первом байте. Это упрощает печать, перемещение и вообще любые действия со строками. При адресе и счетчике, лежащих на стеке, определение слова TYPE может быть таким:
: TYPE ( a #) OVER + SWAP DO I C@ EMIT LOOP ;
(Хотя TYPE в действительности обязано быть написанным в машинных кодах.) Это определение не использует впрямую условного оператора. LOOP на самом деле скрывает в себе такой оператор, поскольку каждый цикл проверяет индекс и делает возврат на DO, если он еще не достиг предела. - 251 -
Если бы использовались символы-ограничители, скажем, ASCII нуль, то определение должно было бы быть написано как:
: TYPE ( a) BEGIN DUP C@ ?DUP WHILE EMIT 1+ REPEAT DROP ;
Нужна дополнительная проверка при каждом прохождении цикла. (WHILE является оператором условного перехода.) Замечание по оптимизации: использование слова ?DUP с точки зрения времени исполнения накладно, поскольку оно само содержит дополнительную проверку. Более быстрым было бы определение:
: TYPE ( a) BEGIN DUP C@ DUP WHILE EMIT 1+ REPEAT 2DROP ;
Стандарт '83 применил этот принцип в слове INTERPRET, которое ныне воспринимает счетчик вместо поиска символа-ограничителя.
Оборотной стороной этой медали являются некоторые структуры данных, которые легче `связывать` вместе. Каждая запись указывает на последующую (или предыдущую). Последняя (или первая) запись в цепи может обозначаться нулем в своем поле связи. Раз у Вас есть поле связи, Вам все равно придется работать с его значением. Нет необходимости хранить счетчик того, сколько имеется записей. Если Вы всякий раз уменьшаете счетчик для выяснения момента окончания, то создаете себе этим лишнюю работу. (Такая технология применяется при создании словаря Форта как связного списка.)
ВЫЧИСЛЕНИЕ РЕЗУЛЬТАТОВ.
------------------------------------------------------------ СОВЕТ Не принимайте решений, но вычисляйте. ------------------------------------------------------------
Во многих случаях условные операторы применяются ошибочно тогда, когда различные варианты поведения обусловливаются численными различиями. Раз задействованы числа, то мы их можем посчитать. (См. в главе 4 раздел "Расчеты или структуры данных или логика".)
------------------------------------------------------------
СОВЕТ Используйте логические значения в качестве чисел. ------------------------------------------------------------ - 252 -
Это высказывание замечательно венчает собой предыдущий совет "Не принимайте решений, но вычисляйте". Идея состоит в том, что булевские значения, которые в компьютере представлены числами, могут быть эффективно использованы для улучшения принятия решений. Вот один пример, встречающийся во многих Форт-системах:
: S>D ( n -- d ) \ распространение знака на d DUP 0< IF -1 ELSE 0 THEN ;
(Задачей этого определения является превращение числа одинарной длины в число двойной длины. Это последнее представляется как два 16-разрядных числа на стеке, старшие разряды - на вершине. Превращение положительного целого заключается просто в добавлении нуля на стек для представления этой старшей части. Но преобразование отрицательного требует "расширения знака", то есть старшая часть должна быть представлена всеми единицами.) Вышеприведенное определение проверяет одинарное число на отрицательность.
Если таковая обнаружена, оно кладет на стек минус-единицу; в противном случае - ноль. Но обратите внимание, что выход получается чисто арифметическим; качественных изменений самого процесса нет. Из этого факта можно извлечь выгоду, используя само логическое значение:
: S>D ( n -- d ) \ распространение знака на d DUP 0< ;
Эта версия оставляет на стеке ноль или минус-единицу без принятия решений. (В системах до 83-го стандарта определение должно было бы быть таким):
: S>D ( n -- d ) \ распространение знака на d DUP 0< NEGATE ;
См. приложение В.) С такими "гибридными числами" можно проделывать даже больше:
------------------------------------------------------------ СОВЕТ Для повышения эффективности формирования числового выхода используйте AND. ------------------------------------------------------------ - 253 -
Для случая структуры, принимающей решение о формировании нуля либо ненулевого числа "n", традиционная фраза
( ? ) IF n ELSE 0 THEN
эквивалентна более простому выражению
( ? ) n AND
Опять же секрет в том, что в системах стандарта '83 "истина" представляется -1 (всеми единичками). Делая AND числу "n" с флагом, мы оставляем либо "n" (все биты остаются), либо "0" (все биты очищаются). Для закрепления - еще пример:
( ? ) IF 200 ELSE 0 THEN
то же самое, что
( ? ) 200 AND
Посмотрите на следующий случай:
n a b < IF 45 + THEN
Фраза либо добавляет 45 к "n", либо нет, в зависимости от соотношения величин "a" и "b". Поскольку "добавить 45 или нет" - это то же, что и "добавить 45 или добавить 0", то различие в двух возможных исходах - чисто числовое. Мы можем позволить себе избежать принятия решения и просто вычислить:
n a b < 45 AND +
----------------------------------------------------------------
Мур:
Выражение "45 AND" работает быстрее, чем IF, и, конечно, более грациозно. Оно проще. Если Вы освоите привычку обращать внимание на точки, в которых вычисляете отличие одного значения от другого, то обычно при выполнении арифметических действий над логическими величинами Вы будете получать тот же результат более чистым образом.
Я не знаю, как это называется. Здесь нет терминологии - просто выполнение арифметики со значениями "истина". Но это совершенно корректно, и когда-нибудь булева алгебра и арифметические выражения к этому приспособятся. - 254 -
В книгах часто встречается множество кусочно-линейных аппроксимаций, неспособных ясно изобразить положение дел. К примеру, выражение: x = 1 для t < 0 x = 0 для t >= 0 было бы эквивалентно t 0< 1 AND как единому, а не кусочному выражению.
----------------------------------------------------------------
Я называю подобные флаги "гибридными величинами", поскольку они - булевские значения (значения "истина"), которые применяются в качестве данных (арифметических величин). Я тоже не знаю, как их еще назвать. Можно сократить также и многочисленные ELSEы (когда оба результата - не нулевые), вычленяя различие между результатами. К примеру,
: ШАГОВИКИ 'ПРОВЕРКА? @ IF 150 ELSE 151 THEN LOAD ;
может быть упрощено до
: ШАГОВИКИ 150 'ПРОВЕРКА? @ 1 AND + LOAD ;
Этот подход работает потому, что, по смыслу, мы хотим загрузить либо блок 150, либо, для тестирования, следующий после него блок.
ЗАМЕЧАНИЕ О ТРЮКАХ.
Такого рода подходы часто клеймятся как "трюкачество". В большой компьютерной индустрии трюки имеют плохую репутацию. Трюк - это просто использование преимуществ некоторых свойств операции. Трюки широко применяются в инженерной области. Дымоходы тянут дым, используя преимущества того факта, что тепло поднимается вверх. Автомобильные шины обеспечивают трение, используя преимущества земного притяжения. Арифметико-логические устройства (АЛУ) пользуются знанием того факта, что вычитание числа - это то же самое, что прибавление его двоичного дополнения. Эти трюки позволяют создавать более простые и эффективные конструкции. Их использование оправдано тем, что предположения, на которых они строятся, безусловно останутся в силе. Использование трюков становится опасным тогда, когда они зависят от чего-то, что вполне может измениться или не защищено упрятыванием информации. - 255 -
Кроме того, трюки трудны для чтения, когда те предположения, на которых они основаны, не понятны или не объяснены. Что до замены условных операторов ANDами, то, когда эта технология становится частью словаря программиста, код может стать даже `более` читабельным. В случае применения трюка, специфичного для конкретного приложения, например, порядка, в котором данные должны размещаться в таблице, в листинге должны быть ясно задокументированы приемы, примененные в трюке.
------------------------------------------------------------ СОВЕТ Используйте MIN и MAX в качестве ограничителей. ------------------------------------------------------------
Предположим, мы хотим уменьшить содержимое переменной ЧИСЛО, но не хотим, чтобы ее значение становилось меньше нуля:
-1 ЧИСЛО +! ЧИСЛО @ -1 = IF 0 ЧИСЛО ! THEN
Это проще записать как
ЧИСЛО @ 1- 0 MAX ЧИСЛО !
В этом случае условный оператор заключен в слове MAX.
ИСПОЛЬЗОВАНИЕ ТАБЛИЦ РЕШЕНИЙ.
------------------------------------------------------------ СОВЕТ Используйте таблицы решений. ------------------------------------------------------------
Мы предложили их во второй главе. Таблица решений - это структура, содержащая либо данные ("таблица данных"), либо адреса функций ("таблица функций"), сгруппированные в соответствии с любым числом измерений. Каждое из измерений представляет все возможные, взаимно исключающие состояния определенного аспекта проблемы. На пересечении "правильных" положений по каждому из измерений лежит нужный элемент: кусок данных или функция, которую надо выполнить. Очевидно, что таблица решений для случая, когда задача живет в нескольких измерениях - это вариант лучший, нежели структура управления. - 256 -
ТАБЛИЦА ДАННЫХ С ОДНИМ ИЗМЕРЕНИЕМ.
Вот пример простой одноразмерной таблицы данных. В нашей задаче имеется флаг по имени 'ШОССЕ?, который находится в "истине", когда мы имеем в виду загородные шоссе, и во "лжи", когда это - улицы города. Давайте построим слово ОГРАНИЧЕНИЕ-СКОРОСТИ, возвращающее скоростной предел, зависящий от текущего состояния.
Используя IF THEN, мы могли бы написать:
: ОГРАНИЧЕНИЕ-СКОРОСТИ ( -- ограничение-скорости ) 'ШОССЕ? @ IF 55 ELSE 25 THEN ;
Мы можем устранить IF THEN, используя гибридное значение вместе с AND:
: ОГРАНИЧЕНИЕ-СКОРОСТИ 25 'ШОССЕ? @ 30 AND + ;
Но такой метод не отвечает нашей концептуальной модели задачи и по этой причине не очень удобочитаем. Давайте попробуем таблицу данных. Она имеет одно измерение и только два элемента, так что она не очень большая:
CREATE ПРЕДЕЛЫ 25 , 55 ,
Слово ОГРАНИЧЕНИЕ-СКОРОСТИ? должно теперь использовать булевское значение для подсчета смещения в таблице данных:
: ОГРАНИЧЕНИЕ-СКОРОСТИ ( -- ограничение-скорости ) ПРЕДЕЛЫ 'ШОССЕ? @ 2 AND + @ ;
Достигли ли мы чего-нибудь по сравнению с использованием IF THEN? Видимо, для такой простой задачи, нет. Что мы все-таки сделали, так это отделили процесс принятия решения от самих данных. Это лучше окупается, если мы имеем более, чем один набор данных для одного решения. Предположим, мы имели бы еще
CREATE #ПОЛОС 4 , 10 ,
представляющее число полос на городской улице и на магистральном шоссе. Мы можем использовать идентичный код для вычисления текущего числа полос:
: #ПОЛОС? ( -- #полос) #ПОЛОС 'ШОССЕ? @ 2 AND + @ ; - 257 -
Применяя технику факторизации, мы упрощаем это до:
: У-ДОРОГИ ( для-шоссе для-города ) CREATE , , DOES> ( -- данные) 'ШОССЕ? @ 2 AND + @ ;
55 25 У-ДОРОГИ ОГРАНИЧЕНИЕ-СКОРОСТИ? 10 4 У-ДОРОГИ #ПОЛОС?
ТАБЛИЦА ДАННЫХ С ДВУМЯ ИЗМЕРЕНИЯМИ.
Во второй главе мы представили задачу вычисления платы за телефон. На рис. 8-4 показано решение задачи с использованием двухмерной таблицы данных.
Рис.8-4. Решение задачи о плате за телефон.
\ Телефонные тарифы 03/30/84 CREATE ПОЛНЫЙ 30 , 20 , 12 , CREATE СРЕДНИЙ 22 , 15 , 10 , CREATE НИЗКИЙ 12 , 9 , 6 , VARIABLE ТАРИФ \ показывает на ПОЛНЫЙ, СРЕДНИЙ или НИЗКИЙ \ в зависимости от времени суток ПОЛНЫЙ ТАРИФ ! \ к примеру : ПЛАТА ( о -- ) CREATE , DOES> ( -- плата ) @ ТАРИФ @ + @ ; 0 ПЛАТА 1МИНУТА \ плата за первую минуту 2 ПЛАТА +МИНУТА \ плата за каждую дополнительную минуту 4 ПЛАТА /МИЛИ \ плата за каждые 100 миль
\ Телефонные тарифы 03/30/84 VARIABLE ОПЕРАТОР? \ 90, если помогал оператор, иначе 0 VARIABLE #МИЛЬ \ сотни миль : ?ПОМОЩЬ ( прямой-вызов плата -- полная-плата) ОПЕРАТОР? @ + ; : РАССТОЯНИЕ ( -- плата ) #МИЛЬ @ /МИЛЬ * ; : ПЕРВАЯ ( -- плата ) 1МИНУТА ?ПОМОЩЬ РАССТОЯНИЕ + ; : ДОПОЛНИТЕЛЬНАЯ ( -- плата ) +МИНУТА РАССТОЯНИЕ + ; : СУММА ( #минут -- полная-плата) 1- ДОПОЛНИТЕЛЬНАЯ * ПЕРВАЯ + ;
В этой задаче каждое из измерений таблицы данных состоит из трех взаимно исключающих состояний. Поэтому простое булевское число (истина/ложь) здесь не подходит. Каждое измерение задачи реализовано особым образом. - 258 -
Текущий тариф, который зависит от времени дня, хранится как адрес, представляющий одну из трех подтаблиц структуры тарифов. Мы можем сказать:
ПОЛНЫЙ ТАРИФ !
или
НИЗКИЙ ТАРИФ !
и т.д. Текущая плата, будь то за первую минуту, за последующие или за расстояние, выражается как смещение в таблице (0, 2 или 4). Замечание по оптимизации: мы разработали двухразмерную таблицу как набор из трех одноразмерных, указываемых ТАРИФом. Этот метод устраняет надобность в умножении, которое иначе бы потребовалось для построения структуры с двумя измерениями. В определенных случаях умножение может работать недопустимо медленно.
ТАБЛИЦА РЕШЕНИЙ С ДВУМЯ ИЗМЕРЕНИЯМИ.
Мы возвращаемся вновь к нашему примеру Крошечного Редактора из третьей главы для иллюстрации таблицы решений о двух измерениях. На рисунке 8-5 мы конструируем таблицу функций, которые следует запускать, когда нажимаются различные клавиши. Результат то же, что и при применении оператора CASE, но только для двух имеющихся режимов - нормального и режима вставки. Каждая клавиша имеет различное поведение в зависимости от текущего режима. Первый блок определяет смену режимов. Если мы вызываем
НОРМАЛЬНЫЙ РЕЖИМ# !
то переходим в нормальный режим.
ВСТАВОЧНЫЙ РЕЖИМ# !
вводит режим вставки. Следующий блок строит таблицу функций по имени ФУНКЦИИ. Она состоит из кода ASCII клавиши, за которым следует адрес программы, вызываемой в нормальном режиме, а дальше - адрес программы, вызываемой в режиме вставки, когда на эту клавишу нажимают.
После этого идет следующая клавиша со своей парой адресов, и так далее. - 259 -
На третьем блоке слово 'ФУНКЦИИ берет значение клавиши, ищет его в таблице ФУНКЦИИ, а затем возвращает адрес ячейки, отвечающей найденному коду. (Мы предустанавливаем значение переменной СОВПАЛ на точку последней строки из таблицы - на те функции, которые надо выполнить, когда нажата `любая` клавиша.) Слово ДЕЙСТВИЕ вызывает 'ФУКНЦИИ, затем добавляет содержимое переменной РЕЖИМ#. Поскольку эта переменная содержит либо 2, либо 4, добавляя такое смещение, мы указываем внутри таблицы на программу, которую надо исполнить. Простое
@ EXECUTE
ее выполнит (или слово @EXECUTE или PERFORM, если оно есть в Вашей системе). В Фиг-Форте надо изменить определение слова ЕСТЬ так:
: ЕСТЬ [COMPILE] ' CFA , ;
Рис.8-5. Реализация Крошечного Редактора.
Блок # 30 0 \ Крошечный редактор 1 2 CONSTANT НОРМАЛЬНЫЙ \ смещение в ФУНКЦИях 2 4 CONSTANT ВСТАВОЧНЫЙ \ " 3 6 CONSTANT /КЛ \ байтов в таблице на каждую клавишу 4 VARIABLE РЕЖИМ# \ текущее смещение в таблице 5 НОРМАЛЬНЫЙ РЕЖИМ# ! 6 : ВЫКЛ-ВСТАВКУ НОРМАЛЬНЫЙ РЕЖИМ# ! ; 7 : ВКЛ-ВСТАВКУ ВСТАВОЧНЫЙ РЕЖИМ# ! ; 8 9 VARIABLE ВЫХОД? \ t=время выходить из цикла 10 : ВЫХОД TRUE ВЫХОД? ! ; 11 12 13 14 15 - 260 -
Блок # 31 0 \ Крошечный редактор таблица функций 1 : ЕСТЬ ' , ; \ функция ( -- ) ( для стандарта '83) 2 CREATE ФУНКЦИИ 3 \ клавиши нормальный втставочный 4 4 , ( Ctrl-D) ЕСТЬ СТЕРЕТЬ ЕСТЬ ВЫКЛ-ВСТАВКУ 5 9 , ( Ctrl-I) ЕСТЬ ВКЛ-ВСТАВКУ ЕСТЬ ВЫКЛ-ВСТАВКУ 6 8 , ( забой ) ЕСТЬ НАЗАД ЕСТЬ ВСТАВИТЬ< 7 60 , ( стрелка влево) ЕСТЬ НАЗАД ЕСТЬ ВЫКЛ-ВСТАВКУ 8 62 , ( стрелка вправо) ЕСТЬ ВПЕРЕД ЕСТЬ ВЫКЛ-ВСТАВКУ 9 27 , ( выход) ЕСТЬ ВЫХОД ЕСТЬ ВЫКЛ-ВСТАВКУ 10 0 , ( нет совпадения) ЕСТЬ ЗАМЕСТИТЬ ЕСТЬ ВСТАВИТЬ 11 HERE /КЛ - CONSTANT 'НЕ \ адрес клавиши несовпадения 12 13 14 15
Блок # 32 0 \ Крошечный редактор продолж. 1 VARIABLE СОВПАЛ 2 : 'ФУНКЦИИ ( клавиша -- адр-совпадения ) 3 'НЕ СОВПАЛ ! 'НЕ ФУНКЦИИ 4 DO DUP I @ = IF I СОВПАЛ ! LEAVE THEN /КЛ 5 +LOOP DROP СОВПАЛ @ ; 6 : ДЕЙСТВИЕ ( клавиша ) 'ФУНКЦИИ РЕЖИМ# @ + @ EXECUTE ; 7 : ПУСК FALSE ВЫХОД? ! 8 BEGIN KEY ДЕЙСТВИЕ ВЫХОД? @ UNTIL ; 9 10 11 12 13 14 15
В Фортах 79-го стандарта используйте:
: ЕСТЬ [COMPILE] ' , ;
Мы устранили также избыточность при компиляции в определении сразу после таблицы функций:
HERE /КЛ - CONSTANT 'НЕ \ адрес клавиши несовпадения
Мы сделали константу для последней строки в таблице. (В тот момент, когда вызывается HERE, оно указывает на следующую - 261 -
свободную ячечку после последнего элемента таблицы. Последняя строка таблицы начинается на 6 байтов раньше.) Теперь имеются два слова:
ФУНКЦИИ ( адрес начала таблицы функций) 'НЕ ( адрес строки "не-совпадения"; это программы, вызываемые для любой клавиши, которой нет в таблице)
Мы используем эти имена для получения адресов, передаваемых слову DO:
'НЕ ФУНКЦИИ DO
для запуска цикла, который пробегает от начала таблицы до ее конца. Нам неизвестно, сколько строк записано в этой таблице. Мы даже можем добавить или убрать строку в ней без необходимости изменения какого-либо другого места кода, даже того, где производится поиск в таблице. Точно также константа /КЛ упрятывает информацию о количестве колонок в таблице. К несчастью, подход, избранный для реализации слова 'ФУНКЦИИ упрощен и нехорош; в нем используется локальная переменная для уменьшения манипуляций со стеком. Более простое решение без использования локальной переменной:
: 'ФУНКЦИИ ( клавиша -- адр-совпадения ) 'НЕ SWAP 'НЕ ФУНКЦИИ DO DUP I @ = IF SWAP DROP I SWAP LEAVE THEN /КЛ +LOOP DROP ;
(Мы предложим другое решение попозже в этой главе, в "Использовании структурированных выходов".)
ТАБЛИЦЫ РЕШЕНИЙ ДЛЯ ПОВЫШЕНИЯ СКОРОСТИ.
Мы установили, что если можно вычислить значение вместо поиска его по таблице, то так и следует делать. Исключение составляет случай, когда требования по скорости оправдывают большую сложность таблицы. Вот пример вычисления степеней двойки с восьмиразрядной точностью:
CREATE ДВОЙКИ 1 C, 2 C, 4 C, 8 C, 16 C, 32 C, : 2** ( n -- 2-в-степени-n ) ДВОЙКИ + C@ ; - 262 -
Вместо вычисления ответа при помощи умножения двойки на саму себя "n" раз, все такие ответы вычислены заранее и записаны в таблицу.
Можно просто добавлять смещение в таблице и получать результат. В целом сложение работает гораздо быстрее умножения.
----------------------------------------------------------------
Мур приводит другой пример:
Если Вам требуется подсчитывать кусочные функции, скажем, для графического дисплея, то обычно для этого высокого разрешения не нужно. Скорее всего, 7-ми битной функции будет совершенно достаточно. Табличный просмотр 128-ми чисел происходит быстрее, чем что бы то ни было другое из того, что Вы способны применить. Для вычисления низкочастотных функций таблицы решений превосходны. Однако если Вы вынуждены интерполировать, это значит, что приходится все равно вычислять функцию. Скорее всего, будет лучше подсчитать чуть более сложную функцию, избегая таким образом просмотра таблицы.
----------------------------------------------------------------
ПЕРЕПРОЕКТИРОВАНИЕ.
------------------------------------------------------------ СОВЕТ Одно изменение внизу способно предотвратить десять решений наверху. ------------------------------------------------------------
В нашем интервью с Муром в начале главы мы отметили, что большое количество проверок могло бы быть устранено из задачи, елси перепроектировать ее так, чтобы можно было применять два слова вместо одного: "Либо говоришь РАБОТАТЬ, либо ДЕЛАТЬ-ВИД". Легче следовать простому, содержательному алгоритму при изменении контекста его окружения, чем выбирать из нескольких алгоритмов при одинаковом окружении. Припомните наш пример слова ЯБЛОКИ из первой главы. Изначально оно было определено как переменная; затем на него по всей задаче была сделана масса ссылок из слов, которые увеличивали количество яблок (когда приходили новые партии), уменьшали его (когда яблоки продавались) и проверяли их текущее количество (при инвентаризации). - 263 -
Когда стало необходимо поддерживать второй сорт яблок, `неправильный` подход состоял бы в том, чтобы добавить сложность во все слова по поставкам/продажам/проверкам. `Правильный` же подход был тот, который мы на самом деле выбрали: добавить сложность "внизу", то есть в сами ЯБЛОКИ.
Этот принцип может быть осуществлен многими способами. В главе 7 (в "Таблице состояний") мы употребляли таблицы состояний для реализации слов РАБОТАТЬ и ДЕЛАТЬ-ВИД, которые меняли значение группы переменных. Позже в этой главе мы использовали векторизованное исполнение для определений ВИДИМЫЙ и НЕВИДИМЫЙ с целью изменения значений слов TYPE', EMIT', SPACES' и CR' и легкого управления всем форматирующим кодом, который их использует.
------------------------------------------------------------ СОВЕТ Не делайте проверок для чего-то, что не может произойти. ------------------------------------------------------------
Много современных программистов осчастливлены задачей проверки ошибок. У функции нет необходимости проверять аргументы, передаваемые ей другим компонентом системы. Вызывающая программа должна сама нести ответственность за обеспечение нужных границ для вызываемого компонента.
------------------------------------------------------------ СОВЕТ Перепроверьте алгоритм. ------------------------------------------------------------
----------------------------------------------------------------
Мур:
Большое количество условных операторов возникают из-за беспорядочного мышления при решении задачи. В теории сервоуправления многие думают, что алгоритм управления должен быть разным в зависимости от того, велико расстояние или мало. Издалека Вы работаете в чуть живом режиме, поближе - в замедленном, совсем близко - в торопливом. Приходится проверять свое удаление для выяснения того, какой из алгоритмов избрать. Я выработал нелинейный алгоритм сервоуправления, который поддерживает весь этот диапазон. Этот подход устраняет перескоки в точках перехода из одного режима в другой. Он также убирает логику, нужную для принятия решения об - 264 -
алгоритме. Он освобождает Вас от необходимости эмпирически определять эти переходные точки. И, конечно, Ваша программа значительно упрощается, когда вместо трех алгоритмов применяется один. Вместо того, чтобы пытаться избавиться от условных операторов, лучше проверить теорию, которая приводит к возникновению этих операторов.
----------------------------------------------------------------
------------------------------------------------------------ СОВЕТ Остерегайтесь проверок на специальные случаи. ------------------------------------------------------------
Один из примеров мы упомянули в этой книге раньше: если Вы обеспечиваете непопадание пользователя в беду, то Вам не требуется постоянно проверять, не попал ли он все-таки в эту беду.
----------------------------------------------------------------
Мур:
Другой хороший пример - это написание ассемблеров. Очень часто, даже если код операции может и не иметь ассоциированного с ним номера регистра, можно многое упростить, делая вид, что такой регистр есть - скажем, Регистр 0. Решение упрощается, когда производятся арифметические действия над ненужными битовыми полями. Просто запишите в них нули и продолжайте вычисления, которые Вы могли бы избежать, делая проверки на ноль и обходя их. Это - другая ипостась принципа "безразличности". Если Вам безразлично, то дайте безразличное число и используте его.
----------------------------------------------------------------
Каждый раз, когда у Вас возникает особая ситация, попытайтесь найти алгоритм, для которого эта ситация становится нормальной.
------------------------------------------------------------ СОВЕТ Используйте свойства компонента. ------------------------------------------------------------ - 265 -
Хорошо спроектированный компонент - аппаратный или программный - позволит Вам разработать соответствующий лексикон в чистой и эффективной манере. Набор символьной графики старого принтера Epson MX-80 (хотя ныне и устаревший) хорошо иллюстрирует эту точку зрения. На рисунке 8-6 показаны графические символы, производимые кодами ASCII от 160 до 233.
Рис.8-6. Набор символьной графики Epson MX-80.
0 0 1 0 0 1 1 1 0 0 1 1 0 0 0 0 0 0 0 0 1 0 . . . 1 1 0 0 0 0 0 0 0 0 0 0 1 1
Каждый из графических символов является различной комбинацией шести квадратиков, либо заполненных, либо пустых.
Предположим, в нашей задаче мы хотим использовать эти символы для создания рисунка. Мы знаем для каждого символа, что хотим иметь в каждой из шести позиций - и должны выдать подходящий ASCII-код на принтер. Не нужно слишком долго приглядываться для понимания того, что здесь применена очень четкая последовательность. Пусть мы имеем шестибайтную таблицу, в которой каждый байт представляет пиксел в последовательности:
+-----------+ ПИКСЕЛЫ | 0 | 1 | |-----+-----| | 2 | 3 | |-----+-----| | 4 | 5 | +-----------+
и пусть каждый байт содержит шестнадцатеричное FF, если он "включен", ноль - если "выключен", тогда вот как мало надо кода, чтобы вычислить нужный символ:
CREATE ПИКСЕЛЫ 6 ALLOT : ПИКСЕЛ ( i -- a ) ПИКСЕЛЫ + ; : СИМВОЛ ( -- граф-символ ) 160 6 0 DO I ПИКСЕЛ C@ I 2** AND + LOOP ;
(Мы ввели слово 2** несколько советов назад.) Нет необходимости принимать какие-нибудь решения в определении слова СИМВОЛ. Этот последний просто вычисляется. - 266 -
Замечание: для использования того же алгоритма при переводе шести смежных точек на большую ось мы можем просто переопределить слово ПИКСЕЛ. Это пример добавления перенаправления задним числом и хорошей декомпозиции. К сожалению, внешние компоненты не всегда хорошо спроектированы. К примеру, персональный компьютер IBM использует в своем дисплее аналогичную схему графических символов, но без какой-нибудь выраженной взаимосвязи между кодами ASCII и наборами точек. Единственный способ найти нужный символ - это сравнение наборов в просмотровой таблице.
----------------------------------------------------------------
Мур:
Ассемблер для 68000 - это еще один пример, об который можно сломать голову в поисках того, как бы получше выразить эти коды операций минимальным набором команд. По всем признакам, хорошего решения нет. Люди, проектировавшие 68000, вообще не думали ни о каких ассемблерах. А ведь они могли бы гораздо облегчить положение вещей, причем задаром.
----------------------------------------------------------------
Если свойства компонента использются подобным образом, то код становится зависимым от этих свойств и от самого компонента. Впрочем, это простительно, поскольку весь зависимый код собран в единый лексикон, который может быть при необходимости легко изменен.
ИСПОЛЬЗОВАНИЕ СТРУКТУРИРОВАННЫХ ВЫХОДОВ.
------------------------------------------------------------ СОВЕТ Используйте структурированные выходы. ------------------------------------------------------------
В главе по факторизации мы демонстрировали возможность вычленения структур управления при помощи следующего приема:
: УСЛОВНО A Б OR В AND IF NOT R> DROP THEN ; : АКТИВНЫЙ УСЛОВНО БЕСИТЬСЯ РЕЗВИТЬСЯ ПРЫГАТЬ ; : ПАССИВНЫЙ УСЛОВНО СИДЕТЬ ЕСТЬ СПАТЬ ;
Форт позволяет нам менять поток управления, непосредственно манипулируя стеком возвратов. (Для устранения сомнений см. - 267 -
"Начала программирования...", гл. 9.) Неосмотрительное использование этого трюка может привести к неструктурированному коду с неприятными побочными эффектами. Однако дисциплинированное использование структурированных выходов может в действительности упростить код и таким образом улучшить его читабельность и управляемость.
----------------------------------------------------------------
Мур:
Все больше и больше мне нравится применять R> DROP для изменения потока управления. По эффекту это похоже на слово ABORT", котрое имеет встроенную конструкцию IF THEN. Но ведь там всего один IF THEN для всей системы, а не для каждой ошибки. Я либо делаю выход, либо не делаю. Если я его делаю, то мне незачем проделывать свой остальной путь до конца. Я урезаю все целиком. Альтернативой является обременение всей остальной задачи проверками на возникновение ошибки. Это неудобно.
----------------------------------------------------------------
"Аварийный выход" обманывает нормальный путь потока управления при определенных условиях. Форт дает эту возможность при помощи слов ABORT" и QUIT. "Структурированный выход" расширяет эту концепцию, позволяя немедленное прекращение выполнения одного слова, без завершения задачи в целом.
Эта технология не должна путаться с использованием GOTO, которое неструктурировано донельзя. При помощи GOTO можно перейти куда угодно, внутрь или наружу текущего модуля. При нашем же методе Вы эффективно перепрыгиваете прямо на точку выхода из модуля (определения через двоеточие) и продолжаете исполнение вызывающего слова. Слово EXIT заканчивает определение, в котором он появляется. Фраза R> DROP завершает определение, которое `вызвало` то определение, в котором эта фраза звучит; таким образом, оно производит тот же эффект, только может использоваться на один уровень ниже. Вот несколько примеров для обоих подходов. Если у Вас имеется фраза с IF ELSE THEN, в которой после THEN больше ничего нет, типа:
... ГОЛОДЕН? IF С'ЕСТЬ ELSE ЗАКОПАТЬ THEN ;
то можно удалить ELSE, используя EXIT:
... ГОЛОДЕН? IF С'ЕСТЬ EXIT THEN ЗАКОПАТЬ ; - 268 -
(если условие истинно, мы едим и продолжаем; слово EXIT работает наподобие ;. Если условие ложно, мы перепрыгиваем на THEN и ЗАКОПАТЬ.) Использование EXIT здесь более эффективно, оно экономит два байта и необходимость исполнения дополнительного кода, но оно же и ухудшает читабельность.
----------------------------------------------------------------
Мур комментирует ценности и опасности этой технологии:
Когда работаешь с условными операторами, бывает особенно удобным выскакивать из середины без переходов на все эти THENы в конце. В одной задаче у меня было слово, выглядевшее так:
: ПРОВЕРКА ПРОСТОЙ 1УСЛОВИЕ IF ... EXIT THEN 2УСЛОВИЕ IF ... EXIT THEN 3УСЛОВИЕ IF ... EXIT THEN ; ПРОСТОЙ использовался для простых случаев, он заканчивался на R> DROP. Остальные условия были более сложными. Все выходят на ту же точку без необходимости болезненного прохождения через все IFы, ELSEы и THENы. В конечном результате, если ни одно из условий не выполнялось, стоял выход на ошибку. Это был плохой код, трудный в отладке. Однако он отобразил природу проблемы. Не было никакой лучшей схемы для его реализации. EXIT и R> DROP, по крайней мере, позволяли сохранить контроль над обстановкой.
----------------------------------------------------------------
Программисты иногда используют EXIT также для того, чтобы изящным образом выбираться из сложных циклов, использующих BEGIN. Или можно было бы использовать родственный метод для цикла DO LOOP, который мы использовали в слове 'ФУНКЦИИ в нашем Крошечном Редакторе раньше в этой главе. В этом слове мы просматриваем последовательность ячеек в поисках совпадения. Если мы таковое находим, то хотим вернуть адрес совпадения; если же не находим, то желаем иметь адрес последней строки в таблице функций. Можно предложить слово LEAP (см. приложение В), которое будет работать как EXIT (будет имитировать ;). Теперь можно написать:
: 'ФУНКЦИИ ( клавиша -- адр-совпадения ) 'НЕ ФУНКЦИИ DO DUP I @ = IF DROP I LEAP THEN /КЛ +LOOP DROP 'НЕ ; - 269 -
Если совпадение находится, мы делаем LEAP, но не через +LOOP, а прямо из определения, оставляя I (адрес, по которому произошло совпадение) на стеке. Если оно не находится, мы проходим через ограничитель цикла и выполняем
DROP 'НЕ
что сбрасывает номер клавиши, которую искали, а затем оставляет адрес последней строки! Как мы увидели, встречаются случаи, когда преждевременный выход хорошо подходит, и хороши даже многочисленные точки выхода и продолжения. Следует, однако, помнить, что использование EXIT и R> DROP в строгом смысле `не есть следование` принципам структурированного программирования и требует огромной осторожности. К примеру, у Вас на стеке может быть число, которое в конце определения поглощается. Ранний EXIT оставит нежелательное число на стеке. Забавы со стеком возвратов сродни играм с огнем. Можно обжечься. Но зато иметь огонь так здорово!
СКОРОСТНАЯ РАБОТА.
------------------------------------------------------------ СОВЕТ Выполняйте действие сразу, как только узнаете, что его выполнить необходимо, но не позже. ------------------------------------------------------------
Всякий раз, когда Вы устанавливаете флаг, спросите себя, зачем. Если ответ состоит в том, "чтобы знать, что мне надо сделать то-и-другое попозже", то задумайтесь, нельзя ли сделать то-и-другое `теперь`.
Небольшое изменение структуры может существенно упростить Вашу разработку.
------------------------------------------------------------ СОВЕТ Не откладывайте на время исполнения того, что можете сделать сегодня. ------------------------------------------------------------
Когда у Вас есть возможность принять решение до компиляции, принимайте его. Предположим, у Вас два варианта массивов: один с проверкой границ для стадии отладки, а другой, работающий быстрее, но без проверок, для реальной задачи. - 270 -
Держите эти две версии на разных блоках. При компиляции задачи загружайте только нужную Вам версию. Кстати, если Вы последуете этому предложению, то, видимо, замучаетесь убирать и добавлять скобочки в блоках загрузки для выбора всякий раз нужной версии. Вместо этого пишите определения, которые принимают решения за Вас. Пример (уже рассмотренный в другом контексте):
: ШАГОВИКИ 150 'ПРОВЕРКА? @ 1 AND + LOAD ;
------------------------------------------------------------ СОВЕТ Делайте с флагом действие "DUP", а не повторяйте его вычисление. ------------------------------------------------------------
Иногда флаг нужен для того, чтобы показать, был или не был исполнен предыдущий участок кода. Вот определение, которое оставляет флаг, означающий, что СДЕЛАТЬ-ЭТО было исполнено:
: Я-СДЕЛАЛ? ( t=сделал) Я-ДОЛЖЕН? IF СДЕЛАТЬ-ЭТО TRUE ELSE FALSE THEN ;
Это можно упростить до:
: Я-СДЕЛАЛ? ( t=сделал) Я-ДОЛЖЕН? DUP IF СДЕЛАТЬ-ЭТО THEN ;
------------------------------------------------------------ СОВЕТ Не устанавливайте флаг, устанавливайте данные. ------------------------------------------------------------
Если единственной целью установки флага является возможность для некоторого кода выбрать одно из двух чисел, то лучше устанавливать само такое число. Пример "цветов" в шестой главе из раздела "Критерии для фрагментации" иллюстрирует этот тезис. Задачей слова СВЕТЛО является установка флага, который показывает, не хотим ли мы установить бит повышенной яркости.
Почему бы нам было не написать
: СВЕТЛО TRUE 'СВЕТ? ! ;
для установки флага, и
'СВЕТ @ IF 8 OR THEN ... - 271 -
для его использования? Этот подход не столь прост, как элементарная запись в переменную битовой маски для интенсивности:
: СВЕТЛО 8 'СВЕТ? ! ;
и затем ее использование в виде
'СВЕТЛО @ OR ...
------------------------------------------------------------ СОВЕТ Не устанавливайте флаг, устанавливайте функцию. (Векторизуйте.) ------------------------------------------------------------
Этот совет аналогичен предыдущему и имеет тот же ареал обитания. Если единственной целью записи флага является возможность в будущем для кода принять решение между одной или другой функцией, то лучше записывать адрес самой функции. К примеру, код для передачи символа на принтер отличен от того, который выкидывает его на видеодисплей. В плохой программе было бы написано:
VARIABLE УСТРОЙСТВО ( 0=видео | 1=принтер ) : ВИДЕО FALSE УСТРОЙСТВО ! ; : ПРИНТЕР TRUE УСТРОЙСТВО ! ; : TYPE ( a # -- ) УСТРОЙСТВО @ IF ( ... код для принтера ... ) ELSE ( ... код для дисплея .... ) THEN ;
Это плохо потому, что Вы принимаете рещение каждый раз, когда печатаете строку. Предпочтительное решение должно использовать векторизованное исполнение, например:
DOER TYPE ( a # -- ) : ВИДЕО MAKE TYPE ( ... код для дисплея ... ) ; : ПРИНТЕР MAKE TYPE ( ... код для принтера ... ) ;
Это лучше, поскольку слову TYPE не надо каждый раз решать, какой из кодов использовать, оно это уже знает. (В многозадачной системе задачи для принтера и для монитора будут иметь каждая свою собственную копию исполнительного вектора для TYPE, хранимую в пользовательской переменной.) Вышеприведенный пример показывает также и ограничения для этого совета. В нашей второй версии у нас не осталось простого - 272 -
способа узнать, какое установлено текущее устройство - принтер или дисплей. Нам это может быть нужно, например, для того, чтобы решить, очищать ли экран или выдавать символ перевода формата. Мы используем знание о состоянии повторно, поэтому наше правило более неприменимо.
Флаг позволяет сделать простой реализацию дополнительных операций, зависящих от состояния. В случае слова TYPE, тем не менее, нам нужна скорость работы. Мы так часто печатаем строки, что не можем при этом зря тратить время. Лучшим решением здесь может быть и установка функции TYPE, и загрузка флага:
DOER TYPE ( a # -- ) : ВИДЕО 0 УСТРОЙСТВО ! MAKE TYPE ( ... код для дисплея ... ) ; : ПРИНТЕР 1 УСТРОЙСТВО ! MAKE TYPE ( ... код для принтера ... ) ;
TYPE уже знает, какой код исполнять, остальные же определения будут использовать флаг. Другая возможность - это написание слова, которое выбирает значение вектора слова TYPE типа DOER (указатель на текущий код) и сравнивает его с адресом слова ПРИНТЕР. Если оно меньше этого адреса, то мы используем программу для ВИДЕО, иначе - обращаемся к ПРИНТЕРу. Если изменение состояния приводит к изменению небольшого количества функций, то все равно можно использовать DOER/MAKE. Вот определения трех операторов работы с памятью, которые можно выключать одновременно.
DOER !' ( векторное ! ) DOER CMOVE' ( векторное CMOVE) DOER FILL' ( векторное FILL ) : ЗАПИСЬ MAKE !' ! ;AND MAKE CMOVE' CMOVE ;AND MAKE FILL' FILL ;AND : -ЗАПИСЬ MAKE !' 2DROP ;AND MAKE CMOVE' 2DROP DROP ;AND MAKE FILL' 2DROP DROP ;AND
Однако, если количество необходимых к векторизации функций велико, предпочтительнее таблица состояний. Венчает это правило "запасной структурированный выход", слово типа DOER, которое векторизуется для осуществления структурированного выхода.
DOER КОЛЕБАТЬСЯ ( запасной выход ) : РАЗРЫВ КОЛЕБАТЬСЯ РАЗВОДИТЬСЯ ;
( ... Гораздо позже в тексте: ) : СМЯГЧИТЬСЯ MAKE КОЛЕБАТЬСЯ ДАРИТЬ-ЦВЕТЫ R> DROP ; - 273 -
По умолчанию КОЛЕБАТЬСЯ ни к чему не приводит. Если мы вызываем РАЗРЫВ, то дело кончается судом. Однако если СМЯГЧИТЬСЯ перед РАЗРЫВом, то мы пошлем букет цветов, а затем перепрыгнем сразу к точке с запятой, минуя суд так, что наш партнер(ша) о его возможности и не догадается. Такой подход особенно подходит там, где завершение должно производиться с помощью функции, определенной гораздо позже в программе (при декомпозиции по возрастанию сложности).
Увеличение сложности более раннего кода ограничено единственно запасным выходом в нужной точке.
УПРОЩЕНИЕ.
Этот совет я оставил напоследок, поскольку он служит примером награды за стремление к простоте.
------------------------------------------------------------ СОВЕТ Пытайтесь не хранить какие бы то ни было флаги в памяти. ------------------------------------------------------------
Флаг на стеке сильно отличается от такового в памяти. На стеке флаги могут быть легко получены (опросом аппаратуры, вычислением или чем угодно), а затем употреблены структурами управления. Короткая жизнь без особых затруднений. Но только запишите флаг в память, и увидите, что случится. В добавление к наличию самого флага, у Вас теперь добавились сложности по его размещению. Ведь оно должно быть:
* создано * инициализировано (даже до того, как что-то действительно изменится) * сброшено (то есть передача его определенной команде оставит флаг в нужном текущем состоянии)
Поскольку в памяти все флаги - переменные, то они нереентерабельны. Пример случая, в котором нам следовало бы переосмыслить необходимость флага - это тот пример, который мы уже несколько раз рассматривали. В "цветах" мы предположили, что наилучшим синтаксисом будет:
СВЕТЛО СИНИЙ
то есть прилагательное СВЕТЛО предшествует названию цвета. Отлично. Но помните код, реализующий эту версию? Сравните его с - 274 -
простотой такого подхода:
0 CONSTANT ЧЕРНЫЙ 1 CONSTANT СИНИЙ 2 CONSTANT ЗЕЛЕНЫЙ 3 CONSTANT ЖЕЛТЫЙ 4 CONSTANT КРАСНЫЙ 5 CONSTANT ФИОЛЕТОВЫЙ 6 CONSTANT КОРИЧНЕВЫЙ 7 CONSTANT СЕРЫЙ : СВЕТЛЫЙ ( цвет -- цвет' ) 8 OR ;
В этой версии мы пересмотрели синтаксис и теперь говорим
СИНИЙ СВЕТЛЫЙ
Мы выставили цвет, а затем смодифицировали его. Необходимость в переменной отпала, так же, как и в коде для ее разыменования и в еще большем объеме кода для ее сброса впоследствии. И полученный текст столь прост, что его невозможно не понять. Когда я первый раз написал эти команды, то выбрал подход, свойственный для английского (или русского) языка. "СИНИЙ СВЕТЛЫЙ" звучит задом наперед, что не совсем приемлемо.
Это было еще до начала моих бесед с Чаком Муром.
----------------------------------------------------------------
Философия Мура убедительна:
Я бы провел различие между хорошей читабельностью на английском языке и просто хорошей читабельностью. В других языках, типа испанского, прилагательные следуют за существительными. Нам следовало бы быть независимыми от деталей того, на каком языке мы размышляем сами. Все зависит от Ваших личных наклонностей: к простоте ли или к эмуляции английского. Английский язык не столь превосходен, чтобы нам было необходимо следовать ему раболепно.
----------------------------------------------------------------
Если бы я продавал свои "цвета" в пакете для графических рисовальщиков, то побеспокоился бы насчет создания флага. Однако при написании этих слов исключительно для собственного употребления, и если бы мне пришлось делать это вновь, я предпочел бы Мурово влияние и использовал бы "СИНИЙ СВЕТЛЫЙ". - 275 -
ИТОГИ ~~~~~ Использование логики и условных операторов в качестве существенного структурного элемента в программировании ведет к переусложненному, трудно управляемому и неэффективному коду. В этой главе мы обсуждали несколько путей для минимизации, оптимизации или устранения ненужных структур условного управления. В качестве последнего замечания: Фортовские игры с условными переходами и флагами не доступны в большинстве других современных языков. Реально же японцы базируют свой проект компьютера пятого поколения на языке под названием ПРОЛОГ - ПРОграммирование в ЛОГике - на котором пишут исключительно в логических терминах. Было бы интересно посмотреть на построение боевых рядов, если бы мы поставили ребром такой вопрос:
с IFами или без IFов?
В этой книге мы рассмотрели шесть первых шагов цикла разработки программного обеспечения, исследуя философские вопросы проектирования и практические соображения по реализации робастных, эффективных, читабельных программ. Мы не обсуждали оптимизацию, выверение, отладку, документирование, управление проектом, Фортовы инструменты для разработки, определения на ассемблере, использование и ограничения рекурсии, разработку многопрограммных приложений и целевую компиляцию.Но это уже совсем другая история.
ЛИТЕРАТУРА ~~~~~~~~~~ 1. Charles Eaker, "Just in Case," FORTH Dimensions II/3, p.37
ДЛЯ ДАЛЬНЕЙШИХ РАЗМЫШЛЕНИЙ ~~~~~~~~~~~~~~~~~~~~~~~~~~ У Вас имеется слово CHOOSE, которое принимает аргумент "n" и возвращает случайное число в пределах от 0 до n-1. Результат всегда положительный или нулевой. Можно использовать CHOOSE для выдачи флага: фраза
2 CHOOSE
дает случайный флаг 0 или 1 (ложь или истина). Напишите фразу для получения случайного числа между 0 и 19 (включительно) `или` между -20 и 0.