Л.БРОУДИ. СПОСОБ МЫШЛЕНИЯ - ФОРТ. часть 7 ГЛАВА 7 --------------------------------------------------------------- - 203 - ГЛАВА 7 Р А Б О Т А С Д А Н Н Ы М И : ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ С Т Е К И И С О С Т О Я Н И Я ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ---------------------------------------------------------------- Форт оперирует с данными одним из двух способов: либо на стеке, либо в структурах данных. Когда какой из подходов применять и как управлять и стеком, и структурами данных - вот тематика этой главы. ШИКАРНЫЙ СТЕК ~~~~~~~~~~~~~ Для Форт-слов простейший способ прередачи аргументов друг другу - это через стек. Процесс "прост", поскольку вся работа по заталкиванию и извлечению чисел со стека подразумевается сама собою разумеющейся. ---------------------------------------------------------------- Мур: Стек данных использует идею о "скрытой информации". Аргументы, которые должны передаваться между подпрограммами, не представляются в вызывающей последовательности. Один и тот же аргумент способен проходить через целую кучу слов совершенно незаметно, даже ниже уровня осторожности, соблюдаемого программистом, просто потому, что его не надо представлять специально. ---------------------------------------------------------------- Вот один из важных результатов такого подхода: аргументы не именованы. Они находятся на стеке, а не в именованных переменных. Такой эффект - одна из причин элегантности Форта. В то же время это также и одна из причин, по которой плохо написанный на Форте код может быть нечитаемым. Давайте исследуем этот парадокс. - 204 - Наличие стека сродни местоимениям в языке. Вот пассаж: Возьми этот дар, оберни его в папиросную бумагу и положи его в футляр. Обратите внимание, что слово "дар" использовано лишь однажды. Дар впоследствии упоминается как "он". Неконкретность конструкции "его" делает русский (или английский) язык более читабельным (если ссылка однозначна). То же и со стеком, неопределенная передача аргументов делает код более понятным. Мы подчеркиваем `процессы`, а не `передачу аргументов` процессам. Наша аналогия с местоимениями может подсказать, почему плохо написанный на Форте текст может быть таким нечитабельным. В разговорном языке возникают затруднения, когда на слишком много вещей одновременно ссылаются местоимениями. Сорви обертку и открой футляр. Вынь дар и выброси его. В этой фразе конфуз возник оттого, что мы использовали "его" для ссылки одновременно на много вещей. Есть два решения по преодолению этой ошибки. Простейшее - это использование реального имени вместо "него": Сорви обертку и открой футляр. Вынь дар и выброси `футляр`. Или мы можем ввести слова "первый" и "последний". Однако лучшим решением было бы перепроектирование фразы: Сорви обертку и открой подарок. Отбрось футляр. Так и в Форте мы имеем те же наблюдения: ------------------------------------------------------------ СОВЕТ Упрощайте код за счет использования стека. Однако не уходите в стек слишком глубоко внутри отдельно взятого определения. Измените планировку, или, как к последней инстанции, обратитесь к именованной переменной. ------------------------------------------------------------ Некоторые новички в Форте смотрят на стек так же, как гимнаст глядит на трамплин: как на отличное место для того, чтобы на нем скакать. Однако стек предназначен для передачи данных, а не для акробатики. Так насколько глубоко это "слишком глубоко"? В общем случае три элемента на стеке - это максимум того, чем Вы можете - 205 - управлять внутри одного определения. (Для арифметики двойной точности каждый "элемент" занимает две позиции в стеке, но они логично воспринимаются за один элемент такими операторами, как 2DUP, 2OVER и т.д.) В обычном лексиконе стековых операторов ROT - единственный, который дает доступ к третьему элементу на стеке. Кроме слов PICK и ROLL (которые мы позже прокомментируем), нет легкого способа добраться до того, что лежит ниже. Для продолжения наших аналогий можно предположить, что три элемента в стеке соответствуют трем русским (английским) местоимениям - "это" ("this"), "то" ("that") и "се" ("th'other"). ПЕРЕПРОЕКТИРОВАНИЕ. Давайте вообразим ситуацию, когда неверно выбранный подход ведет к проблеме беспорядка на стеке. Предположим, мы пытаемся написать определение слова +THRU (см. главу 5, "Организация листингов", "Блоки загрузки глав"). Мы решили, что тело нашего цикла будет ... DO I LOAD LOOP ; то есть мы поместим LOAD в цикл и будем давать ему индекс цикла, загружая блоки по их абсолютным номерам. Изначально на стеке мы имеем: низ верх где "низ" и "верх" - это `смещения` от BLK. Мы должны их представить для DO в следующем виде: верх+1+blk низ+blk Самой сложной нашей задачей является прибавление значения BLK к обоим смещениям. Мы уже ступили на неверную дорожку, но еще об этом не догадываемся. Так что давайте продолжать. Мы пытаемся: - 206 - низ верх BLK @ низ верх blk SWAP низ blk верх OVER низ blk верх blk + низ blk верх+blk 1+ низ blk верх+blk+1 ROT ROT верх+blk+1 низ blk + верх+blk+1 низ+blk Мы все проделали, но что же это за путаница! Если мы любим самобичевание, то должны проделать еще два таких усилия, приходя к BLK @ DUP ROT + 1+ ROT ROT + и к BLK @ ROT OVER + ROT ROT + 1+ SWAP Все три предложения делают одно и то же, но код, кажется, становится лишь все мрачнее, но не лучше. Опыт нам подсказывает, что комбинация ROT ROT - это опасный признак: на стеке - столпотворение. Нет необходимости в тщательной проработке вариантов для определения проблемы: как только мы создали две копии "blk", мы сразу получили четыре элемента на стеке. Первое, о чем обычно вспоминают в этом месте - это о стеке возвратов: BLK @ DUP >R + 1+ SWAP R> + (Смотрите далее "Шикарный стек возвратов".) Мы здесь продублировали "blk", сохраняя его копию на стеке возвратов и прибавляя другую копию к "верху". Признаем улучшение. А как читабельность? Дальше мы думаем: "Может быть, нам нужна именованная переменная." Конечно, у нас уже есть одна: BLK. Поэтому пробуем: BLK @ + 1+ SWAP BLK @ + - 207 - Теперь это читается лучше, но все еще слишком длинно, и к тому же избыточно. BLK @ + появляется здесь дважды. "BLK @ +"? Это звучит как-то знакомо. Наконец наши нейроны дают нужное соединение. Мы смотрим вновь на только что определенное слово +LOAD: : +LOAD ( смещение -- ) BLK @ + LOAD ; Это слово, +LOAD, должно было бы и делать такую работу. Все, что нам нужно написать - это: : +THRU ( низ верх ) 1+ SWAP DO I +LOAD LOOP ; Таким образом мы не создали более эффективной версии, поскольку работа BLK @ + будет проделываться в каждом проходе цикла. Однако у нас получился более чистый, более простой концептуально и более читабельный кусок кода. В данном случае неэффективность незаметна, поскольку проявляется один раз при загрузке каждого блока. Перепроектирование или переосмысливание задачи должно быть тем путем, который нам следует избирать всякий раз, когда дела становятся плохи. ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ. Большинство задач могут быть перегруппированы так, что лишь несколько аргументов на стеке нужны для них одновременно. Однако бывают ситуации, когда ничего сделать не удается. Вот пример наихудшего случая. Пусть у нас имеется слово ЛИНИЯ, которое проводит линию между двумя точками, определенными их координатами в следующем порядке: ( x1 y1 x2 y2 ) где x1,y1 представляют координаты одного конца, а x2,y2 - противоположного конца линии. Теперь Вам надо написать слово по имени [РАМКА], которое берет четыре аргумента в таком порядке: ( x1 y1 x2 y2 ) где x1 y1 представляют координаты верхнего левого угла, а x2 y2 - нижнего правого угла рамки. У Вас не просто четыре элемента на стеке, но каждый из них должен использоваться более одного раза при рисовании линий от точки до точки. Хотя мы и используем стек для получения этих - 208 - четырех аргументов, алгоритм рисования сам по себе не соответствует природе стека. Если Вы торопитесь, то, быть может, лучшим выходом было бы использовать простое решение: VARIABLE ВЕРХ ( координата y - вершина рамки) VARIABLE ЛЕВО ( " x - левая сторона) VARIABLE НИЗ ( " y - низ рамки) VARIABLE ПРАВО ( " x - правая сторона) : [РАМКА] ( x1 y1 x2 y2 ) НИЗ ! ПРАВО ! ВЕРХ ! ЛЕВО ! ЛЕВО @ ВЕРХ @ ПРАВО @ ВЕРХ @ ЛИНИЯ ПРАВО @ ВЕРХ @ ПРАВО @ НИЗ @ ЛИНИЯ ПРАВО @ НИЗ @ ЛЕВО @ НИЗ @ ЛИНИЯ ЛЕВО @ НИЗ @ ЛЕВО @ ВЕРХ @ ЛИНИЯ ; Мы сделали так: создали четыре именованные переменные, по одной на каждую из координат. Первое, что делает [РАМКА] - это записывает в эти переменные аргументы со стека. Затем с их использованием рисуются линии. Подобные переменные, которые используются лишь внутри определения (или, в некоторых случаях, внутри лексикона) называются "локальными". Каюсь, что много раз я отчаянно пытался проделать на стеке как можно больше вместо того, чтобы определять локальную переменную. Есть три причины на то, чтобы подобного избегать. Во-первых, это болезненно сказывается на коде. Во-вторых, результат получается нечитаемым. В-третьих, вся проделанная работа становится бесполезной, когда становится необходимым изменение в проекте, и изменяется порядок следования пары аргументов. Все эти DUPы, ROTы и OVERы в действительности не решили проблему, а только гарцевали вокруг да около. Имея в виду эту третью причину, я рекомендую следующее: ------------------------------------------------------------ СОВЕТ В первую очередь в фазе проектирования держите на стеке только те аргументы, которые используете немедленно. В остальных случаях создавайте локальные переменные. (При необходимости убирайте переменные на стадии оптимизации.) ------------------------------------------------------------ В-четвертых, если определение чрезвычайно критично ко времени исполнения, подобные заковыристые манипуляции со стеком (типа ROT ROT) могут хорошо поедать тактовые циклы. Прямой доступ к переменным быстрее. Если это `действительно` критично ко времени, Вам может понадобиться все равно преобразовать его в ассемблер. В этом случае все Ваши проблемы со стеком выставляются за дверь, поскольку любые Ваши данные будут обрабатываться либо в - 209 - регистрах, либо косвенно через регистры. К счастью, определения с беспорядочнейшими стековыми аргументами часто принадлежат к тем, что пишутся в коде. Наш примитив [РАМКА] как раз в этом ключе. Другой подобный - это CMOVE>. Конечно, способ, выбранный нами для [РАМКА], мошенничает, по полчаса выуживая нужное на стек, но он, вне всякого сомнения, является наилучшим решением. Что в нем плохо, так это затраты на создание четырех именованных переменных, заголовков и всего остального для них, исключительно для использования в единственной подпрограмме. (Если Вы для задачи применяете целевую компиляцию, то при этом заголовков в словаре не требуется, и единственной потерей будут 8 байтов в памяти под переменные. В Форт-системах будущего (*) заголовки могут быть в любом случае вынесены в другие страницы памяти; опять же, потери составят только 8 байтов (**). Позвольте мне повторить: этот пример демонстрирует наихудший случай, и в большинстве Форт-приложений встречается редко. Если слова хорошо факторизованы, то каждое из них спроектировано для того, чтобы делать очень немногое. Слова, делающие мало, обычно требуют и мало аргументов. В нашем случае мы имеем дело с двумя точками, каждая из которых представлена двумя координатами. Нельзя ли изменить проектировку? Первое, ЛИНИЯ, может быть, `слишком` примитивный примитив. Она требует четыре аргумента, поскольку способна рисовать линии между двумя любыми точками, и диагонально, если нужно. При рисовании нашей рамки нам могут понадобиться только строго вертикальные и горизонтальные линии. В таком лучае мы могли бы написать более мощные, но менее специфичные слова ВЕРТИКАЛЬ и ГОРИЗОНТАЛЬ для их изображения. Каждое из них будет требовать только `трех` аргументов: x и y начальной позиции и длину. Факторизация функции упрощает определение слова [РАМКА]. Или мы могли бы обнаружить, что такой синтаксис для пользователя выглядит более натуральным: 10 10 НАЧАЛО! 30 30 РАМКА где НАЧАЛО! устанавливает двухэлементный указатель на то место, откуда будет расти рамка (верхний левый угол). Затем "30 30 РАМКА" рисует рамку в 30 единиц высотой и 30 шириной относительно начала. (*) - уже настоящего (**) - на самом деле в обоих случаях - 16 байтов, т.е. 8 слов. - 210 - Этот подход сокращает количество стековых аргументов для слова РАМКА как для проектной единицы. ------------------------------------------------------------ СОВЕТ При определении состава аргументов, передаваемых через структуры данных, а не через стек, выбирайте те, которые более постоянны или которые представляют текущее состояние. ------------------------------------------------------------ ПО ПОВОДУ СЛОВ PICK И ROLL. Некоторые чудаки любят слова PICK и ROLL. Они их используют для доступа к элементам с любого уровня стека. Мы их не рекомендуем. С одной стороны, PICK и ROLL побуждают программиста думать о стеке как о массиве, которым тот не является. Если у Вас настолько много элементов на стеке, что нужны PICK и ROLL, то эти элементы должны были бы быть действительно в массиве. Второе, программист начинает считать возможным обращаться к аргументам, оставленным на стеке определениями более высокого, вызывающего уровня без того, чтобы эти элементы действительно `передавались` в качестве аргументов, что делает определения зависящими от других определений. Это - признак неструктурированности, и это - опасно. Наконец, позиция элемента на стеке зависит от того, что находится над ним, а число вещей над ним может постоянно меняться. К примеру, если адрес находится у Вас в четвертом элементе стека, то можно написать 4 PICK @ для загрузки его содержимого. Но Вам придется писать ( n) 5 PICK ! поскольку при наличии на стеке "n" адрес теперь перемещается в пятую позицию. Подобный код трудно читать и еще труднее переделывать. ДЕЛАЙТЕ СТЕКОВЫЕ РИСУНКИ. Когда Вам нужно разрешить некую запутанную стековую ситуацию, лучше ее проработать с помощью карандаша и бумаги. Некоторые люди даже изготавливают формы, типа той, что показана - 211 - на рис. 7-1. Будучи оформленными подобным образом (вместо закорючек на обороте Вашей телефонной книжки), стековые комментарии становятся приятной внешней документацией. Рис.7-1. Пример стекового комментария. ---------------------------------------------------------- | | | Имя слова: CMOVE> Программист: LPB Дата: 9/23/83 | | | |----------------------------------------------------------| | Операции | Стековый эффект |Стек возвр| |----------------------------------------------------------| | / / / / / / / / / | s d # | | |----------------------------------------------------------| | ?DUP IF | s d # | | --|----------------------------------------------------------| | | 1- DUP >R | s d #-1 | #-1 | | |----------------------------------------------------------| | | + | s end-of-d | #-1 | | |----------------------------------------------------------| | | SWAP | end-of-d s | #-1 | | |----------------------------------------------------------| | | DUP | end-of-d s s | #-1 | | |----------------------------------------------------------| | | R> | end-of-d s s #-1 | | | |----------------------------------------------------------| | | + | end-of-d s end-of-s | | | |----------------------------------------------------------| | | DO | end-of-d | | | |----------------------------------------------------------| | | -- I C@ | end-of-d last-char | | | |-|--------------------------------------------------------| | | | OVER | end-of-d last-char end-of-d| | | |-|--------------------------------------------------------| | | | C! | end-of-d | | | |-|--------------------------------------------------------| | | -- 1- | next-to-end-of-d | | | |----------------------------------------------------------| | | -1 +LOOP | | | --|----------------------------------------------------------| | | ELSE | s d | | | |----------------------------------------------------------| | | DROP | s | | | |----------------------------------------------------------| | | THEN | x | | --|----------------------------------------------------------| | DROP ; | | | ---------------------------------------------------------- - 212 - СОВЕТЫ ПО СТЕКУ ~~~~~~~~~~~~~~~ ------------------------------------------------------------ СОВЕТ Убедитесь в том, что изменения на стеке происходят при любых возможных потоках передачи управления. ------------------------------------------------------------ В этом стековом комментарии на слово CMOVE> (рис. 7-1), внутренняя скобка очерчивает содержимое цикла DO LOOP. Глубина стека при выходе из цикла та же, что и при входе в него: один элемент. Внутри внешних скобок результат работы на стеке части IF тот же, что и части ELSE: остается один элемент. (Что представляет собой этот оставшийся элемент, значения не имеет, и поэтому обозначается "х" сразу после THEN.) ------------------------------------------------------------ СОВЕТ Когда два действия выполняются над одним и тем же числом, выполняйте сначала ту из функций, которая оставляет свой результат глубже на стеке. ------------------------------------------------------------ Для примера: : COUNT ( a -- a+1 # ) DUP C@ SWAP 1+ SWAP ; (где вначале Вы вычисляете счетчик) более эффективно может быть записано в виде: : COUNT ( a -- a+1 # ) DUP 1+ SWAP C@ ; ------------------------------------------------------------ СОВЕТ Где можно, сохраняйте количество возвращаемых аргументов постоянным во всех возможных случаях. ------------------------------------------------------------ Часто обнаруживаются определения, которые выполняют некоторую работу и, если что-то было не так, возвращают код ошибки. Вот один из путей, по которому можно спроектировать стековый интерфейс: ( -- код-ошибки f | -- t ) Если значение флага - истина, то операция удачна. Если же оно - ложь, то операция окончилась неудачей и на стеке присутствует еще одно число, показывающее природу ошибки. - 213 - Вы можете обнаружить, что манипуляции со стеком проще, если переделать интерфейс так: ( -- код-ошибки | 0=нет-ошибки ) Одно число служит и флагом, и (при неудаче) номером ошибки. Обратите внимание на то, что при этом использована обратная логика: не равенство нулю говорит об ошибке. Под значение кода может быть использовано любое число, кроме нуля. ШИКАРНЫЙ СТЕК ВОЗВРАТОВ ~~~~~~~~~~~~~~~~~~~~~~~ А как насчет использования стека возвратов для хранения временных аргументов? Хороший ли это стиль? Некоторые люди с большой осторожностью используют этот стек. Однако стек возвратов предлагает самое простое разрешение некоторым неприятным стековым ситуациям. Посмотрите на определение CMOVE> из предыдущего раздела. Если вы решаете использовать стек возвратов для этого, то помните, что Вы при этом используете компонент Форта, предназначенный для иных целей. (Смотрите раздел под названием "Совместное использование компонентов" далее в этой главе.) Вот некоторые рекомендации о том, как не попасться на собственный крючок: ------------------------------------------------------------ СОВЕТ 1. Операции со стеком возвратов должны располагаться симметрично. 2. Они должны располагаться симметрично при любых направлениях передачи управления. 3. При факторизации определений следите, чтобы не получилось так, что одна часть разбиения содержит один оператор, работающий со стеком возвратов, а другая часть - его ответный оператор. 4. При использовании внутри цикла DO LOOP такие операторы должны использоваться симметрично внутри цикла, а слово I работает неверно в окружении >R и R>. ------------------------------------------------------------ Для каждого из слов >R должно присутствовать R> внутри того же определения. Иногда операторы кажутся расположенными симметрично, но из-за извилистого пути передачи управления таковыми не являются. К примеру: ... BEGIN ... >R ... WHILE ... R> ... REPEAT - 214 - Если такая конструкция используется во внешнем цикле Вашей задачи, все будет в порядке до тех пор, пока Вы из него не выйдете (быть может, через много часов), и тогда все неожиданно зависнет. Причина? При последнем прохождении цикла разрешающий оператор R> не был исполнен. ПРОБЛЕМА ПЕРЕМЕННЫХ ~~~~~~~~~~~~~~~~~~~ Хотя немедленно интересующие нас данные мы держим на стеке, но в то же время мы зависим и от большого количества информации, заключенной в переменных, и готовой к частому доступу. Участок кода может изменить содержимое переменной без того, чтобы обязательно знать способ использования этих данных, кто их использует или же когда и будут ли они вообще использоваться. Другой кусок кода может взять содержимое переменной и использовать его без знания того, откуда там это значение. Для каждого слова, которое кладет значение на стек, другое слово должно снимать это значение. Стек дает нам соединение от точки к точке, наподобие почты. Переменные, с другой стороны, могут быть установлены любой командой и считаны любое число раз - или совсем не считаны - любой командой. Переменные доступны каждому, кто удосужится на них взглянуть - как картины. Так, переменные могут использоваться для отображения текущего положения дел. Использование такого отображения может упростить задачу. В примере с римскими цифрами из главы 4 мы использовали переменную #КОЛОНКИ для представления текущего положения смещения; слова ЕДИНИЧКА, ПЯТЕРКА и ДЕСЯТКА зависели от этой информации для определения того, какой тип символа печатать. Нам не пришлось каждый раз применять описания типа ДЕСЯТКИ ЕДИНИЧКА, ДЕСЯТКИ ПЯТЕРКА и т.д. С другой стороны, такое использование добавляет новый уровень сложности. Для того, чтобы сделать что-то текущим, мы должны определять переменную или некоторый тип структуры данных. Мы также должны помнить о том, чтобы ее инициализировать, если есть вероятность того, что какой-либо кусок кода начнет на нее ссылаться до того, как мы успеем ее установить. Более серьезной проблемой в переменных является то, что они не "реентерабельны". В многозадачной Форт-системе каждая из задач, которая требует локальных переменных, должна иметь свои собственные копии их. Для этого служат переменные типа USER. (См. "Начальный курс ...", гл. 9, "География Форта"). - 215 - Даже в пределах одной задачи определение, ссылающееся на переменную, труднее проверять, изменять и использовать повторно в другой обстановке, отличной от такой, в которой аргументы передаются через стек. Представим себе, что мы разрабатываем редактор для текстового процессора. Нам нужна программа, которая вычисляет количество символов между текущим положением курсора и предыдущей последовательностью возврат-каретки/перевод-строки. Так мы пишем слово, в котором при помощи цикла DO LOOP, начиная от текущей позиции (КУРСОР @) и заканчивая нулевой позицией, производится поиск символа перевода строки. Когда в цикле обнаруживается искомая символьная последовательность, мы вычитаем ее относительный адрес из нашего текущего положения курсора ее-позиция КУРСОР @ SWAP - для получения расстояния между ними. Стековая картинка слова будет: ( -- расстояние-до-предыдущего-вк/пс ) Но при последующем кодировании мы обнаруживаем, что аналогичное слово нужно для вычисления расстояния от произвольного символа - `не` от текущей позиции курсора. Мы останавливаемся на том, что вычленяем "КУРСОР @" и передаем начальный адрес через стек в качестве аргумента, получая: ( начальное-положение -- расстояние-до-предыдущего-вк/пс ) Выделив ссылку на переменную мы сделали определение более полезным. ------------------------------------------------------------ СОВЕТ За исключением случаев, когда манипуляции со стеком достигают уровня нечитабельности, пытайтесь передавать аргументы через стек вместо того, чтобы брать их из переменных. ------------------------------------------------------------ ---------------------------------------------------------------- Кожж: Большая часть модульности Форта происходит от проектирования и понимания слов Форта как "функций" в - 216 - математическом смысле. Мой опыт показывает, что Форт-программист обычно старается избегать определения любых, кроме наиболее существенных глобальных переменных (у меня есть друг, у которого над столом висит надпись "Помоги убрать переменные"), и пытается писать слова со свойством так называемой "ссылочной переносимости", т.е. при одних и тех же данных на стеке слово всегда дает одинаковый ответ независимо от более общего контекста, в котором оно исполняется. Это на деле - как раз то свойство, которое мы используем для индивидуальной проверки слов. Слова, не имеющие его, гораздо труднее тестировать. Чувствуется, что "именованные переменные", значения которых часто меняются - вещь, немногим лучшая "запрещенного" ныне GOTO. ---------------------------------------------------------------- Ранее мы предлагали использовать локальные переменные в первую очередь во время фазы проектирования для уменьшения движения на стеке. Важно отметить, что при этом переменные использовались только внутри одного определения. В нашем примере [РАМКА] получает четыре аргумента со стека и немедленно загружает их в локальные переменные для собственного употребления. Четыре переменные вне определения не используются, и слово работает так же надежно, как функция. Программисты, не привыкшие к языку, в котором данные могут передаваться автоматически, не всегда используют стек так хорошо, как можно бы. Майкл Хэм выдвигает предположение, что начинаюшие пользователи Форта не доверяют стеку [1]. Он говорит о том, что вначале действительно чувствуешь себя безопасней при хранении чисел в переменных вместо содержания их на стеке. Ощущаешь, что "лучше и не говорить о том, что `могло` бы произойти со стеком во время всей этой кутерьмы". Хэму потребовалось некоторое время на понимание того, что "если слова правильно заботятся о себе, используя стек только для ожидаемого ввода и вывода и чистя его за собой, то они могут рассматриваться как замкнутые системы ... Я могу положить счетчик на стек в начале цикла, пройти через всю программу в каждой ее части, и в конце нее счетчик будет существовать опять на вершине стека, ни на волос не сместившись". - 217 - ЛОКАЛЬНЫЕ И ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ / ИНИЦИАЛИЗАЦИЯ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Как мы уже говорили раньше, переменная, которая используется исключительно внутри одного определения (или одного лексикона), и спрятана от остального кода, называется локальной. Переменная же, используемая более, чем одним лексиконом, называется глобальной. Как мы видели в предыдущей главе, набор глобальных переменных, которые совместно описывают общий интерфейс между несколькими лексиконами, называется "интерфейсным лексиконом". Форт не делает различий между локальными и глобальными переменными. Их проводят Форт-программисты. ---------------------------------------------------------------- Мур: Нам следовало бы писать для читателя. Если на что-то, типа временной переменной для накапливания суммы, ссылаются лишь локально, то мы и должны определить это что-то локально. Подручнее определить это в том же блоке, где оно используется, где приведены нужные примечания. Если же что-то используется глобально, то мы должны составлять логически взаимосвязанные вещи и определять их все вместе в отдельном блоке. По одной на строку и с комментарием. Вопрос: где Вы все это будете инициализировать? Кое-кто считает, что надо делать это в той же строке, сразу же после определения. Но при этом вытесняются комментарии, и не остается места под добавочные примечания. И инициализация разбрасывается по всему тексту задачи. Я стараюсь проделывать всю инициализацию в блоке загрузки. После того, как я загрузил все свои блоки, инициализирую все, что должно быть инициализировано. Инициализация может даже включать в себя установку таблиц цветов или вызов какой-нибудь программы сброса. Если Ваша программа обречена на целевую компиляцию, то легко вписать сюда же слово, которое производит все установки. При этом можно делать и гораздо больше работы. Мне случалось давать определения переменных в ПЗУ, причем их тела размещались в массиве в верхней памяти, а начальные значения - в ПЗУ, и я копировал наверх эти значения во время инициализации. Впрочем, чаще надо просто занести в несколько переменных что-нибудь, отличное от нуля. ---------------------------------------------------------------- - 218 - СОХРАНЕНИЕ И ВОССТАНОВЛЕНИЕ СОСТОЯНИЯ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Переменные характерны тем, что когда меняешь их содержимое, то затираешь значение, которое там было раньше. Давайте рассмотрим некоторые проблемы, которые это может породить и кое-что из того, что можно с ними сделать. BASE - это переменная, которая показывает текущую системы счисления для всего числового ввода и вывода. Следующие слова обычно присутствуют в Форт-системах: : DECIMAL 10 BASE ! ; : HEX 16 BASE ! ; Представьте себе, что мы пишем слово, печатающее "дамп" памяти. Обычно мы работаем в десятичном режиме, но хотим показать дамп в шестнадцатеричном. Мы пишем так: : DUMP ( a # ) HEX ... ( код для дампа ) ... DECIMAL ; Это работает - большую часть времени. Однако здесь заложена презумпция того, что мы хотим вернуться назад к десятичной системе. А что как если мы работали в шестнадцатеричной и хотим вернуться именно к ней? До изменения значения переменной BASE на HEX нам следует сохранить ее текущее значение. Когда дамп закончится, мы его восстановим. Это значит, что мы должны на время отложить старое значение, пока формируем дамп. Одно из возможных мест для этого - стек возвратов: : DUMP ( a # ) BASE @ >R HEX ( код для дампа ) R> BASE ; Если это покажется сложным, мы можем определить временную переменную: VARIABLE СТАРЫЙ-BASE : DUMP ( a # ) BASE @ СТАРЫЙ-BASE ! HEX ( код для дампа ) СТАРЫЙ-BASE @ BASE ! ; Как же быстро все усложняется. В этой ситуации если и текущее, и старое содержимое переменной принадлежит только Вашей задаче (и не является частью Вашей системы), и если такая ситуация возникает более, чем один раз, примените технику факторизации: : СХОРОНИТЬ ( а) DUP 2+ 2 CMOVE> ; : ЭКСГУМИРОВАТЬ ( а) DUP 2+ SWAP 2 CMOVE ; - 219 - Затем вместо определения двух переменных типа УСЛОВИЕ и СТАРОЕ-УСЛОВИЕ, определите одну переменную двойной длины: 2VARIABLE УСЛОВИЕ Используйте СХОРОНИТЬ и ЭКСГУМИРОВАТЬ для сохранения и восстановления начального значения: : ДЕЛИШКИ УСЛОВИЕ СХОРОНИТЬ 17 УСЛОВИЕ ! ( делишки) УСЛОВИЕ ЭКСГУМИРОВАТЬ ; СХОРОНИТЬ держит "старую" версию условия по адресу УСЛОВИЕ 2+. Вам все равно следует быть осторожными. Возвращаясь к нашему примеру DUMPа, предположим, что мы решили добавить черту дружелюбия, позволяя пользователю прекращать дамп в любой момент нажатием на клавишу "выход". Итак, внутри цикла мы строим проверку на нажатие клавиши и в при необходимости исполняем QUIT. Но что же при этом случается? Пользователь начинает работать в десятичной системе, затем он вводит DUMP. Он покидает дамп на полдороге и оказывается, на свое удивление, в шестнадцатеричной. В простом, подручном случае лучшим решением является использование не QUIT, а контролируемого выхода (через LEAVE и т.д.) в конец определения, где BASE восстанавливается. В очень сложных задачах контролируемый выход часто непрактичен, хотя множество переменных должно быть как-то восстановлено в своих естественных значениях. ---------------------------------------------------------------- Мур ссылается на такой пример: Вы и вправду оказались завязанными в узел. Сами себе создаете трудности. Если мне нужен шестнадцатеричный дамп, я говорю HEX DUMP. Если нужен десятичный, то говорю DECIMAL DUMP. Я не доверяю слову DUMP привилегию произвольно менять мое окружение. Имеется философский выбор между восстановлением ситуации по окончании и установкой ситуации в начале. Долгое время мне казалось, что я должен восстанавливать все в конце. И я должен был бы делать это постоянно и везде. Но ведь трудно определить "везде". Так что теперь я стараюсь устанавливать ситацию перед началом. Если у меня есть слово, которое отвечает за положение вещей, то лучше самому определить это положение. Если же кто-то другой его изменяет, то и ему не надо беспокоиться о том, чтобы его восстанавливать. Всегда имеется больше выходов, чем входов. ---------------------------------------------------------------- - 220 - В случаях, когда нужно что-то сбросить до окончания работы, мне кажется полезным использование единственного слова (я называю его ОЧИСТКА). Я вызываю слово ОЧИСТКА: * в точке нормального завершения программы * там, где пользователь может свободно выйти (перед QUIT) * перед любой точкой, в которой может возникнуть фатальная ошибка, которая вызывает аварийное завершение (ABORT). Наконец, если перед Вами возникает ситуация, когда надо сохранять/восстанавливать значение, убедитесь в том, что это не есть следствие плохой факторизации. Предположим, к примеру, что мы написали: : ДЛИННАЯ 18 #ДЫР ! ; : КОРОТКАЯ 9 #ДЫР ! ; : ИГРА #ДЫР @ 0 DO I ДЫРА ИГРАТЬ LOOP ; Текущая ИГРА может быть либо короткой, либо длинной. Позже мы решили, что нам нужно слово для игры с `любым` количеством дыр. Поэтому мы вызываем слово ИГРА так, чтобы не испортить текущий тип игры (число дыр): : ДЫРЫ ( n) #ДЫР @ SWAP #ДЫР ! ИГРА #ДЫР ! ; Поскольку нам понадобилось слово ДЫРЫ уже после того, как была определена ИГРА, то, кажется, оно и должно иметь большую сложность: мы строим ДЫРЫ вокруг игры. Но на самом деле - может быть, Вы это уже видите - правильнее будет переделать: : ДЫРЫ ( n) 0 DO I ДЫРА ИГРАТЬ LOOP ; : ИГРА #ДЫР @ ДЫРЫ ; Мы можем построить игру вокруг дыр и избежать всей этой белиберды с запоминанием/восстановлением. ВНУТРЕННИЕ СТЕКИ ПРОГРАММ ~~~~~~~~~~~~~~~~~~~~~~~~~ В последнем разделе мы исследовали некоторые пути для сохранения и восстановления `единственного` предыдущего значения. Некоторые задачи требуют того же для `нескольких` значений. Часто для Вас может оказаться самым удобным определение своего собственного стека. Вот исходный текст для пользовательского стека, имеющий очень простой контроль ошибок (при ошибках стек очищается): - 221 - CREATE СТЕК 12 ALLOT \ { 2указ-стека | 10стек [5 элем.] } HERE CONSTANT СТЕК> : ИНИЦ-СТЕК СТЕК СТЕК ! ; ИНИЦ-СТЕК : ?СБОЙ ( ?) IF ." Ошибка стека" ИНИЦ-СТЕК ABORT THEN ; : PUSH ( n) 2 СТЕК +! СТЕК @ DUP СТЕК> = ?СБОЙ ; : POP ( -- n) СТЕК @ @ -2 СТЕК +! СТЕК @ СТЕК < ?СБОЙ ; Слово PUSH берет число со стека данных и "заталкивает" его на этот новый стек. POP работает обратным образом, число "всплывает" из нового стека и ложится на Фортов стек данных. В реальной задаче Вам может захотеться применить другие имена для PUSH и POP с тем, чтобы они лучше соответствовали своему концептуальному назначению. СОВМЕСТНОЕ ИСПОЛЬЗОВАНИЕ КОМПОНЕНТОВ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ------------------------------------------------------------ СОВЕТ Можно использовать компонент для дополнительных нужд, кроме тех, для которых он введен, если выполняются такие условия: 1. Все использования компонента взаимно исключают друг друга 2. При каждом использовании компонента с прерыванием предыдущего обращения состояние компонента восстанавливается по окончании использования. ------------------------------------------------------------ Мы видели простой пример использования этого принципа со стеком возвратов. Этот стек - компонент Форт-системы, служащий для того, чтобы держать адреса возвратов, и вследствие этого указывающий, откуда Вы пришли и куда идете. Можно использовать этот стек и для хранения временных значений, и во многих случаях это хорошо. Проблемы же возникают тогда, когда игнорируется один из вышеуказанных принципов. В моем форматтере текста вывод может производиться невидимо. Это делается с двумя целями: (1) для заглядывания вперед с целью поиска какого-либо совпадения, и (2) для форматирования списка содержания (весь документ форматируется и вычисляются номера страниц без реального отображения чего бы то ни было). Было соблазнительно думать, что раз уж я добавил однажды возможность делать невидимым вывод, то мог бы использовать эту возможность для обслуживания обеих названных целей. К несчастью, цели эти взаимно друг друга не исключают. Давайте посмотрим, что могло бы произойти, если бы я пренебрег этим правилом. Пусть слово ВЫВОД производит вывод, и - 222 - оно достаточно умно, чтобы знать, видимый он или нет. Слова ВИДИМЫЙ и НЕВИДИМЫЙ ставят нужный режим. Мой код для заглядывания вперед вначале исполняет слово НЕВИДИМЫЙ, затем проверяет-форматирует дальнейший текст для определения его длины, и в конце исполняет ВИДИМЫЙ для восстановления положения вещей. Это отлично работает. Позже я добавляю режим сбора содержания. Вначале код выполняет НЕВИДИМЫЙ, затем проходит по документу, собирая номера страниц и т.д.; и наконец выполняет ВИДИМЫЙ для восстановления нормального вывода. Понятно? Предположим, что я формирую содержание и дошел до того места, где делается заглядывание вперед. Когда заглядывание кончается, я исполняю ВИДИМЫЙ. И неожиданно я принимаюсь печатать документ в то самое время, когда предполагал составлять содержание. Каково же решение? Их может быть несколько. Одно из решений рассматривает проблему в разрезе того, что заглядывающий код анализирует флаг видимости/невидимости, который мог быть предварительно установлен составителем содержания. Таким образом, код для заглядывания должен был бы уметь сохранять и впоследствии восстанавливать этот флаг. Другое решение предусматривает наличие двух раздельных переменных - одной для индикации того, что мы заглядываем вперед, а другой - для индикации печати содержания. Слово ВЫВОД требует, чтобы оба флага содержали ЛОЖЬ для того, чтобы и вправду что-нибудь напечатать. Имеются два пути для реализации последнего подхода, в зависимости от того, как Вы хотите произвести декомпозицию задачи. Первое, мы могли бы вложить проверку одного условия в проверку второго: : [ВЫВОД] ... ( исходное определение, всегда производит вывод) ... ; VARIABLE 'ЗАГЛЯД? ( t=заглядывание) : <ВЫВОД> 'ЗАГЛЯД? @ NOT IF [ВЫВОД] THEN ; VARIABLE 'ОГЛ? ( t=составление-содержания) : ВЫВОД 'ОГЛ? @ NOT IF <ВЫВОД> THEN ; ВЫВОД проверяет, что мы не делаем составление содержания, и вызывает <ВЫВОД>, который, в свою очередь, проверяет, что мы не заглядываем вперед, и вызывает [ВЫВОД]. В цикле разработки слово [ВЫВОД], которое всегда делает вывод, изначально называлось ВЫВОД. Затем для включения проверки на заглядывание было определено новое слово ВЫВОД, а старое - переименовано в [ВЫВОД], добавляя таким образом уровень сложности задним числом без нарушения какого-либо кода, который использует слово ВЫВОД. - 223 - Наконец, по добавлении составления содержания, для этой проверки определился новый ВЫВОД, а предыдущий был переименован в <ВЫВОД>. Это - один подход к использованию двух переменных. Другой состоит в том, что оба теста делаются в одном слове: : ВЫВОД 'ЗАГЛЯД? @ 'ОГЛ? @ OR NOT IF [ВЫВОД] THEN ; Однако в данном конкретном случае еще один подход может упразднить всю эту суету. Мы можем использовать единственную переменную не в качестве флага, а как счетчик. Определяем: VARIABLE 'НЕВИДИМЫЙ? ( t=невидимый) : ВЫВОД 'НЕВИДИМЫЙ? @ 0= IF [ВЫВОД] THEN ; : НЕВИДИМЫЙ 1 'НЕВИДИМЫЙ? +! ; : ВИДИМЫЙ -1 'НЕВИДИМЫЙ? +! ; Заглядывающий код начинает с вызова НЕВИДИМЫЙ, который наращивает счетчик на единичку. Значение, отличное от нуля - это "истина" (t), поэтому ВЫВОД не будет работать. По завершению заглядывания код вызывает ВИДИМЫЙ, который уменьшает счетчик назад к нулю ("ложь"). Код составления содержания также начинает с НЕВИДИМЫЙ и заканчивает ВИДИМЫЙ. Когда при сборе содержания мы достигаем заглядывания вперед, второе применение НЕВИДИМЫЙ увеличивает значение счетчика до 2. Последующий вызов ВИДИМЫЙ уменьшает счетчик до единицы, так что мы все равно остаемся невидимыми до тех пор, пока содержание не будет составлено. (Отметьте, что мы должны применять 0= вместо NOT. В стандарте-83 изменена функция NOT так, что она означает инверсию чего-либо, так что 1 NOT означает "истина". Между прочим, я думаю, что это было ошибкой.) Такое использование счетчика может быть, однако, опасным. Оно требует парности в использовании команд: два вызова ВИДИМЫЙ приводят к ситуации невидимости. Чтобы этого не было, можно проверять в ВИДИМЫЙ обнуление счетчика: : ВИДИМЫЙ 'НЕВИДИМЫЙ? @ 1- 0 MAX 'НЕВИДИМЫЙ? ! ; ТАБЛИЦА СОСТОЯНИЯ ~~~~~~~~~~~~~~~~~ Одна переменная способна отображать единственное условие либо флаг, значение или адрес функции. Собранные вместе условия представляют `состояние` задачи или определенного компонента [2]. Некоторые приложения требуют - 224 - возможности сохранения текущего состояния для его последующего восстановления или, быть может, чтобы иметь альтернативные состояния. ------------------------------------------------------------ СОВЕТ Когда для задачи требуется содержать целую группу условий одновременно, используйте таблицу состояния, а не отдельные переменные. ------------------------------------------------------------ В простейшем случае требуется сохранение и восстановление состояния. Предположим, мы изначально имеем шесть переменных, представляющих определенный компонент, как показано на рис. 7-2. Рис..7-2. Собрание родственных переменных. VARIABLE СВЕРХУ VARIABLE СНИЗУ VARIABLE СЛЕВА VARIABLE СПРАВА VARIABLE ВНУТРИ VARIABLE СНАРУЖИ Теперь предположим, что нам всех их надо сохранить таким образом, чтобы можно было произвести дальнейшие действия, а затем обратно восстановить. Мы могли бы определить: : @СОСТОЯНИЕ ( -- сверху снизу слева справа внутри снаружи) СВЕРХУ @ СНИЗУ @ СЛЕВА @ СПРАВА @ ВНУТРИ @ СНАРУЖИ @ ; : !СОСТОЯНИЕ ( сверху снизу слева справа внутри снаружи -- ) СНАРУЖИ ! ВНУТРИ ! СПРАВА ! СЛЕВА ! СНИЗУ ! СВЕРХУ ! ; таким образом сохраняя значения на стеке до тех пор, пока не придет время их восстановить. Либо мы могли бы определить второй набор переменных для всех вышеперечисленных, и в них по отдельности сохранять состояние. Однако предпочтительней будет такая технология, при которой создается таблица, а каждый элемент таблицы имеет свое имя. Затем определяется вторая таблица такой же длины. Как видно из рисунка 7-3, можно сохранять состояние копированием таблицы, называемой УКАЗАТЕЛИ, во вторую таблицу по имени АРХИВ. - 225 - Рис.7-3. Концептуальная модель для сохранения таблицы состояния. УКАЗАТЕЛИ АРХИВ +-----------+ +-----------+ СВЕРХУ|___________| ====> |___________| СНИЗУ|___________| |___________| СЛЕВА|___________| |___________| СПРАВА|___________| |___________| ВНУТРИ|___________| |___________| СНАРУЖИ| | | | +-----------+ +-----------+ Этот подход мы изложили в коде на рис. 7-4. Рис.7-4. Реализация сохранения/восстановления таблицы состояния. 0 CONSTANT УКАЗАТЕЛИ \ ' таблицы состояния, ПОЗЖЕ МЕНЯЕТСЯ : ПОЗИЦИЯ ( смещ -- смещ+2 ) CREATE DUP , 2+ DOES> ( -- a) @ УКАЗАТЕЛИ + ; 0 \ начальное смещение ПОЗИЦИЯ СВЕРХУ ПОЗИЦИЯ СНИЗУ ПОЗИЦИЯ СЛЕВА ПОЗИЦИЯ СПРАВА ПОЗИЦИЯ ВНУТРИ ПОЗИЦИЯ СНАРУЖИ CONSTANT /УКАЗАТЕЛИ \ ' конечного вычисленного смещения HERE ' УКАЗАТЕЛИ >BODY ! /УКАЗАТЕЛИ ALLOT \ реальная таблица CREATE АРХИВ /УКАЗАТЕЛИ ALLOT \ место для сохранения : АРХИВИРОВАТЬ УКАЗАТЕЛИ АРХИВ /УКАЗАТЕЛИ CMOVE ; : РЕСТАВРИРОВАТЬ АРХИВ УКАЗАТЕЛИ /УКАЗАТЕЛИ CMOVE ; Обратите внимание, что в этой реализации имена указателей - СВЕРХУ, СНИЗУ и т.д. всегда возвращают один и тот же адрес. Для представления текущих значений любых состояний всегда используется лишь одно место в памяти. Также отметьте, что мы определяем УКАЗАТЕЛИ (имя таблицы) как константу, а не через CREATE, используя для этого подставное нулевое значение. Это делается для того, чтобы ссылаться на УКАЗАТЕЛИ в определяющем слове ПОЗИЦИЯ, иначе мы не можем этого делать, пока не закончим определять имена полей, не выясним реальный размер таблицы и не будем в состоянии выполнить для нее ALLOT. - 226 - Когда имена полей созданы, мы определяем размер таблицы как константу /УКАЗАТЕЛИ. Наконец мы резервируем место для самой таблицы, модифицируя ее начальный адрес (HERE) внутри константы УКАЗАТЕЛИ. (Слово BODY> преобразует адрес, возвращаемый словом ', в адрес содержимого константы.) И вот УКАЗАТЕЛИ возвращают адрес определенной позже таблицы так же, как определенное через CREATE слово возвращает адрес таблицы, расположенной немедленно после заголовка этого слова. Хотя мы имеем право менять значение константы во время компиляции, как в нашем примере, здесь есть стилистическое ограничение: ------------------------------------------------------------ СОВЕТ Значение константы никогда не следует изменять после окончания компиляции задачи. ------------------------------------------------------------ Случай альтернативных состояний несколько более сложен. В этой ситуации нам приходится переключаться вперед и назад между двумя (или более) состояниями, при этом не перепутывая условия в каждом из состояний. Рис. 7-5 демонстрирует концептуальную модель такого рода таблицы состояния. Рис.7-5. Концептуальная модель для таблиц альтернативных состояний. РЕАЛЬНЫЕ ПСЕВДО +-----------+ +-----------+ СВЕРХУ|___________| СВЕРХУ|___________| СНИЗУ|___________| СНИЗУ|___________| СЛЕВА|___________| ИЛИ СЛЕВА|___________| СПРАВА|___________| СПРАВА|___________| ВНУТРИ|___________| ВНУТРИ|___________| СНАРУЖИ| | СНАРУЖИ| | +-----------+ +-----------+ В этой модели имена СВЕРХУ, СНИЗУ и т.д. могут быть выполнены так, что будут указывать на две таблицы, РЕАЛЬНЫЕ и ПСЕВДО. Делая текущей таблицу РЕАЛЬНЫЕ, мы устанавливаем все имена-указатели внутрь этой таблицы; делая текущей таблицу ПСЕВДО, получаем адреса внутри другой таблицы. Программа на рис. 7-6 реализует механизм альтернативных состояний. - 227 - Рис.7-6. Реализация механизма альтернативных состояний. VARIABLE 'УКАЗАТЕЛИ \ указатель на таблицу состояния : УКАЗАТЕЛИ ( -- адр текущей таблицы) 'УКАЗАТЕЛИ @ ; : ПОЗИЦИЯ ( смещ - смещ+2) CREATE DUP , 2+ DOES> ( -- a) @ УКАЗАТЕЛИ + ; 0 \ начальное смещение ПОЗИЦИЯ СВЕРХУ ПОЗИЦИЯ СНИЗУ ПОЗИЦИЯ СЛЕВА ПОЗИЦИЯ СПРАВА ПОЗИЦИЯ ВНУТРИ ПОЗИЦИЯ СНАРУЖИ CONSTANT /УКАЗАТЕЛИ \ конечное вычисленне смещение CREATE РЕАЛЬНЫЕ /УКАЗАТЕЛИ ALLOT \ реальная таблица CREATE ПСЕВДО /УКАЗАТЕЛИ ALLOT \ временная таблица : РАБОТА РЕАЛЬНЫЕ 'УКАЗАТЕЛИ ! ; РАБОТА : ПРИТВОРСТВО ПСЕВДО 'УКАЗАТЕЛИ ! ; Слова РАБОТА и ПРИТВОРСТВО соответственно меняют указатели. К примеру: РАБОТА 10 СВЕРХУ ! СВЕРХУ ? 10 ~~~ ПРИТВОРСТВО 20 СВЕРХУ ! СВЕРХУ ? 20 ~~~ РАБОТА СВЕРХУ ? 10 ~~~ ПРИТВОРСТВО СВЕРХУ ? 20 ~~~ Главное отличие в этом последнем подходе заключается в том, что имена проходят через дополнительный уровень перенаправления (УКАЗАТЕЛИ переделаны из константы в определение через двоеточие). Имена полей можно заставить показывать на одну из двух таблиц состояний. Поэтому каждому из них приходится выполнять немного больше работы. Кроме того, при предыдущем подходе имена соответствовали фиксированным местам в памяти; требовалось применение CMOVE всякий раз, когда мы сохраняли или восстанавливали значения. - 228 - При нынешнем подходе нам нужно лишь поменять единственный указатель для смены текущей таблицы. ВЕКТОРИЗОВАННОЕ ИСПОЛНЕНИЕ ~~~~~~~~~~~~~~~~~~~~~~~~~~ Векторизованное исполнение применяет идеи потоков и перенаправления данных к функциям. Точно так же, как мы сохраняем значения и флаги из переменных, мы можем сохранять и функции, поскольку на последние также можно ссылаться по их адресу. Традиционная технология введения векторизованного исполнения описана в "Начальном курсе...", в главе 9. В этом разделе мы обсудим придуманный мною новый синтаксис, который, мне кажется, может быть использован во многих случаях более элегантно, чем традиционные методы. Этот синтаксис называется DOER/MAKE. (Если в Вашей системе отсутствют такие слова, обратитесь к приложению Б, где приведены детали их реализации и кода.) Работает это так: Вы определяете слово, поведение которого будет веторизовываться, с помощью определяющего слова DOER, например: DOER ПЛАТФОРМА Вначале новое слово ПЛАТФОРМА ничего не делает. Затем Вы можете написать слова, которые изменяют то, что делает ПЛАТФОРМА, используя слово MAKE: : ЛЕВОЕ-КРЫЛО MAKE ПЛАТФОРМА ." сторонник " ; : ПРАВОЕ-КРЫЛО MAKE ПЛАТФОРМА ." противник " ; Когда вызывается ЛЕВОЕ-КРЫЛО, фраза MAKE ПЛАТФОРМА изменяет то, что должна делать платформа. Теперь, если Вы вводите ПЛАТФОРМА, то получаете: ЛЕВОЕ-КРЫЛО ok ~~~ ПЛАТФОРМА сторонник ok ~~~~~~~~~~~~~ ПРАВОЕ-КРЫЛО заставит слово ПЛАТФОРМА печатать "противник". Можно использовать платформу и внутри другого определения: : ЛОЗУНГ ." Наш кандидат - последовательный " ПЛАТФОРМА ." больших дотаций для промышленности. " ; Выражение ЛЕВОЕ-КРЫЛО ЛОЗУНГ - 229 - будет отражать направление одной предвыборной компании, в то время, как ПРАВОЕ-КРЫЛО ЛОЗУНГ будет отражать второе направление. Код "MAKE"-слова может содержать любые Форт-конструкции, по желанию сколь угодно длинные; следует только помнить о необходимости заканчивать его точкой с запятой. Эта последняя служит в конце левого-крыла не только этому крылу, но и части кода после слова MAKE. Когда слово MAKE перенаправляет слово-DOER, оно одновременно `останавливает` исполнение того определения, в котором само находится. Когда, к примеру, Вы вызываете ЛЕВОЕ-КРЫЛО, MAKE перенаправляет слово ПЛАТФОРМА и завершает исполнение. Вызов левого-крыла на приводит к распечатке "сторонника". На рисунке 7-7 эта точка показана с использованием иллюстрации состояния словаря. Рис.7-7. Слова DOER и MAKE. DOER ДЖО ok +--------+ ~~~ | ДЖО | +--------+ Создает слово-DOER с именем ДЖО, которое ничего не делает. : ТЕСТ MAKE ДЖО 1 . ; ok +--------+ ~~~ | ДЖО | +--------+ +--------+--------+--------+--------+--------+--------+ | ТЕСТ | MAKE | ДЖО | 1 | . | ; | +--------+--------+--------+--------+--------+--------+ Определяет новое слово с именем ТЕСТ. ТЕСТ ok +--------+ ~~~ | ДЖО |---------------------- +--------+ \/ +--------+--------+--------+--------+--------+--------+ | ТЕСТ | MAKE | ДЖО | 1 | . | ; | +--------+--------+--------+--------+--------+--------+ MAKE перенаправляет ДЖО таким образом, что оно указывает на код после выражения MAKE ДЖО, а затем прекращает исполнение остальной части слова ТЕСТ. ДЖО 1 ok ~~~~~ Исполняет код, на который указывает ДЖО ( 1 . ). - 230 - Если Вы хотите `продолжить` исполнение, то можете использовать вместо точки с запятой слово ;AND. Слово ;AND завершает код, на который направляется слово-DOER и производит исполнение определения, в котором оно применяется, как показано на рис. 7-8. Наконец, можно связывать `цепи` слов-DOERов при помощи `не` использования слова ;AND. Рисунок 7-9 это поясняет лучше, чем мне удалось бы это описать. Рис.7-8. Несколько слов MAKE, параллельно использующих ;AND. DOER СЭМ ok ~~~ DOER БИЛЛ ok ~~~ +--------+ +--------+ | СЭМ | | БИЛЛ | +--------+ +--------+ Создание двух слов типа DOER, которые ничего не делают. : ТЕСТБ MAKE СЭМ 2 . ;AND MAKE БИЛЛ 3 . ; +-------+------+-----+---+---+------+------+------+---+---+---+ | ТЕСТБ | MAKE | СЭМ | 2 | . | ;AND | MAKE | БИЛЛ | 3 | . | ; | +-------+------+-----+---+---+------+------+------+---+---+---+ Определение нового слова по имени ТЕСТБ. ТЕСТБ ok ~~~ +--------+ +--------+ | СЭМ |-------- | БИЛЛ | +--------+ \/ +--------+ +-------+------+-----+---+---+------+------+------+---+---+---+ | ТЕСТБ | MAKE | СЭМ | 2 | . | ;AND | MAKE | БИЛЛ | 3 | . | ; | +-------+------+-----+---+---+------+------+------+---+---+---+ Первый MAKE перенаправляет СЭМа так, чтобы тот показывал на код после него... +--------+ +--------+ | СЭМ |-------- | БИЛЛ |--------------- +--------+ \/ +--------+ПРОДОЛЖЕНИЕ \/ +-------+------+-----+---+---+------+------+------+---+---+---+ | ТЕСТБ | MAKE | СЭМ | 2 | . | ;AND | MAKE | БИЛЛ | 3 | . | ; | +-------+------+-----+---+---+------+------+------+---+---+---+ слово ;AND продолжает исполнение ТЕСТБ. Следующий MAKE перенаправляет БИЛЛа. СЭМ 2 ok ~~~~~ БИЛЛ 3 ok ~~~~~ - 231 - Два слова-DOERа были переустановлены одновременно единственным словом ТЕСТБ. Код для СЭМа заканчивается по ;AND, код БИЛЛа заканчивается точкой с запятой. Рис.7-9. Последовательное использование нескольких MAKE. : ТЕСТВ MAKE ДЖО 4 . MAKE ДЖО 5 . ; ok ~~~ +-------+------+-----+---+---+------+-----+---+---+---+ | ТЕСТВ | MAKE | ДЖО | 4 | . | MAKE | ДЖО | 5 | . | ; | +-------+------+-----+---+---+------+-----+---+---+---+ Определение нового слова с именем ТЕСТВ. ТЕСТВ ok ~~~ +-----+ | ДЖО |-------------- +-----+ \/ +-------+------+-----+---+---+------+-----+---+---+---+ | ТЕСТВ | MAKE | ДЖО | 4 | . | MAKE | ДЖО | 5 | . | ; | +-------+------+-----+---+---+------+-----+---+---+---+ MAKE перенаправляет ДЖО на код после MAKE ДЖО. ДЖО 4 ok ~~~~~ \/ +-------+------+-----+---+---+------+-----+---+---+---+ | ТЕСТВ | MAKE | ДЖО | 4 | . | MAKE | ДЖО | 5 | . | ; | +-------+------+-----+---+---+------+-----+---+---+---+ Исполнение кода, на который указывает ДЖО ( 4 . MAKE ...) +-----+ | ДЖО |----------------------------------- +-----+ \/ +-------+------+-----+---+---+------+-----+---+---+---+ | ТЕСТВ | MAKE | ДЖО | 4 | . | MAKE | ДЖО | 5 | . | ; | +-------+------+-----+---+---+------+-----+---+---+---+ После исполнения 4 . следующий MAKE перенаправляет ДЖО так, чтобы он показывал на пятерку. (Не было ;AND чтобы его остановить.) ДЖО 5 ok ~~~~~ +-------+------+-----+---+---+------+-----+---+---+---+ | ТЕСТВ | MAKE | ДЖО | 4 | . | MAKE | ДЖО | 5 | . | ; | +-------+------+-----+---+---+------+-----+---+---+---+ Вводя ДЖО второй раз, получаем исполнение кода, на который он теперь указывает ( 5 . ). Здесь указатель и останется. - 232 - ИСПОЛЬЗОВАНИЕ DOER/MAKE ~~~~~~~~~~~~~~~~~~~~~~~ Есть множество случаев, для которых конструкция DOER/MAKE доказала свою полезность. Вот они: 1. Для изменения состояния функции (когда внешняя проверка этого состояния необязательна). Слова ЛЕВОЕ-КРЫЛО и ПРАВОЕ-КРЫЛО изменяют состояние слова ПЛАТФОРМА. 2. Для факторизации внутренних фаз из определений родственных, но лежащих внутри таких структур управления, как циклы. Вообразите себе определение слова по имени ДАМП, расчитанного для исследования определенной области памяти. : ДАМП ( а # ) 0 DO I 16 MOD 0= IF CR DUP I + 5 U.R 2 SPACES THEN DUP I + @ 6 U.R 2 +LOOP DROP ; ~~~~~~~~~~~~~~~~~ Проблема возникает, когда Вы пишете определение с именем СДАМП, расчитанным на печать не в по-словном, а по-байтном формате: : ДАМП ( а # ) 0 DO I 16 MOD 0= IF CR DUP I + 5 U.R 2 SPACES THEN DUP I + C@ 4 U.R LOOP DROP ; ~~~~~~~~~~~~~~~ Код в этих двух определениях одинаков, за исключением подчеркнутых фрагментов. Но их факторизация затруднена, поскольку они находятся внутри циклов DO LOOP. Вот решение этой проблемы с использованием DOER/MAKE. Код, содержащий различия, был заменен на слово .ЯЧЕЙКИ, поведение которого векторизуется кодом в ДАМПе и СДАМПе. (Обратите внимание на то, что "1 +LOOP" дает тот же эффект, что и "LOOP".) DOER .ЯЧЕЙКИ ( а -- прибавка ) \ распечатать байт или слово : <ДАМП> ( а # ) 0 DO I 16 MOD 0= IF CR DUP I + 5 U.R 2 SPACES THEN DUP I + .ЯЧЕЙКИ +LOOP DROP ; : ДАМП ( а # ) MAKE .ЯЧЕЙКИ @ 6 U.R 2 ;AND <ДАМП> ; : СДАМП ( а # ) MAKE .ЯЧЕЙКИ C@ 4 U.R 1 ;AND <ДАМП> ; Обратите внимание на то, как ДАМП и СДАМП `выставляют` вектор, а затем переходят к `исполнению` (слово <ДАМП>). 3. Для изменения состояния родственных функций после вызова единственной команды. К примеру: - 233 - DOER TYPE' DOER EMIT' DOER SPACES' DOER CR' : ВИДИМО MAKE TYPE' TYPE ;AND MAKE EMIT' EMIT ;AND MAKE SPACES' SPACES ;AND MAKE CR' CR ; : НЕВИДИМО MAKE TYPE' 2DROP ;AND MAKE EMIT' DROP ;AND MAKE SPACES' DROP ;AND MAKE CR' ; Мы здесь определили набор векторизованных определений для вывода, имена которых имеют на конце значок "вторичности". Слово ВИДИМО устанавливает их на соответствующие функции. Слово НЕВИДИМО переводит их в нерабочее положение, съедая аргументы, которые нормально должны были бы быть ими использованы. Говорим НЕВИДИМО - и все слова, определенные в терминах этих четырех операторов вывода `не` будут производить вывод. 4. Для изменения состояния только для одного следующего вхождения, а затем изменение состояния (или восстановления) вновь. Представим себе, что мы пишем приключенческую игру. Когда игрок впервые входит в определенную комнату, игра должна показать ее подробное описание. Если же он позже вновь в нее позже возвращается, игрушка должна выдать короткое сообщение. Мы пишем: DOER АНОНС : ДЛИННЫЙ MAKE АНОНС CR ." Вы в большом тронном зале с высоким троном," CR ." покрытым красным бархатным ковром." MAKE АНОНС CR ." Вы в тронном зале." ; Слово АНОНС будет показывать одно из сообщений. Вначале мы говорим ДЛИННЫЙ, инициализируя АНОНС на длинное сообщение. Теперь мы можем проверить АНОНС и убедиться, что он действительно печатает подробное описание. После завершения этого следующим этапом он "делает" анонс коротким. Если мы снова попробуем АНОНС, то он напечатает лаконичное сообщение. И так будет до тех пор, пока мы не скажем ДЛИННЫЙ опять. В результате мы устанавливаем очередь поведений. Мы можем создать такую очередь для любого числа поведений, позволяя - 234 - каждому из них выставлять последующее. Нижеприведенный пример (хотя и не ужасно практически полезный) иллюстрирует такой метод. DOER ГДЕ VARIABLE РУБАШКА VARIABLE ШТАНЫ VARIABLE ГАРДЕРОБ VARIABLE МАШИНА : ПОРЯДОК \ определить порядок поиска MAKE ГДЕ РУБАШКА MAKE ГДЕ ШТАНЫ MAKE ГДЕ ГАРДЕРОБ MAKE ГДЕ МАШИНА MAKE ГДЕ 0 ; : ШАРИТЬ ( -- a|0 ) \ искать место, где находится 17 ПОРЯДОК 5 0 DO ГДЕ DUP 0= OVER @ 17 = OR IF LEAVE ELSE DROP THEN LOOP ; В этом отрывке мы создали список переменных, затем определили ПОРЯДОК, в котором они должны просматриваться. Слово ШАРИТЬ проверяет каждую из них в поисках содержащей число 17. ШАРИТЬ возвращает либо адрес соответствующей переменной, либо ноль, если ни одна из них не содержит этого числа. Оно делает это, просто исполняя слово ГДЕ пять раз. И каждый раз ГДЕ возвращает разные адреса, по порядку, и в конце концов - ноль. Мы можем даже определить слово-DOER, которое бесконечно переключает свое собственное поведение: DOER РЕЧЬ : ПЕРЕКЛЮЧИТЬ BEGIN MAKE РЕЧЬ ." ПРИВЕТ " MAKE РЕЧЬ ." ПРОЩАЙ " 0 UNTIL ; 5. Для создания ссылки вперед. Такая ссылка обычно нужна в качестве "затычки", то есть слова, вызываемого в определении нижнего уровня, действие которого определится только в компоненте, который будет создан в программе позже. Для реализации ссылки вперед постройте слово с помощью DOER до первого использования его имени. DOER ПОКА-НЕ-ОПРЕДЕЛЕНО Позже в программе используйте MAKE: MAKE ПОКА-НЕ-ОПРЕДЕЛЕНО ВЕСЬ ЭТОТ ШУМ ; - 235 - (Помните, что MAKE может использоваться и вне определения через двоеточие.) 6. Рекурсия, прямая и косвенная. Прямая рекурсия возникает тогда, когда слово обращается к самому себе. Хорошим примером может послужить рекурсивное определение наибольшего-общего-делителя: НОД от а, б = а если б = 0 НОД от б, а mod б если б > 0 Это отлично переводится в: DOER НОД ( а б -- нод) MAKE НОД ?DUP IF DUP ROT ROT MOD НОД THEN ; Косвенная рекурсия имеет место тогда, когда одно слово вызывает второе слово, а второе слово вызывает первое. Это можно устроить, используя конструкцию: DOER Б : А ... Б ... ; MAKE Б ... А ... ; 7. Отладка. Я часто определяю: DOER SNAP (сокращение от SNAPSHOT - моментальный снимок), затем внедряю SNAP в свою задачу в точке, в которой хочу ее проконтролировать. К примеру, вызывая SNAP внутри цикла интерпретатора клавиатуры, я могу так настроить SNAP, чтобы наблюдать за тем, что происходит в структуре данных по мере нажатия клавиш. И я могу изменить функцию SNAP без перекомпиляции всего цикла. Ситуации, в которых предпочтителен подход с использованием ' и EXECUTE, возникают тогда, когда необходимо передать управление через адрес вектора, например, при векторизации через таблицу решений, или при сохранении/восстановлении содержимого вектора. ИТОГИ ~~~~~ В этой главе мы исследовали все за и против использования стека или переменных и других структур данных. Использование стека предпочтительней для обеспечения тестирования и возможности повторного использования, однако слишком большое - 236 - количество чисел для манипуляций на стеке в пределах одного определения вредит удобству как при написании, так и при чтении программ. Мы исследовали также технику сохранения и восстановления структур данных и в заключение изучили векторизованное исполнение с использованием конструкций DOER/MAKE. ЛИТЕРАТУРА ~~~~~~~~~~ 1. Michael Ham, "Why Novices Use So Many Variables," `FORTH Dimensions`, vol. 5, no. 4, November/December 1983. 2. Daniel Slater, "A State Space Approach to Robotics," `The Journal of FORTH Applications and Research`, 1, 1 (September 1983), 17.