Создание слов-определителей
Замечательная способность языка Форт к расширению является следствием наличия в нем так называемых определяющих слов. Единственное назначение этих слов состоит в компиляции (определении или создании) других слов. Наиболее важным из определяющих слов (слов-описателей) является : (двоеточие). Из других слов, с которыми вы к настоящему времени познакомились, это CREATE, VARIABLE. CONSTANT и т. п. Во время исполнения определяющего слова оно создает новое слово, помещая а словарь заголовок создаваемого слова, после которого следует все остальное, что необходимо для исполнения нового слова. В заголовке содержатся имя слова и некоторая дополнительная информация. На любом этапе программирования на Форте определяющие слова используются для соединения между собой простых программ в более сложные. Исключительной особенностью Форта, которую мы рассмотрим в этой главе, является то, что вы сами можете создать новые определяющие слова, не ограничиваясь теми, которые предусмотрены в базовом языке Форт. И при этом создание новых определяющих слов производится так же просто, как и создание "обычных" слов языка. Это открывает неограниченные возможности для создания новых типов слов и новых типов данных, которые могут сделать ваши программы более эффективными и в то же время облегчить их написание.
Порождающие и порождаемые слова
Каждое определяющее слово, входящее в ядро языка Форт, способно породить определенный класс слов. Например, хотя каждое слово, которое определяется с помощью : (слова-двоеточия), выполняет отличные от других слов действия, но они сходны между собой по способу определения, компиляции и исполнения. Все слова, определенные через : (двоеточие), принадлежат к одному классу, поскольку они составляются из более простых слов для объединения функций этих слов. Аналогично все слова, создаваемые с помощью слова-определителя CONSTANT, относятся к классу "констант", потому что все они одинаково компилируются и исполняются. Следовательно, каждое слово можно отнести к какому-либо классу в соответствии с порождающим его словом.
Отношение между словами можно сделать более ясным, если назвать определяющие слова словами-родителями, а порожденные ими слова словами-детьми. Все слова-дети общего слова-родителя ведут себя сходным образом, но все же отлично от других слов-детей, родных им "по крови". Общее в поведении всех "единокровных" слов-детей является следствием того, что они происходят от одного "родителя". Различия между "детьми" определяются тем, что при создании в них были скомпилированы различные значения. Чтобы понять, почему слова-дети ведут себя так или иначе, вы должны проанализировать определение слов-родителей и их собственные определения.
Если описать "генеалогию" слов, то мы сможем выделить три стадии, которые в литературе по языку Форт называют ходом событий. Ход событий - это то, что происходит, когда: 1. Рождается слово-родитель (компиляция определяющего слова). 2. Родитель активен и порождает (исполнение определяющего слова слово-ребенка и компиляция слова-ребенка) 3. Ребенок действует самостоятельно (исполнение слова-ребенка)
Причина того, что мы выделяем три, а не четыре стадии, очень простая: когда определяющее слово исполняется, оно компилирует слово-ребенок. Поэтому то, что кажется двумя стадиями, на самом деле это одно действие. Общее в поведении слов-детей, происходящих от одного "родителя", предписывается им на первой стадии при определении слова-родителя. Слова, определенные с помощью, похожи по своему поведению из-за того способа, которым определено само это слово, а одинаково они исполняются потому, что одно из действий определяющего слова - сделать так, чтобы "дети" вели себя одинаково. Это легче всего проследить на примере слов-детей, происходящих от VARIABLE и CONSTANT.
Все переменные кладут в стек адрес, по которому записано их содержимое, в то время как константы выдают в стек свои значения. Различие их действия обусловлено способом определения слов VARIABLE и CONSTANT. Различия в поведении слов-детей закладываются на стадии 2 при исполнении определяющего слова и компиляции слова-ребенка.
Слова-дети действуют по-разному, потому что они различаются по своему содержимому. Каждое слово, определенное через двоеточие, отличается от других, потому что в его определении используются другие комбинации слов. Константы отличаются друг от друга значениями величин, с которыми они были созданы. Поэтому мы можем сказать, что слова-дети общего порождающего слова-родителя одинаковы, поскольку они одинаково исполняются, но различны потому, что содержат различные значения величин или адресов (помимо того, что они имеют разные имена и расположены в словаре в разных местах). Применяя терминологию указанных трех стадий или хода событий, еще раз посмотрим на их последовательность:
Событие 1: Создается определяющее слово для компиляции слов-детей с определенным типом поведения.
Событие 2: Исполняется определяющее слово для того, чтобы создать слово-ребенок со своим содержимым и поведением.
Событие 3: Исполняется слово-ребенок в соответствии с тем, что слово-родитель научило слово-ребенка делать со своим содержимым.
Может показаться мистическим, что одно слово способно определить, как будет исполняться другое слово, но в самом деле это совсем просто. Когда определяющее слово порождает слово-ребенка, то кроме записи его содержимого слово-родитель записывает в него также адрес машинного кода стадии исполнения. Код стадии исполнения - это программа на машинном языке, которая описывает, как должно вести себя слово-ребенок, т.е. что оно должно делать со своим содержимым. Так как каждое определяющее слово записывает адрес специфического кода стадии исполнения во вес свои слова-дети, они и исполняются одинаково. (В гл.15 мы более детально рассмотрим действие кода стадии исполнения в словах, определенных через двоеточие.)
Определяющие слова
Мы рассмотрели, как используются определяющие слова (слова-определители) для порождения своих "отпрысков", но как же создаются сами определяющие слова? С первого взгляда может показаться, что можно ответить на этот вопрос, просматривая содержимое этих слов в словаре.
Но это не так уж просто. Определение слова : в ядре языка выглядит так, как будто оно само определено через двоеточие, но это, очевидно, абсурд. Еще более странно то, что слова, которые, казалось бы, определены через :, находятся в словаре раньше, чем само :. Как разрешить проблему первичности курицы и яйца?
Конечно, ядро Форта было создано без использования языка Форт (по крайней мере, в обычном смысле), хотя оно выглядит так, как будто бы,с использованием. Ядро должно было быть написано на другом языке - либо на ассемблере, либо на языке высокого уровня. В этом все дело. Практически можно написать Форт-систему, используя другую Форт-систему. Создание нового Форта с использованием Форта производится программой, которая называется метакомпилятором, и о нем уместно сказать несколько слов.
Метакомпиляция - это разновидность обычной компиляции Форта (т.е. процесса добавления новых слов в словарь). Но вместо того, чтобы строить словарь, начиная с адреса, на который указывает слово HERE, "метасловарь" размещается в некотором другом месте памяти. В памяти создается "образ" всего кода, который потребуется новому Форту; этот код затем выгружается на диск таким образом, чтобы его можно было запустить на исполнение с помощью операционной системы компьютера. Метакомпиляция может породить копию Форта или новый Форт, специализированный для определенных задач, даже для выполнения на ЭВМ другого типа. Независимо от того, был ли Форт создан на языке ассемблера, метакомпилятора или другом языке высокого уровня, кажущийся парадокс появления в словаре слов, определенных через двоеточие, раньше определения двоеточия, объясняется тем, что при создании самого ядра использовался совершенно другой способ, отличающийся от добавления к словарю новых слов. Теперь мы выяснили, как в Форте создаются определяющие слова и "обычные" слова, входящие в ядро Форт-системы. Ну а может ли пользователь создать новые определяющие слова и как?
Ответом на этот вопрос является слово CREATE, которое может использоваться самостоятельно или совместно с другим словом DOES>.
Слово CREATE дает само по себе наиболее общий способ определения новых слов в Форте. Как мы уже видели в гл.6, слово CREATE используется в форме CREATE CHILD-WORD где CHILD-WORD - это определяемое слово. Напомним вкратце: действие CREATE состоит в том, что оно помещает в словарь заголовок для слова CHILD-WORD, а когда это слово исполняется, то в стек кладется адрес его содержимого. Слово CREATE не резервирует никакого пространства после заголовка определяемого слова; резервирование места выполняется отдельной операцией, обычно словами С,, , (запятая) или ALLOT. Вы знаете также, что определение : VARIABLE CREATE 0 , ; создает VARIABLE как определяющее слово, которое может использоваться для определения произвольного числа переменных, каждая из которых оказывается инициализирована нулем. Определение слова VARIABLE является действием первой стадии. Действие второй стадии происходит, когда мы используем слово так, как, например, в данном случае: VARIABLE DISCOUNT Ее можно разложить на отдельные события: VARIABLE Начинает исполнение определяющего слова CREATE Делает имя "DISCOUNT" словом в словаре Запоминает адрес кода стадии исполнения в DISCOUNT 0 , Компилирует в DISCOUNT два байта нулей
Действие VARIABLE на третьей стадии происходит тогда, когда исполняется слово DISCOUNT, т.е. когда исполняется код стадии исполнения, который был записан в содержимое DISCOUNT словом CREATE, при этом в стек помещается адрес содержимого DISCOUNT.
Любое слово, определенное через двоеточие, которое содержит как часть своего определения слово CREATE, является новым определяющим словом. Как можно было бы определить CONSTANT ? Казалось бы, это можно сделать следующим образом: : BAD-CONSTANT CREATE , @ ; (Плохая_константа) но мы сразу же замечаем, что слово BAD-CONSTANT работать не может, так как операция @ будет совершаться на второй стадии, когда создается слово-ребенок, а не тогда, когда это слово должно исполняться. В действительности нам нужно определить слово CONSTANT так, чтобы содержимое слова-ребенка извлекалось на третьей стадии.
Это достигается с помощью слова DOES>. Но прежде чем рассматривать, как это осуществить, проделаем несколько упражнений, в которых мы дополнительно познакомимся с применением слова CREATE.
Упражнения
1. Опишите определяющее слово 2VARIABLE. которое должно создать переменную для хранения чисел двойной длины. Определите его таким образом, чтобы переменная инициализировалась нулем, и так, чтобы оно компилировало двойное число в стек, когда исполняется 2VARIABLE (последний метод используется в нескольких нестандартных версиях форта для всех переменных). 2. Опишите определяющее слово $CONSTANT, которое при исполнении в форме $CONSTANT строка" будет запоминать в словаре строку как счетную. При исполнении COUNT TYPE строка должна выводиться на экран. 3. Опишите слово-определитель RESERVE, которое должно создавать слова, для которых в словаре резервируется n байтов. Таким образом, с помощью 10 RESERVE 10BYTES определяется слово 10BYTES, которое, в свою очередь, резервирует 10 байтов. Напишите слово RESERVE, которое должно инициализировать все зарезервированные байты нулями. 4. Опишите слово-определитель BLOCKARRAY, которое должно запомнить число, взятое из стека, и после этого зарезервировать еще 1024 байта для содержимого блока, т.е. 213 BLOCKARRAY BLK1 должно создать слово BLK1, в которое должно быть занесено число 213, после которого должны следовать 1024 свободных байта. 5. Теперь напишите слово GETBLOCK. которое должно заполнять содержимое блочного массива содержимым указанного блока. Таким образом, BLK1 GETBLOCK должно поместить содержимое блока 213 в зарезервированное пространство в BLK1 (используйте слово nBLOCK). Напишите слово PUTBLOCK, которое должно пересылать содержимое блочного массива в блок с указанным номером. (Подсказка: вспомните действие слов UPDATE и SAVE-BUFFERS или FLUSH.) 6. Напишите два слова В@ и В!, которые должны соответственно извлекать и запоминать числа в указанном блочном массиве, т.е. 5 BLK1 В@ должно извлекать пятый элемент из блочного массива, который должен прийти из блока 213.
Эти упражнения показывают, что для хранения-извлечения и манипулирования с данными на диске могут быть полезными специальные определяющие слова и специализированные массивы. Такого рода специализированные слова особенно полезны при создании новых версий языка Форт для разработки программ обработки данных. Например, можно определить типы массивов для хранения данных в полях различных размеров, в частности для файлов инвентаризации, файла медицинских наблюдений и т.д. Имея специализированные типы массивов и специализированные определяющие слова, проще организовать слежение за тем, где и в каком формате хранятся данные. Например, массив BP-BLK (блок_давления_крови) может содержать записи о кровяном давлении пациентов, и если он организован по вышеописанной схеме, то вам не надо помнить, в каком блоке он записан. Эту идею можно распространить на соответствующие блоки для веса, роста и других показателей пациентов. Возможности здесь не ограничены.
Создание новых определяющих слов
Каждый раз, когда слово CREATE используется внутри определения через двоеточие, мы создаем новые определяющие слова. В упражнениях вы имели дело с разнообразными определяющими словами, которые по-разному действовали при компиляции слов-детей, но все порожденные слова-дети действовали одинаково при исполнении: они оставляли адрес своего содержимого в стеке. Слово DOES> нужно для того, чтобы определяющее слово задало способ поведения слова-ребенка на стадии исполнения. Теперь мы можем определить константу следующим образом: : CONSTANT CREATE , DOES> @ ;
На первой стадии деятельность слова CONSTANT проявляется во время его компиляции. Если слово CONSTANT исполняется, например, для компиляции слова 1024 CONSTANT 1K то на второй стадии действия слова CONSTANT можно расчленить следующим образом: CONSTANT Начинает исполнение определяющего слова CREATE Заносит в словарь имя "1K" Запоминает адрес кода стадии исполнения в слове 1K , Компилирует число 1024, взятое из стека
Из присутствия в определении DOES> @ мы узнаем, что на третьей стадии действие слова CONSTANT (при исполнении 1K) более сложное, чем в случае VARIABLE.
При исполнении 1K вначале в стек кладется адрес содержимого 1K (потому что слово CREATE помещает код стадии исполнения в 1K, чтобы работать таким образом) и после этого @ извлекает содержимое из этого адреса, помещая в стек число 1024. Другими словами, оператор @, следующий после DOES>, исполняется тогда, когда слово-ребенок исполняется, а не тогда, когда оно определяется.
Определение слова CONSTANT является отличным примером создания новых определяющих слов. Слова, которые находятся между CREATE и DOES>, исполняются на второй стадии, т.е. тогда, когда исполняется слово-родитель и компилируется слово-ребенок. Когда исполняется само слово-ребенок, то вначале оно помещает в стек адрес его содержимого, а потом исполняются слова, которые находятся в определяемом слове после DOES>, описывающие, что должно делать слово-ребенок,
Приведем пример использования определяющих слов, с которыми мы вновь встретимся в гл. 12 и 13, когда будем обсуждать разработку программы-редактора. Как вы уже знаете, многие терминалы и принтеры управляются кодами ASCII со значениями 0 - 31 (их называют управляющими). Значения управляющих кодов должны быть записаны в константах и выводиться на терминал словом EMIT, но лучше для этого определить специальное слово IS-CONTROL - это не что иное, как CONSTANT, в которое добавлено слово EMIT, описывающее поведение слова IS-CONTROL при исполнении. Слово IS-CONTROL. можно использовать для создания целого семейства родственных слов, например: 7 IS-CONTROL BELL (эвуковой_сигнал) 8 IS-CONTROL BACKSPACE (возврат_влево) 12 IS-CONTROL FORMFEED (подача_страницы) 13 IS-CONTROL CR (возврат_каретки) где каждое слово будет задавать терминалу определенное действие. Одно из достоинств определяющих слов уже очевидно: они способствуют улучшению читабельности программ. Например, при использовании слова IS-CONTROL нужно только конкретизировать данные, которыми отличается новое слово от других слов-детей, порождаемых словом IS-CONTROL, а именно управляющим кодом и именем.
Каждое слово-ребенок в семействе определений имеет свою индивидуальность. Определяющие слова дают вам возможность разграничить общее поведение слов, имеющих общее происхождение, и их индивидуальное поведение. Общее поведение слов-детей запрограммировано в исполнительной части определения слова, следующей после DOES>. Индивидуальное поведение слов-детей определяется значением (или значениями), которое находилось в стеке, когда оно создавалось, и, конечно, его уникальным именем.
Задачи, для которых требуется некоторое количество слов, имеющих сходные определения, лучше всего решаются при помощи нового определяющего слова. Вот еще один пример. В гл. 4 мы показали вам способ представления математических функций на Форте. С помощью определяющего слова можно создать любое количество линейных уравнений вида у = ax + b путем создания слов-детей с коэффициентами а и Ь, находящимися в стеке. Если затем будет исполняться слово-ребенок и в стеке находится значение х, то оно будет оставлять значение решения - у. Приведем определение этого определяющего слова: : LINEAR ( а b --) (линейная функция) CREATE SWAP , , DOES> DUP >R @ * R>
2+ @ + ;
Обратите внимание, что величины а и b переставляются в стеке при создании слова LINEAR, чтобы избавиться от стековых манипуляций на стадии исполнения. Если мы определили линейное уравнение у=3х+17 при помощи 3 17 LINEAR ALINE то, когда оно будет исполняться в форме 2 ALINE мы увидим решение 23. Исполнение слова ALINE можно описать следующим образом: DUP ( -- 2 адр адр) Адрес числа 3 in ALINE >R ( -- 2 адр) Помещает адрес в стек возвратов @ ( -- 2 3) Извлекает число 3 (а) * ( -- 6 ) 6 = ax, первый член R> ( -- 6 адр) Адрес числа 3 в ALINE 3+ ( -- 6 адр+2) Адрес числа 17 в ALINE @ ( -- 6 17) Извлекает 17 (b), второй член + ( -- 23) 23 = у = ax + b, решение
Пример показывает общую методику, используемую в сложных определяющих словах. Так как адрес первого элемента ALINE потребуется для извлечения двух чисел, его запоминают в стеке возвратов.
После извлечения адреса из стека возвратов его нужно увеличить на 2, чтобы указать на следующий элемент ALINE, при этом в него будет скомпилировано число 17, которое надо извлечь. Хотя в этом примере мы обошлись обычными словами для стековых манипуляций, в случае, если в словах-детях нужно запомнить несколько чисел или байтов, могут потребоваться некоторые изменения технических приемов.
Лучше всего можно оценить мощь определяющих слов из практических примеров. Из приведенных ниже упражнений вы сможете извлечь еще некоторые идеи.
Упражнения
1. Определите слово 2CONSTANT, которое должно работать так же, как и CONSTANT, но с двойными числами. 2. Определите слово MAKEDATE (создать_дату), для которого в стеке должны находиться числа: месяц, день и год, чтобы оно при исполнении выдавало дату с косой чертой в качестве разделителя. Например, 12 7 41 MAKEDATE PEARLHARBOR должно создать слово PEARLHARBOR, которое при исполнении должно выдавать дату в виде 12/07/41. (Вспомните вывод по шаблону из гл.5.) 3. Определите определяющее слово COUNTEB (счетчик), которое использует число из стека, чтобы инициализировать порождаемые им слова. Когда исполняются слова-дети COUNTER, то при очередном исполнении их содержимое должно изменяться таким образом, что 0 COUNTER COUNTIT должно создать COUNTIT, которое при исполнении будет последовательно изменять свое содержимое: 1, 2, 3 и т.д. Как можно извлечь содержимое COUNTIT ? 4. Как можно использовать слова, производные от COUNTER, для подсчета частоты использования определенных слов в программе? 5. Для регистрации цвета фотографических красителей экспериментально определяют насыщенность составляющих цветов : голубого, желтого и пурпурного, значения каждой из которых могут изменяться в диапазоне от 0 до 255. Определите слово COLOR, которое берет из стека величины насыщенности цветов, а порождаемые им слова должны выдавать насыщенность каждого из трех цветов в процентах. Так, например, 128 128 128 COLOR 1DYE будет создавать слово IDYE, которое при исполнении должно вывести "50% голубого, 50% желтого, 50% пурпурного".
Возможно, что прежде, чем определить само слово COLOR, вы захотите определить три слова, которые будут выводить процентное содержание цвета. 6. Определите слово QUADRATIC, которое работает подобно LINEAR, но определяет порождаемые им слова, выдающие в стек решение уравнения у = ах2 + bх + с, если вначале в стеке находится х. 7. Уравнение Михаэлиса-Ментена широко применяется в биологии и биохимии для определения скорости энзиматических реакций. Его общий вид такой: Q = QмаксS / (KM + S), где Q - скорость реакции, S - концентрация субстрата, Qмакс - максимальная скорость реакции и KM - константа полунасыщения, т.е. концентрация субстрата, при которой скорость реакции составляет половину от максимального значения. Напишите определяющие слова для решения этого уравнения, причем в фазе компиляции в стеке находятся значения Quarr и КМ, а значение S задается в стеке во время исполнения. 8. Многочлен n-й степени является очень полезной функцией ввиду того, что он может "имитировать" почти любую другую функцию. Многочлен имеет вид у = а1 + а2x +а3x^2 +... + аnx^(n-1) Напишите порождающее слово POLYNOM, для которого в стеке заданы некоторое количество коэффициентов (т.е. значений аi), и производные от него слова-дети будут рассчитывать значение полинома (у), используя эти коэффициенты. Вам потребуется использовать в определяющем слове циклы DO-LOOP после CREATE и после DOES>. Понятно ли вы вам, как POLYNOM может заменить слова QUADRATIC и LINEAR ? 9. Определите слово , которое должно использоваться в форме мин макс n A-CONSTANT где A-CONSTANT будет иметь начальное значение п, но возвращать число мин, если его содержимое меньше, чем мин. или число макс, если его содержимое больше, чем макс. (Содержимое A-CONSTANT должно быть изменено при помощи ' или ' >BODY и !.)
Определение массивов
В гл.6 мы ознакомились с созданием одномерных массивов с помощью слов CREATE и ALLOT, однако, чтобы извлекать или записывать данные, нам приходилось вычислять величину смещения адреса.
Слова- определители находят превосходное применение при создании массивов, поскольку с их помощью можно создать описание массива с определенной размерностью, которое при исполнении будет выдавать адрес нужного элемента в стек. Приведем пример слова-определителя для создания массива из символов, или байтов, с именем CARRAY. Один из возможных вариантов слова-определителя такой: : CARRAY CREATE ALLOT DOES> + ;
Если его использовать в форме 30 CARRAY NOVEMBER то будет создан массив из 30 байтов, элементы которого нумеруются числами от 0 до 29. На стадии исполнения слова-ребенка требуется наличие в стеке номера элемента массива, чтобы вычислить его адрес. Таким образом, при исполнении 1 NOVEMBER C@ . будет рассчитан адрес второго байта в массиве NOVEMBER, извлечено его содержимое и после этого выведено на экран.
Существуют два способа нумерации элементов массивов, начиная либо с нулевого элемента (как в вышеприведенном случае), либо с элемента 1. Если вы хотите, чтобы выражение 1NOVEMBER выдало адрес первого (а не второго) байта в массиве NOVEMBER, определите CARRAY таким образом: : CARRAY CREATE ALLOT OOES> 1- + ; Несмотря на то, что нумерация с первого элемента приводит к небольшому проигрышу в скорости, вы убедитесь в том, что она удобнее для работы. Для создания массивов чисел одинарной длины можно использовать похожее определение (принимая также, что нумерация элементов начинается с нуля) : ARRAY CREATE 2 * ALLOT DOES> SWAP 2 * + ;
Для регистрации цвета фотографических красителей уверены, что вы сможете самостоятельно проанализировать, что делает это определение. Можете ли вы предложить определение, которое создает массив, начинающийся с первого элемента ?
В упражнениях мы предложим вам написать эквивалентное слово для создания массивов двойных чисел. Обратите внимание на то, что массивы, порождаемые этими простыми определениями, будут безропотно возвращать в стек адреса элементов, находящихся вне резервированного адресного пространства, если номер элемента выходит из заданного диапазона.
Запись данных за пределами резервированной области может привести к катастрофическим последствиям. Поэтому корректное решение задачи состоит в том, чтобы написать слово, которое производит проверку значения номера элемента массива, прежде, чем его использовать для работы с определенным массивом, на допустимость, т.е. попадание его в область разрешенных значений. Однако включение проверки в опреде ление массивов ARRAY и CARRAY приведет к снижению быстродействия, независимо от того, будет обнаружена ошибка или нет. Если скорость не очень важна, то можно переписать определения массивов, включив в них проверку ошибок. В таком случае в слово, определяющее массив, наряду с размером резервированной для массива области должно быть скомпилировано число элементов (чтобы использовать для проверки). Если принять, что номер младшего элемента массива равен 0, то одним из возможных определений, выполняющих проверку, может быть : ECARRAY ( n --) CREATE DUP , 2+ ALLOT DOES> DUP @ 3 PICK SWAP U< ( 2 PICK для Форт-83) IF + 2+ ELSE ." Ошибка индекса" ABORT THEN :
Приведенное определение можно расчленить на отдельные действия следующим образом: : ECARRAY (Имя определяющего слова) CREATE (Создает заголовок слова-ребенка) DUP (Копирует число элементов массива в стеке) (Помещает в слово-ребенок адрес кода стадии исполнения) , (Компилирует максимальный номер элемента) 2+ (Устанавливает число байтов, необходимых для компиляции массива) ALLOT (Резервирует в словаре место для массива) DOES> (Определяет поведение слова-ребенка при исполнении) (Номер элемента находится в стеке) (Оставляет в стеке адрес содержимого слова-ребенка) DUP ( -- n адр адр ) @ ( -- n адр макс ) (Максимальная размерность массива) 3 PICK ( -- n адр макс n ) (Номер элемента) (2 PICK для Форт-83) SWAP ( - n адр n макс ) U< (Номер элемента меньше допустимого?) IF (Если номер меньше допустимого...) + (Вычисляет адрес элемента и) 2+ (смещение для обхода максимального значения) ELSE (Но, если номер элемента слишком большой) ." Ошибка индекса" (то выдает сообщение об ошибке) ABORT (очищает стек и прекращает работу) THEN ; (Заканчивает определение)
Не могли бы вы попытаться несколько повысить эффективность этого определения, используя стек возвратов?
Слово для проверки недопустимого значения индекса массива в определении чисел одинарной длины будет иметь такую же форму, однако в нем должна быть предусмотрена возможность использования для каждого числа двух байтов. Чтобы обнаружить, что индекс слишком велик или попадает в область отрицательных значений, используется оператор сравнения U
Упражнения
1. Создайте слово для определения массивов чисел двойной длины DARRAY, которое работает так же. как CARRAY и ARRAY. 2. Перепишите определения ARRAY и EARRAY с именами 1ARRAY и 1EARRAY, принимая номер начального элемента равным I. (Помните о необходимости ввести проверку ошибки в EARRAY.) 3. Напишите новые версии слов CARRAY и ARRAY, назвав их OCARRAY и OARRAY, которые инициализируют все элементы массива-ребенка нулями. 4. Напишите определяющее слово PRESERVE, которое должно скомпилировать все слова, находящиеся в массиве, в именованный массив. 5. Модифицируйте слово, определенное в упражнении 4 (назовите его SAVE-TO-RETURN), которое при исполнении возвращало бы в стек числа в первоначальной последовательности. 6. Это полезное, хотя и не очень приятное упражнение. Создайте слово .WORD, производные слова от которого при исполнении просто печатают свое имя. Таким образом, в результате исполнения .WORD .Nasty будет создано слово .Nasty, которое будет печатать на экране "Nasty".
Отвлечение: реализация игры "Жизнь"
Большинство из тех, кто был связан с работой на-компьютерах в 70-х гг., по крайней мере, слышали что-либо об игре "Жизнь", придуманной английским математиком Джоном Конвейем. Статья Мартина Гарднера в рубрике "Математические игры" в журнале Scientific American вызвала повальное увлечение, которое привело, как говорят, к такому расточению машинного времени на ЭВМ, как ни одна другая проблема. В наше время вследствие возросшего искусства программистов эта задача не представляется более бросающей вызов. (Например, с MMSFORTH поставляется программа, занимающая всего 5 блоков.)
Принципы игры "Жизнь" очень простые: перед началом игры с помощью простого редактора на экране дисплея изображаются колонии "клеточных бактерий" (представленные простыми графическими образами или буквами). После того как введена картина их исходного расположения, начинается жизнь первого "поколения". У каждой клетки имеется восемь соседних позиций
Для программирования игры "жизнь" нужно создать два массива. Первый массив - это массив клеток, изображаемых на экране. Второй - массив числа соседей каждой клетки. После завершения подсчета числа соседей значения элементов массива используются для определения мест, где клетки умирают, продолжают жить или возникают. Эта информация используется для обновления первого массива, который будет представлять следующее поколение. Вас может заинтересовать программирование игры "Жизнь" на Форте, однако мы ограничимся здесь составлением программы только для одномерного случая игры LINELIFE, в которой каждая клетка может иметь только двух соседей: слева и справа. В этом случае умирает клетка, не имеющая соседей или имеющая двух соседей, но те, которые имеют только одного соседа, будут выживать. Если соседи находятся по обе стороны от незанятой позиции, то в ней возникает новая клетка. Задача составления этой программы для нас состоит не столько в том, чтобы сделать интересную игру, сколько в том, чтобы рассмотреть еще один пример программирования с использованием конструкции слов-определителей CREATE...DOES.
Программа начинается с объявления констант 42 CONSTANT CHAR 66 CONSTANT LLEN где CHAR - это символ "*", используемый для изображения клетки, а LLEN - это увеличенная на 2 длина строки, выводимой на экране. Слово LLEN делается на два больше длины выводимой строки для того, чтобы программа подсчета числа соседей могла работать и с крайними позициями строки. Следующим шагом будет резервирование места для двух массивов с помощью CREATE IMAGE LLEN ALLOT IMAGE 1+ CONSTANT *IMAGE CREATE CALCS LLEN ALLOT CALCS 1+ CONSTANT *CALCS где IMAGE - это строка, которая должна быть выведена на экран, а CALCS - массив информации о числе соседей каждой клетки.
Первые позиции в IMAGE и CALCS названы соответственно #IMAGE и #CALC. Теперь мы определим слова : CLEAR- IMAGE IMAGE LLEN 32 FILL ; (очистка_изображения) : CLEAR-CALCS CALCS LLEN 0 FILL ; (очистка__счетчика_соседей) для того, чтобы производить очистку массивов, заполняя IMAGE пробелами и CALCS нулями. Для каждой клетки, обнаруженной в массиве IMAGE, мы должны в соответствующих элементах справа и слева от этой клетки массива CALCS увеличить их содержимое на единицу. Эта задача решается двумя словами: : INCS ( адр --) >R R@ C@ 1+ R@ С! (Слева от клетки) R@ 1+ С@ 1+ R@ 1+ С! (На месте клетки) R@ 2+ С@ 2 R> 2+ С! (Справа от клетки) ; : INC-CALC ( п -) DUP *IMAGE + С@ CHAR = IF *CALCS + 1- INCS ELSE DROP THEN ; где слово INCS инкрементирует байты по трем адресам: слева, справа и в текущей позиции, а слово INC-CALC анализирует n-й символ массива IMAGE, не равен ли он CHAR (т.е. символу, изображающему клетку), и если это так, то добавляет 1 к соответствующим элементам в массиве CALCS. Подсчет соседей в каждой позиции для всей строки производится при помощи слова: : CALCULATE CLEAR-CALC LLEN 0 DO I INC-CALC LOOP :
Оно занимается тем, что рассчитывает изменяющиеся значения элементов массива CALC для текущего расположения клеток в массиве IMAGE, затем информация, имеющаяся в массиве CALC, должна быть переведена в новую картину, соответствующую новому поколению клеток. Последнее выполняется процедурой : MAKE-IMAGE CLEAR-IMAGE LLEN 0 DO *CALCS I + C@ DUP 1 = SWAP 2 = OR IF CHAR *IMAGE I + C! THEN LOOP ; которая очищает массив IMAGE, затем с помощью цикла проходит по всем позициям массива CALCS, определяя, равно ли значение элемента 1 или 2, и если это так, то в массив IMAGE, выводимый на экран, помещается клетка (т.е. символ CHAR). Это все, что касается собственно логики программы. Остальная часть программы служит для того, чтобы загрузить ее, записать начальные значения в массивы и установить начальные условия, т.е. инициализировать эволюцию LINELIFE.
Рассмотрим слово-определитель MAKEDO.
Оно может порождать другие слова, которые будут забирать из стека различное число параметров, компилировать их и потом использовать для инициализации начала игры LINELIFE. Каждое число, которое берет из стека MAKEDO, соответствует позиции, куда должна быть помещена клетка в массив IMAGE перед началом игры; это дает вам возможность связать описание любого количества исходных картин расположения клеток с именем. Такой подход представляет собой простую альтернативу созданию специального редактора для ввода картины начала эволюции : MAKEDO CREATE DEPTH DUP С, 0 DO С, LOOP DOES> CLEAR-IMAGE DUP C@ 1+ 1 DO DUP I + C@ * IMAGE + CHAR SWAP C! LOOP DROP ;
Слово MAKEDO заслуживает более внимательного рассмотрения: : MAKEDO (Имя слова-определителя) CREATE (Компилирует заголовок слова-ребенка) (Помещает в слово-ребенок адрес кода стадии исполнения) DEPTH (Помещает в стек значение его глубины) DUP С, (DUP и компилирует глубину стека) 0 DO (Начинает просмотр стека DEPTH раз...) , (Компилирует каждый элемент из стека) LOOP (пока они не кончатся) DOES> (Начинает определение стадии исполнения слова-ребенка) (Помещает в стек адрес содержимого слова-ребенка) CLEAR-IMAGE (Перед началом очищает массив IMAGE) DUP ( -- адр адр) С@ ( -- адр глубина) (Количество чисел в стеке) 1+ ( - адр глубина+1) 1 DO (Начинает цикл извлечения скомпилированных позиций в стек) DUP ( -- адр адр) I ( Индекс цикла, начинает извлечение с 1-й позиции) + ( -- адр адр+i) (Адрес байта) С@ ( Извлекает элемент i-й позиции из слова-ребенка) *IMAGE ( -- адр адр+i адр) ( Показывает первую клетку в IMAGE) + (Адрес, по которому клетка запоминается в IMAGE) CHAR ( -- адр адр_клетки сммв) ( симв - символ клетки) SWAP ( -- адр симв адр_клетки) С! ( Записывает клетку в массив IMAGE) LOOP ( - адр) ( Проходит по всем позициям) DROP ( -- ) ( После завершения удаляет ненужный адрес) ; (Конец определения) Определение слова MAKEDO позволяет скомпилировать произвольное число клеток в LINELIFE, не указывая в явном виде, сколько их есть.
Таким образом, как 1 17 32 MAKEDO 1DO так и 2 3 4 15 16 22 33 40 MAKEDO 2DO являются одинаково правомерными определениями. При исполнении слова, определенного с помощью MAKEDO, вначале осуществляется очистка массива IMAGE, после чего производится произвольное размещение клеток в соответствии с заданным при компиляции слова-ребенка. Это первоначальное расположение клеток будет использовано для начала игры LINELIFE. Чтобы показать текущее расположение клеток, мы используем слово : SHOW-IMAGE *IMAGE LLEN 2 - -TRALING TYPE CR ;
Оно выводит LLEN байтов и делает возврат каретки. Теперь мы можем написать главное слово LINELIFE, которое показывает текущее состояние массива IMAGE (которое также порождено словом MAKEDO) и производит расчеты для неопределенного количества поколений, основываясь на начальном размещении клеток и правилах игры. Это слово определяется следующим образом: : LINELIFE CR BEGIN CALCULATE MAKE-IMAGE SHOW-IMAGE 0 UNTIL ; и может быть использовано в форме 1DO LINELIFE, 2DO LINELIFE и т.д. Несмотря на то, что программа LINELIFE сравнительно простая (и не столь интересная, как настоящая программа игры "Жизнь"), она дает нам возможность необычного использования определяющих слов в практических целях. Благодаря тому, что слово MAKEDO может обращаться с любым числом позиций в стеке при компиляции производных слов, нам удалось обойтись без редактора, необходимого для задания первоначального расположения клеток, которое требуется для начала игры LINELIFE. Слова, порождаемые MAKEDO, позволяют очень просто запоминать исходное состояние клеток для последующих проходов игры.
Прикладная программа на языке ФОРТ для сбора данных
Одной из наиболее важных областей применения Форта является взаимодействие с внешними устройствами компьютера. В нашем заключительном примере мы покажем использование конструкции CREATE... DOES> для облегчения сбора и анализа данных в реальном масштабе времени с помощью компьютера. Этот пример был взят из практического применения Форта одним из авторов в его исследовательской работе.
Собственно говоря, он изучил Форт именно потому, что ни один другой язык не позволял ему так просто добиться того, что он хотел. Задача состояла в том, чтобы снять значения разнообразных показателей свойств воды в озере с помощью датчиков, ввести значения в компьютер, а затем обработать данные, чтобы понять их изменения и взаимосвязь.
Общее число датчиков может доходить до 48, они включают в себя фотоэлементы, электроды для измерения концентрации ионов водорода (рН), приборы для измерения прозрачности воды и измерения потоков, а также электроды для измерения растворенного в воде кислорода. Сборки с датчиками погружались в воду озера. Перед нами стояла задача иметь возможность наблюдать, что измерено в данный момент, а также результаты измерений за последние 24 ч. Датчики через кабели соединялись с компьютером, который находился на берегу. Каждый прибор выдавал на выходе напряжение от 0 до 1000 мВ, пропорциональное измеряемой физической величине. Выход его был соединен с аналого-цифровым преобразователем, чтобы получить цифровую величину для ввода в компьютер через порты ввода. Для нашего примера мы будем предполагать, что написана программа на языке Форт, и с помощью нее величины, выраженные в милливольтах, вводятся в массив PORT-DATA так, что, например, с помощью 1 PORT-DATA @ можно положить в стек текущее значение из порта номер 1 в милливольтах. Программу для сбора данных написать несложно, но она будет сильно зависеть от применяемого компьютера и версии языка Форт.
Задача для нас состоит в том, чтобы преобразовать напряжение на входе в фактические значения рН, температуры и т.д. перед тем, как в дальнейшем представить их в виде таблиц, графиков и записей на диске; при этом для каждого датчика имеется своя функция преобразования. Мы ограничимся здесь только температурными измерениями, поскольку процедуры измерения других параметров во многом похожи. Любой датчик температуры дает на выходе напряжение, которое связано с температурой линейной пропорциональностью, т.е.
Т = aV + b, где Т - температура, V - напряжение, а и b - константы, которые определяются при калибровке и для каждого датчика имеют свои значения. Нам нужно для каждого датчика создать слово, которое будет брать из стека значение напряжения, рассчитывать значение температуры (на практике умноженное на 1000) и запоминать результат в массиве RESULT. Для создания слов, например, 1ТЕМР, 2ТЕМР и т.д. можно использовать слово-определитель port# a b VOLT-TO-TEMP nTEMP (#порта) где nТЕМР - слово-ребенок. Например, если для датчика температуры с номером 8, подключенного к порту номер 32, уравнение имеет вид: Т == 26v + 1200, тогда соответствующее ему слово для преобразования милливольт в значение температуры выглядит так: 32 26 1200 VOLT-TO-TEMP 8TEMP а слово для преобразования напряжение-температура VOLT-TO-TEMP можно определить следующим образом: : VOLT-TO-TEMP (Имя определяющего слова) CREATE (Компилирует заголовок слова-ребенка) (Помещает в слово-ребенок адрес кода стадии исполнения) ROT , SWAP , , (Компилирует #порта, а, b) DOES> (Начинает определение стадии исполнения слова-ребенка) (Помещает в стек адрес содержимого слова-ребенка) >R ( -- ) (Помещает адрес в стек возвратов) R@ @ ( -- #порта ) (Помещает в стек номер порта) PORT-DATA @ ( -- данные ) (Извлекает значение напряжения из порта n) R@ 2+ @ ( -- V а ) ( Извлекает а ) * ( -- V*a ) (Вычисляет значение переменной компонента) R@ 4 + @ ( -- V*a b ) (Извлекает b) + ( -- результат ) (Рассчитывает температуру) R> @ ( -- результат #порта ) (Помещает в стек номер порта) RESULT ! ( -- ) (Запоминает результат в n-й позиции, массива) ; (Конец определения) где начальные значения компилируются в слово-ребенок в такой же последовательности, в какой они появляются в стеке. При исполнении слова-ребенка, например 8TEMP, значение берется из соответствующего порта (в данном случае 32-го), преобразуется в температуру благодаря исполнению слов, находящихся в его определении после слова DOES>. В итоге результаты запоминаются в ранее определенном массиве RESULT.
Аналогичные определяющие слова существуют для каждого типа датчика, например, рН-измерителя, датчика содержания кислорода и т.д. Они будут отличаться после слова DOES>, поскольку отличаются уравнения для преобразования измеренных величин для каждого типа датчика.
Адреса производных слов помещаются в массив для векторного исполнения, который называется CONVERT- VECT, в порядке следования номеров портов, относящихся к датчикам портов.
Главное слово, которое помещает в массив RESULT результаты измерений, может быть определено довольно просто: : CONVERT-VOLTS 48 0 DO CONVERT-VECT I 2 * + @ EXECUTE ;
Хотя мы показали вам только часть программы (сбор данных, их хранение и представление гораздо сложнее), вы убедились, что слова-дети, порождаемые определяющими словами, можно легко приспосабливать к различным типам датчиков с учетом индивидуальных калибровочных характеристик. С помощью определяющих слов калибровочные параметры компилируются непосредственно в индивидуальное слово для каждого датчика. Это пример того, что с помощью определяющих слов программа на исходном языке становится более удобочитаемой, структурированной и компактной.
Выводы
Возможность создавать новые определяющие слова - пожалуй, единственное наиболее мощное средство программирования на языке Форт. Если использование конструкции CREATE.. .DOES> кажется вам странным или даже отпугивающим, не поддавайтесь искушению обойтись без них. Лучше попытайтесь поработать над словами собственного изобретения, чтобы развить первоначальное интуитивное представления о процессах, происходящих при определении новых слов. Любую программу, в которой требуются повторяющиеся определения, можно сделать более изящной, если применить слово-определитель для создания класса слов, выполняющих сходную работу. Такие программы будет легко разрабатывать, они станут более компактными и удобочитаемыми.
В гл.16 мы обсудим программирование на Форт-ассемблере, который в двух существенных моментах имеет прямое отношение к словам-определителям.
Во-первых, Форт- ассемблер представляет пример использования всей мощи слов-определителей. Так, ассемблер для микропроцессора 8080 описывается с помощью всего трех блоков исходного кода, при этом в него включены слова для организации ветвлений и циклов, а для его реализации требуется всего пять слов-определителей, чтобы создать 70 слов мнемоники ассемблера. Во-вторых, если слова, порождаемые словами-определителями, исполняются недопустимо медленно, то вы можете определить их действие на стадии исполнения с помощью слова ;CODE (если вы располагаете Форт-ассемблером). Это слово используется в определениях вместо DOES>, что позволяет применить ассемблерную мнемонику для описания действий порождаемого слова на стадии исполнения. В гл.16 мы используем слово ;CODE для создания версии слова ARRAY на ассемблере. Применение слов-определителей может коренным образом изменить ваш стиль программирования. Через некоторое время вы будете удивляться, как вы могли раньше обходиться без них.