Еще об арифметических операциях
Мы уже не раз говорили о том, что компьютеры используются не только для операций с числами, но и во многих других областях, однако, что бы они ни делали, они имеют дело с числами. Эта большая глава представляет собой что-то вроде попурри на Тему чисел. Однако если в предыдущих и большинстве следующих глав для любой серьезной работы по программированию был нужен весь материал, то некоторые части этой главы потребуются вам только в том случае, если вы будете заниматься "перемалыванием" чисел, например числовыми базами данных. Как минимум, вы должны изучить данное введение и разделы "Операторы для работы с небольшими числами", "Некоторые проблемы операции деления", "Операции с величинами и знаками чисел". Если вы работаете в стандарте Форт83, вам нужно также прочитать раздел "Деление с округлением,. деление с отрицательными числами". Если вы будете работать с очень большими числами и дробями, - вам нужно изучить разделы "Почему используются целые числа?", "Масштабирование чисел", "Числа двойной длины" и "Совместное применение чисел одинарной и двойной длины". А если вас интересует работа с числами с плавающей запятой, вы должны прочитать последний раздел. Если вы совершенно устали от арифметики, переходите к гл. 5 или даже гл. 6; для их понимания вам достаточно того, что вы уже узнали, но потом вам все равно придется вернуться, а потому наш совет - не пропускайте материал этой главы. Наконец, чтобы считать себя всесторонне образованным программистом на языке Форт. вам по требуется узнать все, что изложено в этой главе.
К настоящему времени вы изучили четыре арифметические операции: сложение, вычитание, умножение и деление и соответствующие слова Форта +, -, * и /. Нам остается мало что добавить, кроме как упомянуть о потенциально возможной проблеме арифметического переполнения. Предположим, вы складываете числа 36000 и 37000. Результат должен равняться 73000; т.е. быть больше, чем 16- разрядное число, которое может быть в стеке.
На IBM PC на выполнение 1 миллиона операций 2/ MMSFORTH расходует 22 с, а на миллион операций 2 / 80 с. Поскольку действия с числами 1 и 2 встречаются очень часто, особенно при повторяющихся операциях, использование этих слов может значительно увеличить скорость работы программ. Может быть, это и мелочь, однако это хороший пример того, как Форт оптимизирует быстродействие.
Некоторые проблемы операции деления
Предположим, что вы хотите умножить число 20000 на 5 и разделить произведение на 2, при этом должно получиться 50000. Попробуйте это сделать, введя 20000 5 * 2 / U. 42 и то, что вы увидите, будет наверняка неправильно. При умножении происходит переполнение и заведомо неверный результат умножения точно делится на 2. Но эту задачу можно решить иначе: 20000 2 / 5 * ведь не всегда же вы сможете узнать, будут ли расположены большие и малые числа в правильной поcледовательности. Слово */ разрешает эту проблему, запоминая промежуточный результат в виде 32-разрядного числа, а не 16-разрядного, как обычно, допуская значение произведения до 4271406735. Таким образом устраняется возможность переполнения.
Еще одна проблема, возникающая при делении, - это проблема остатка. При целочисленных операциях сложения, вычитания и умножения результат представляет собой другое целое число. Для деления это не так. Например, если 3 поделить на 2, должно получиться 1.5 = 1 + 5/10. Нам нужно каким-либо образом определить остаток от деления (который иногда называется модулем деления). Остаток находится с помощью слова MOD. Если вы введете 3 2 MOD . то на экране получите 1. 25 7 MOD . дает в результате 4. Другими словами, слово MOD выдает остаток от деления двух чисел, или модуль. Есть еще два других слова, которые дают в результате остаток. Одно из них, /MOD, дает остаток от деления двух чисел и частное, которое помещается поверх остатка. Таким образом, 5 3 /MOD . . дает в результате пару чисел 1 и 2, в то время как 25 7 /MOD . . выдает в результате 3 4. Слово */MOD связано с обеими операциями */ и MOD. 332 */MOD . .
приводит к результату 4 1. Второе и третье число в стеке (в данном случае 3 и 3) перемножаются, образуя 32- разрядный результат, чтобы избежать переполнения, затем делится на число, находящееся на вершине стека, остаток от деления остается в стеке, поверх него на вершине стека находится частное. Назначение слова */MOD такое же, как и */, т.е. избежать переполнения во время операции умножения.
Деление с округлением, деление с отрицательными числами
В школе вас учили, что, если при делении двух чисел возникает остаток, результат нужно округлить до ближайшего целого в сторону уменьшения (вниз), т.е. 3/2 дает в частном 1 и в остатке 1. Ну а что делать, если либо делимое, либо делитель отрицательные? По интуиции вы, возможно, считаете, что, например, при делении -6/4 частное будет -1, а какой будет остаток? Вы, если хотите, можете умножить частное на делитель и сложить с остатком, чтобы получить делимое. Таким образом, в данном случае остаток будет равен -2. Потому что -2 + -1 х 4 равно -6. Именно так устроено деление в Форт-79. Для подтверждения проверим на Форт-79: -6 4 / . при этом получается -1, в результате операций -6 4 MOD . получается -2 и после -6 4 /MOD . . получается -1 -2. Для стандарта Форт-79 можно сформулировать правила "интуитивного" деления: 1) если либо делитель, либо делимое, но не одновременно отрицательные, то частное также отрицательное, 2) частное независимо от знака округляется в сторону, ближайшую к 0; 3) остаток принимает знак делимого, или второго числа в стеке.
Пользуясь этими правилами, мы всегда получаем результаты, приведенные в последних примерах. Эти правила справедливы для большинства версий Форта, за исключением тех, которые основаны на стандарте Форт-83. Точное значение результата операции -6/4 равно -1.5. Согласно школьным правилам надо округлить это число в сторону уменьшения, и мы получили бы -2 (-2 меньше, чем -1). Это так называемое деление с округлением по нижней границе (по "полу"). Его суть в том, что за частное от деления принимается ближайшее меньшее значение.
Частное всегда округляется до нижней границы. А как определить остаток? Вы снова можете умножить частное на делитель и добавить остаток, чтобы получить исходное число. Частное равно -2, делитель 4, их произведение равно -8. Чтобы получить -6 после прибавления к этому произведению остатка, он должен быть равен 2. Сформулируем правила для деления с округлением по нижней границе. 1) если либо делитель, либо делимое, но не оба сразу отрицательные, то частное также отрицательное (точно так же, как для "интуитивного" деления); 2) частное независимо от его знака округляется в сторону ближайшего меньшего числа; 3) остаток от деления принимает знак делителя (в противоположность "интуитивному" делению).
В Форт-83 применяется деление с округлением по нижней границе, что является полной противоположностью Форт-79, в связи с чем нужно быть внимательным при переносе программ из одного стандарта в другой. Хотя большинство машинных языков программирования работают как Форт-79 (за исключением АПЛ), с математической точки зрения более корректно округление по нижней границе, т.е. независимо от знака в сторону нижней границы. Тем не менее многие считают, что это противоречит здравому смыслу. Если вас это смущает, может быть, вам помогут несколько примеров, приведенных ниже. Обычное деление на 0 приводит к бесконечному результату, т.е. к разрыву в нуле. Это может привести к сложностям в графике и других применениях, например для робототехники, где необходимо обеспечить плавный переход от положительных к отрицательным числам. Деление с округлением по нижней границе в связи с этим предпочтительнее, но нужно иметь в виду, что оно занимает времени немного больше.
Обычное деление Деление с округлением по нижней ("интуитивное") границе 4 2 / 2 ok 4 2 / . 2 Ok 3 2 / 1 ok 3 2 / . 1 ok 2 2 / 1 ok 2 2 / . 1 ok 1 2 / 0 ok 1 2 / . 0 ok 0 2 / 0 ok 0 2 / . 0 ok -1 2 / 0 ok -1 2 / .-1 ok -2 2 / -1 ok -2 2 / .-1 ok -3 2 / -1 ok -3 2 / .-2 ok -4 2 / -2 ok -4 2 / ,-2 ok
В связи с необходимостью преобразования программ, написанных на Форт-79, в стандарт Форт-83 нужно иметь правила или алгоритм преобразования результата обычного деления в результат деления с округлением по нижней границе. Проще всего представить этот алгоритм, определив два слова на языке Форт. На Форт-79 слово : FL/ /MOD SWAP IF DUP 0 < IF 1- THEN THEN ; будет выдавать частное с округлением по нижней границе при делении чисел в соответствии со стандартом Форт-79- Вам еще не приходилось обращаться с некоторыми словами, использованными в данном определении, но их нетрудно понять. Выражение /MOD SWAP выдает частное и остаток и переставляет остаток на вершину. Если слово IF обнаруживает положительное число, т.е. остаток не равен 0, исполняются слова, которые следуют за IF до второго оператора THEN. В этом случае частное дублируется на вершине стека и сравнивается с нулем с помощью оператора
Освоив стековые манипуляции в гл. 2, вы можете заметить, что частное и остаток деления с округлением по нижней границе можно найти по аналогии со словом /MOD с помощью : FL/MOD 2DUP FLMOD ROT ROT FL/ ;
Следует подчеркнуть, что данные определения работают очень медленно по сравнению с теми определениями на языке ассемблера, которые введены в Форт-83. И ими нужно пользоваться только в случае крайней необходимости.
Упражнения
1. Проделайте в уме следующие примеры, используя компьютер только для проверки ответов. Найдите остаток и частное при "обычном" делении и делении с округлением по нижней границе: (а) 10/2 (б) 0/2 (в) 11/3 (г) -11/3 (д) 11/-3 (е) -11/-3 (ж) -10/2 (з) 10/-2 (и) -10/-2 (к) -10/0 2. Имеется текст из нескольких сотен литер, расположенный в строчках по 50 символов в строке. Напишите слово для вычисления номера строки, в которой находится 112-я литера. (Не забудьте, что вы работаете в целочисленной арифметике.) 3. Используя слово MOD, определите слово для вычисления количества символов, предшествующих 112-му символу в той строке, где он находится. 4.
Используйте идеи из упражнений 2 и 3 и напишите слово для определения номера строки и номера в строке любого символа в тексте из строчек фиксированной длины. При вызове слова в стеке должны находиться номер символа и длина строки, Приемы, которые вы применили в упражнениях 2-4, пригодятся впоследствии, когда вам потребуется определить местонахождение данных, хранящихся в блоках форта, а именно эти приемы помогут вам найти номер блока, в котором находится конкретный фрагмент данных и число байтов смещения внутри этого блока. 5. Напишите определение слова MOD (под именем NEWMOD) для Форт79, используя стандарт Форт-83- В этом процессе вам нужно будет дать определение деления / в Форт-79. 6. Найдите значение выражения 2000*100/30. Проделайте это двумя способами, избегая в обоих случаях переполнения. В первом используйте */, в другом - /MOD- Теперь проделайте то же, чтобы в результате выдавалось значение частного и остатка. Можете ли вы сделать это также двумя способами? Дальше мы узнаем, как использовать остаток, который получается со словом MOD для выполнения арифметических операций с имитацией плавающей запятой.
Операции с величинами и знаками чисел
Каждое из слов МАХ и MIN сравнивают два числа и оставляют в стеке соответственно большее. или меньшее из них. Так, например, 7 3 МАХ оставляет в стеке 7, в то время как 7 3 MIN возвращает в стек 3. Имеется много применений этих слов, некоторые из них мы увидим в упражнениях. Одно из наиболее частых применений - для ограничения диапазона чисел, поступающих входе. Предположим, что вы хотите ограничить диапазон чисел значениями от 5 до 25. Это может быть сделано следующим словом: : LIMIT-RANGE 5 МАХ 25 MIN :
Если встречается число больше, 25, то в стек помещается число 25, в то же время, когда число меньше 5, в стек помещается 5. При определении слов FL/ и FLMOD вы уже видели, как можно сравнить числа, и использовали для этого конструкцию IF...THEN, и вам, вероятно, нетрудно представить, что МАХ и MIN можно определить таким же способом.
Однако такое определение будет очень медленным. Многие программисты стремятся чаще использовать конструкцию IF...THEN, избегая применения слов МАХ и MIN. Но это их ошибка, поскольку МАХ и MIN позволяют значительно ускорить исполнение программы.
Слова ABS и NEGATE воздействуют на знак чисел. (Со словом NEGATE вы встречались в гл. 3.) Слово ABS возвращает в стек абсолютную величину числа. Так, например, -5 ABS возвращает в стек 5, а 5 ABS выдает в стек то же самое число 5.
В свою очередь, слово NEGATE всегда изменяет знак числа. Т.е. -5 NEGATE положит в стек 5, в то время как 5 NEGATE вернет в стек -5. Более медленная версия слова ABS, основанная на применении NEGATE и конструкции IF...THEN, приводится ниже: : ABS DUP 0 < IF NEGATE THEN ; Покажем применение этих операторов на нескольких упражнениях.
Упражнения
1. Определите слово, которое будет печатать наибольшее из трех верхних чисел в стеке. 2. Определите слово, которое будет печатать наименьшее из трех верхних чисел в стеке, оставляя в стеке исходные числа. 3. Определите слово, которое будет выдавать в стек 1, если любое из трех верхних чисел больше 5 (используйте оператор >). 4. Определите слово, которое будет возвращать 1. если все три верхних числа в стеке больше 5 (также используйте один из операторов сравнения), 5. Опишите слово, которое будет возвращать 1, если число на вершине стека больше, чем два следующих, находящихся ниже. 6. Определите слово, которое будет выражать разность температур, два значения которых находятся в стеке, как положительное число, независимо от знака температуры. 7. Определите слово, которое будет находить наибольшую абсолютную величину двух чисел, независимо от того, положительные они или отрицательные. 8. Определите слово, которое будет выдавать абсолютное значение того из двух чисел, которое ближе к нулю, независимо от знака чисел. 9. Используйте слово NEGATE для определения слова, действующего противоположно слову ABS, т.е. возвращающего число, равное по абсолютному значению исходному числу, но всегда отрицательное или нулевое. 10.
Определите слово OTHER-QUAD, которое переводит координаты точки (х.у) в прямоугольной системе координат п противоположный квадрант, сохраняя абсолютное значение х и у. Это значит, что нужно перевести точку из верхнего левого угла в правый нижний, из нижнего левого - в верхний правый и т.д. Например, -23 5 OTHER-QUAD должно в результате выдать 23 -5. 11. Определите слово NEWNEGATE с функцией NEGATE, используя только число и оператор *.
Определение математических функций
Одной из приятных возможностей языка Форт является то, что он позволяет определять математические функции, расширяя язык для математических приложений. В качестве очень простого примера вы уже встречались с определением слова для вычисления квадрата числа: : SQUARE DUP * ; Для-обозначения названий функций удобно, хотя это не общепринято, пользоваться алгебраической записью, заменяя пробелы точками. Например, определение слова для вычисления выражение а^2+b^2 может быть : А2.+.В2 DUP * SWAP DUP * + ; или для выражения а (а + b) : А(А.+.В) (b a - aa+ab) DUP ROT + * ; Для расчета значения сложных выражений лучше всего разбить их на максимально возможное число простых выражений. При этом будет проще следить за состоянием стека, давая определения отдельным словам, и вообще, определения слов должны быть как можно короче и быстрее. Предположим, например, что b лежит в стеке вторым, число а находится на вершине стека и вы хотите найти значение выражения a^2+ab. Можно определить слово: : А2.+.АВ DUP DUP * ROT * + ; которое сначала вычисляет значение а^2, потом ab и затем складывает оба произведения. С другой стороны, выражение может быть разложено на множители а(а + Ь), а это выражение, как мы только что видели, может быть вычислено с помощью : А(А.+.B) DUP ROT + * ; - Очевидно, что второй вариант проще, короче и более быстродействующий.
Упражнения
1. Объем пирамиды вычисляется по формуле Ah/2, где А - площадь основания пирамиды, h -высота пирамиды. Определите слово PYRVOL (объем пирамиды) для вычисления значения функции с округлением результата до ближайшего целого числа. 2.
Определите слово F-> С для пересчета градусов Фаренгейта в градусы Цельсия по формуле С = 5(F - 32)/9. Значения входных и выходных величин должны быть округлены до целых значений. Можете ли вы предложить способ получения значений с точностью до десятых долей градуса? Для этого применяется так называемое масштабирование, о котором мы вскоре расскажем- 3. Напишите слова для вычисления следующих функций, применяя, где возможно, разложение на множители. Дайте вашим словам подходящие названия. (а) а/с + Ь/с (б) а/с + b/с2 (в)a^2+2ab+b^2 (г) За^2+6аb+Зb^2 (д) a^4+4a^3b+6a^2b^2+4ab^3+b^4
Почему используются целые числа?
Из того, что вы уже знаете, ясно, что Форт сталкивается с определенными проблемами при работе с очень большими числами и числами с плавающей запятой (такими, например, как 23.497 или -0.96). Некоторые считают, что использования 16-разрядных и 32-разрядных целых чисел достаточно практически для всех применений микро- и миниЭВМ и работа с числами с плавающей запятой приводит к расточению времени и памяти ЭВМ. Другие, включая авторов, кто использует ЭВМ для научных и технических задач, считают, что, хотя в большинстве применений можно обойтись целыми числами,- числа с плавающей запятой очень нужны для практического применения языка Форт в некоторых классах задач. Несмотря на отсутствие требований в стандартах Форт-79 и Форт-83, в некоторых версиях языка поддерживается в той или иной мере арифметика с числами с плавающей запятой. Мы рассмотрим более детально математические основы применения чисел с плавающей запятой в конце этой главы. Но для этого, а также для того, чтобы понять,как извлечь максимум возможностей из использования целых чисел, мы должны сначала рассмотреть, что представляют собой числа с плавающей запятой.
Замечания о числах с плавающей запятой
Рассмотрим число 1298. Оно может быть представлено следующим образом: 1000 + 200 +90+8.
Число 1298,325 можно представить так : 1000 + 200 + 90 + 8 + 0.3 + 0.02 + 0.005.
Если в числе имеется десятичная запятая, это означает, что часть его, которая стоит после десятичной запятой, представляет сумму дробей, каждая из которых может быть выражена в виде отношения, которое в десятичной системе имеет в знаменателе 10, 100 и т.д.
Таким образом можно представить 0.325 как 3/10 +2/100+5/1000 или, приведя к общему знаменателю, как 300/1000 + 20/1000 + 5/1000 = 325/1000. Таким образом, любое число с плавающей запятой может быть представлено как целое число плюс отношение двух других целых чисел. Так как числа двойной длины используют 32 разряда, то наибольшее представимое целое число равно 4271406735, а с помощью целых чисел могут быть представлены числа с плавающей запятой не меньше этого числа с погрешностью, не превышающей 1/1000000000. Например, число 4271406735.123456789 можно представить как 4271406735 +123456789/1000000000. Это совсем немалый диапазон и малая погрешность, если бы только имелась возможность следить за значением целой и дробной части. Форт оставляет решать эту задачу программисту. С другой стороны, числа с плавающей запятой называются так потому, что правила арифметики и программы для калькуляторов или компьютеров отслеживают положение десятичной запятой, позволяя ей "плавать", где нужно, т.е., положение плавающей запятой в числе автоматически отслеживается машиной.
Число 1 1/2 может быть представлено точно как 1.5. С другой стороны, рассмотрим число 1 1/3, оно не может быть представлено как 1.3, 1.333 и т.д. в десятичной системе, сколько бы ни было разрядов (хотя, если работать в троичной системе счисления, то 1 1/3 можно точно представить числом 1.1). Другими словами, 1 1/3 - это точное'число, в то время как представление его десятичной дробью является приближенным. Приближение может быть достаточно хорошим, но не абсолютно точным. Если числа известны точно и если они могут быть представлены как целое число плюс отношение -двух целых чисел, то лучше всего было бы все числа представлять через целые. Но рассмотрим реальный мир. Точным микрометром можно измерить толщину с погрешностью в лучшем случае 1/1000 см. Для инженера почти все измерения дают. приближенный результат. Большинство констант, которые применяются в науке и технике, нельзя выразить через целые числа или отношением целых чисел.
Например, нельзя точно представить число Пи. Таким образом, во многих областях точное представление целых чисел и отношений не имеет практического смысла. То же справедливо для представления чисел с плавающей запятой. Имеется и еще одна практическая проблема. В технике часто необходимо использовать числа, которые больше или меньше, чем представимые 16- или 32-разрядными числами. Например, в больших ЭВМ применяются 64-разрядные числа. Очень большие и очень малые числа можно представить числами с плавающей запятой с указанием порядка (иногда их называют действительными числами с показательной или "научной" формой записи), используя не больше 32 разрядов. Они выражаются как действительное число с плавающей запятой плюс число 10, возведенное в какую-либо степень. Рассмотрим табл. 4.1.
Степень Число Степень Число 1 (т.е. 10^1) 10 -1 (т.е. 1/10 2 100 -2 1/100 3 1000 -3 1/1000 4 1000 -4 1/10000 Таблица 4.1. Степени числа 10
Мы видели, что 0.395 может быть выражено как 395/1000, поэтому 0.395 = 395/1000 = 395 х 10^ 3. Аналогично мы можем выразить 3950 как 3950 = 0.395 х 104.
Так как многие принтеры не могут напечатать 10^-3 или 10^4, как показано в тексте, в компьютерной нотации опускается число 10 и указывается только показатель степени, перед которым стоит буква Е (от exponent - показатель степени), т.е. 0.395 будет представлено как 3.95Е-3, а 3950 - как 0.395Е4.
Чтобы преобразовать число из показательной формы представления в обычную форму с плавающей запятой, нужно перенести десятичную запятую влево на значение показателя степени, если он положительный (добавляя, если нужно, нули), или вправо, если показатель степени отрицательный. Теперь мы можем выразить очень большие и очень малые числа. Имея только 32 разряда, можно отображать числа больше 1Е-38, т.е. 100000000000000000000000000000000000000, и меньше 1Е-37, или 0.00000000000000000000000000000000000001.
Число, которое предшествует числу 10 в степени, может быть 6-ти разрядным (десятичным). Таким образом, диапазон представления чисел с плавающей запятой 1Е-38 999999Е38.
Необходимо ввести здесь некоторую терминологию. Целое число, предшествующее числу 10 в степени, называется мантиссой (если вы знакомы с логарифмами, то, наверное, знаете почему; если же не знаете, то это не так уж важно), а показатель степени называется порядком.
Арифметические операции с числами с плавающей запятой в показательной форме
Арифметические действия с числами, представленными в показательной форме, выполняются по простым правилам. Вам совершенно не нужно их понимать, для того чтобы понять все за и против математики целых чисел и чисел с плавающей запятой, но правила простые, и сейчас,самое время , их изучить.
Чтобы сложить два числа или вычесть одно число из другого, нужно привести их к одному порядку, а затем сложить или вычесть их мантиссы. Например, 5Е-3 - 2E-2 = 5E-3 - 20Е-3 = -15Е-3= -1.5Е-2 или 12Е10 + 21Е11= 12Е10 +210Е10 = 222Е10 =22,2Е11 Чтобы перемножить два числа, нужно перемножить их мантиссы и сложить порядки. Например, 5Е-3 * 2Е-2 = 10Е-5 = 1Е-4 или 12Е-10 * 21Е11 = 252Е21 = 2.52Е23.
Чтобы разделить два числа, нужно поделить мантиссу делимого на мантиссу делителя и вычесть порядок делителя из порядка делимого. Например, 5Е-3/2Е-2 = 2.5Е-1 или 12Е10/25Е11 = 0.48Е-1 = 0.048.
Представление чисел в форме с плавающей запятой и порядком упрощает арифметические операции с большими и малыми числами.
Точность и погрешность
Нам осталось рассмотреть еще несколько вопросов, прежде чем вернуться к аргументам за и против использования чисел с плавающей запятой или целых чисел. Точность числа с плавающей за пятой выражается числом знаков в записи мантиссы, или, говоря проще, числом десятичных разрядов числа. Так, число 1.23Е5 имеет точность 3 знака, 112.35Е23 - 5 знаков, а 1230 - 4. Но точность и погрешность - это не одно и то же. Если вы пользуетесь измерителем, снабженным нониусом, то вы сможете считать размер с точностью до 1/10 миллиметра. С другой стороны, если вы измеряете размер горошины, то оценить его с точностью до 0.1 миллиметра вы не сможете, так как горошина не идеально круглая.
Вы можете измерить диаметр 4. 5 мм, выражая такой записью, что точность его 2 знака. Вы можете поддаться искушению и добавить еще два нуля, написав 4.500, т.е. с точностью 4 знака. Но погрешность измерения, вероятно, будет ближе к миллиметру или 1 знаку.
Если вы записываете число как 4.5 или 4.500, вы в лучшем случае обманываете себя или того, кто просматривает ваши данные, в худшем случае подтасовываете погрешность измерения. Самый простой способ подорвать доверие к лабораторному отчету о физических или химических измерениях - это указать большое число знаков погрешности, путая ее с точностью. В чем здесь суть? В том, что в реальном мире точность числа лучше 4-6 знаков требуется крайне редко. Одна из причин, почему числа с плавающей запятой так нравятся ученым и инженерам, состоит в том, что из них ясно видна точность числа по количеству приведенных в записи знаков. Запись 1.23Е5 говорит о том, что, хотя измеренное значение приблизительно равно 123000, его точность +-1000. С другой стороны, если число представляется как целое 123000, то большинство представителей технических наук будут считать, что оно" известно с точностью б знаков. Числа с плавающей запятой и указанием порядка лучше всего удовлетворяют потребности науки и техники.
Существует одна важная сфера, где требуется гораздо большая точность. Это денежные расчеты. Бухгалтер никогда не округлит миллион долларов до трех или четырех знаков. В расчетах должны указываться все доллары и центы. И редко кому потребуется величина порядка числа 10 или 11 даже для выражения бюджета крупных государств (хотя иногда крупные компании испытывают трудности в программировании расчетов, так как им приходится оперировать значениями больше триллиона долларов'). Точно так же редко потребуется и порядок меньше -3 (это соответствовало бы десятым долям цента или пенса или одной тысячной иены). Поэтому для бухгалтерских расчетов 32- разрядные числа с плавающей запятой обычно не.подходят. Они не обеспечивают требуемой точности и в то же время имеют избыточный диапазон представляемых чисел.
Программы бухгалтерских расчетов лучше всего составлять с применением целых чисел, но предусматривая две позиции для отображения сотых долей.
Числа с плавающей запятой: за и против
Многие преимущества чисел с плавающей запятой вам уже должны быть понятны. Основные из них: 1) при вычислениях с числами с плавающей запятой обеспечивается отслеживание положения десятичной запятой и величины числа практически без участия программиста; 2) представление чисел с плавающей запятой с указанием порядка позволяет работать как с очень большими, так и очень малыми числами; 3) арифметика с плавающей запятой позволяет задавать точность или погрешность с достаточным количеством знаков; 4) представление чисел с плавающей запятой распространено в мире инженеров и ученых, не склонных доверять представлению результатов измерений в реальном мире с помощью целых чисел. Хотя последняя причина в первую очередь психологическая, она имеет значение для принятия языка большинством практиков.
Многие (но не все) преимущества целочисленной арифметики вам также должны быть понятны: 1) целые числа обеспечивают большую точность при заданном объеме памяти, для 16-разрядных чисел диапазон -32768 - 32767 или - 2147483648 до 2147483647 для 32-разрядных; 2) с помощью целых чисел можно представить как собственно целые, так и действительные числа, имеющие целую и дробную части, например 12.55 = 12 + 55/100; 3) с помощью целых 32-разрядных чисел представляются целые числа и дроби в диапазоне от 1Е-10 и более чем 1Е+10, при этом с точностью на 4 знака больше, чем с помощью 32-разрядных чисел с плавающей запятой.
Но наиболее часто выдвигаемый аргумент в пользу целых чисел - это более высокая скорости работы. Арифметические действия с плавающей запятой производятся компьютером по приведенным здесь правилам. Из них вытекает, что при арифметических действиях с плавающей запятой компьютер должен выполнить большее количество операций сложения, вычитания, умножения или деления, следовательно, для этого требуется в несколько раз больше времени.
Поскольку арифметические действия часто выполняются в циклах много раз, скорость исполнения выдвигается на первое место. Самый существенный аргумент против включения чисел с плавающей запятой в стандарт состоит в том, что в таком случае приносится в жертву скорость работы. Но насколько серьезен этот аргумент? Посмотрим, как работает Фортран или Бейсик (и многие другие языки). Именно с учетом всех за и против, которые мы обсуждаем, другие языки дают программисту свободу принимать решение, обращаться ли с числами как с целыми или как с числами с плавающей запятой. В Форте это также возможно, правда, не во всех версиях.
Возможность пользоваться числами с плавающей запятой должна быть решающим критерием при выборе подходящей версии Форта. И решение этого вопроса возлагается на программиста.
Для некоторых микрокомпьютеров вопрос об использовании чисел с плавающей запятой становится еще более критичным. Как мы увидим дальше, в некоторых компьютерах доступно так называемое сопроцессорное арифметическое устройство, которое практически представляет собой еще один компьютер, предназначенный для выполнения операций над числами с плавающей запятой, причем с очень высоким быстродействием, повышенной точностью и в расширенном диапазоне чисел. Наиболее известным примером может служить сопроцессор фирмы Intel типа 8087, который может быть использован в IBM PC и совместимых с ней ЭВМ. Микросхема 8087 представляет числа 80-разрядами, при этом мантисса числа может иметь до 18 знаков, а порядок от -4600 до 4600 (в буквальном смысле можно представлять числа больше, чем оценка числа электронов во вселенной). По причине того, что 8087 использует стек так же, как Форт, ее стек можно частично использовать для операций с плавающей запятой. Поэтому Форт с сопроцессором 8087 может выполнять некоторые операции с плавающей запятой с потрясающими точностью и диапазоном и значительно быстрее, чем ЦПУ с 16-разрядными числами. Учитывая, что ЭВМ с сопроцессором 8087 могут также работать с памятью объемом до 1 Мбайта, аргументы в пользу применения целых чисел для экономии времени и памяти, а также получения большей точности становятся слишком слабыми.
Имеется надежда, что в будущем появятся версии языка Форт, в стандарт которых будут включены операторы для работы с числами с плавающей запятой. Ну а как быть, если ваша версия Форта не может работать в арифметике с плавающей запятой или вы не можете пожертвовать скоростью работы? Как можно обращаться с числами, которые большинство из нас записывают как действительные числа (с десятичной запятой)? Как вы будете обращаться с дробями? Решать эти вопросы вам поможет масштабирование.
Масштабирование чисел
Слово "масштабирование" применяется в том же смысле, что и в технике, когда делается чертеж, который отображает объект в увеличенном виде. Предположим, что вам нужно рассмотреть синьку часового винта. Если винт имеет длину всего 1 мм, его нужно изобразить в каком-то масштабе, например 100:1. Это значит, что чертеж должен быть выполнен в масштабе, в 100 раз превышающем истинные размеры. Подобным образом карта может быть масштабирована в обратную сторону, скажем, 1 см вместо 1 км, т.е. 1:100000.
Проще всего показать масштабирование на примере. Предположим, вы хотите сложить в столбик доходы, выраженные в долларах, хотя они сейчас выражены в долларах и центах. В целочисленной арифметике на Форте вы, к примеру, не можете сложить 22.98 и 35.53. Но вы можете произвести сложение, если выразите доходы в центах, а не в долларах. Другими словами, вы можете изменить масштаб входных данных и внутреннего представления операций с денежными единицами в 100 раз. В таком случае вы введете 2298 3533 + . и увидите в результате 5831, что, как вы помните, представляет собой 58 долларов 31 цент. Однако в таком виде результат выглядит не очень красиво. Вы можете выразить результат в долларах и центах с помощью следующего слова: $CENTS 100 /MOD U. " Долларов и" . ." центов" ;
Слово $CENTS производит деление числа на 100, округляет результат в сторону нижней границы и печатает его как число долларов. Остаток от деления выражает центы. (В гл. 5 мы узнаем метод, который называется выводом по шаблону, с помощью которого десятичная запятая может быть помещена в числе в любом месте.) Очень удобно для использования при масштабировании чисел слово /МОD) (его иногда называют масштабным оператором), фактически оно позволяет вам "пересчитать" число.
Число на входе выглядит не очень изящно, поскольку в него нельзя еще ввести десятичную запятую. Как мы вскоре увидим, при вводе числа двойной длины необходимо указывать положение десятичной точки, на основании этого Форт распознает числа двойной длины. Но при расчетах Форт игнорирует положение десятичной точки. Как вы видели на примере $CENTS, программист не всегда должен заботиться о масштабировании, эта работа может быть оставлена компьютеру. И не обязательно, чтобы масштабный коэффициент был кратен 10. Вот, например, слово, которое берет из стека число дюймов и переводит их в футы и дюймы: : .FTTN (дюймы -->) 12 /MOD . ." футов и" . ." дюймов" ; Очевидно, можно сделать преобразование в обратную сторону. Определим слово : TOINCHES (футы дюймы - дюймы) SWAP 12 * + ;
Если ввести 10 6 TOINCHES, то мы увидим в стеке длину в дюймах 126.
Умножение на дроби
Предположим, что вы хотите умножить число на 3/4 с помощью калькулятора. Вы можете в действительности умножить на 0.75, зная, что 0.75 равно 3/4. Так как вы не сможете умножить на 0.75 с помощью целых чисел, вы можете на Форте вычислить дробное выражение, умножая на 3/4. Однако позвольте, просто так нельзя разделить 3 на 4, а потом умножить на результат, так как 3 4 / дает 0. Поэтому нужно сначала умножить число на 3, а затем произвести деление, т.е. вам нужно применить оператор */ (он, как и /MOD, называется масштабным оператором). Таким образом, выражение 3 4 / 100 * дает неверный результат 0, в то время как 100 34*/ дает правильный результат 75. Напомним, что во избежание переполнения */ сохраняет промежуточный результат умножения в виде 32-разрядного числа.
Отсюда вытекает общее правило: чтобы обеспечить надлежащую точность смешанной операции, всегда выполняйте операции, которые дают в результате большое число, прежде чем делать деление, поскольку промежуточный результат получается без переполнения. Это еще одна причина необходимости разложения выражения на множители перед его вычислением.
Например, выражение а/х + b/x + с/ х может дать менее точный результат, чем выражение (а+b+с)/х. Чтобы убедиться в справедливости этого утверждения, проверьте это с какими-либо числами.
Оператор */ очень удобен для умножения на дробь с постоянным знаменателем. Например, вы можете определить слово PERCENT (процент) : : PERCENT 100 */ ; так, что выражение 130 50 PERCENT положит в стек число 65 (50% от 130). В некоторых случаях вы хотели бы иметь результат смешанной операции */ с большей точностью, чем получается с по мощью обычного умножения с последующим делением.Наилучший метод избежать потери точности - это убедиться, что перед операцией */ входные операнды уже увеличены с помощью масштабирования для получения желаемой точности. Например, если вы вычисляете 3/4 от 123, то в плавающей арифметике вы получите 92.25, а в целочисленной - 92. Следует изменить масштаб чисел, чтобы операция и */ производилась не с числом 123, а с числом 12300. Это еще один пример того, что, применяя целочисленную арифметику, программист должен заранее все продумать. Но есть еще один способ не потерять точность, если вы по какой-либо причине не хотите предварительным масштабированием увеличить числа: применить оператор */MOD (подобно */, он называется масштабным оператором). Например, выражение 123 3 4 /MOD выдает на вершину стека частное 92 и вторым сверху - остаток 1. Вы можете использовать остаток, если не хотите потерять точность, применяя выражение 123 3 4 */MOD SWAP 100 4 */ которое положит в стек 92 25. Это значит, что на вершине стека находится число, которое представляет собой два разряда после десятичной запятой в представлении с плавающей запятой. Если вы продумаете тщательно программу, то сможете с помощью целых чисел выразить эквивалент числа с плавающей запятой, а, пользуясь форматным выводом, как описано в гл.5, вы сможете даже напечатать результат с десятичной запятой (например, 92.25).
Приближенное представление чисел с помощью рациональных дробей
Предположим, что вы хотите найти длину периметра круга, которая равна диаметру, умноженному на число Пи.
Как можно выразить иррациональное число Пи, т.е. число, которое не может быть выражено в виде отношения двух целых чисел? Для приближенного вычисления можно использовать : PI* 31416 10000 "/ , Если диаметр равен 10 см, то выражение 10 PI* дает 31 - величину периметра с погрешностью 1 см. Очевидно, вам может потребоваться большая точность, для чего вы можете изменить масштаб диаметра на входе, например задать его в миллиметрах. Выражение 100 PI* даст величину 314,т.е. длину периметра 314 мм. С помощью следующего слова вы можете найти площадь круга (которая равна квадрату радиуса, помноженному на Пи или четвертой части квадрата диаметра, умноженной на Пи) : : AREA DUP 4 */ PI* ; Что делает PI* ? Оно просто умножает число на отношение 31416/10000, которое равно 3.1415. 31416/10000 представляет собой апроксимацию иррационального числа Пи рациональной дробью, правда, не очень хорошей апроксимацией, потому что для невысокой точности всего нескольких знаков используются большие числа. Дробно-рациональная аппроксимация применяется для большого разнообразия иррациональных чисел и физических констант, включая гораздо лучшую апроксимацию числа Пи. В табл. 4.2 приведено несколько примеров. Заметим, что все эти апроксимации дают большую точность, чем можно получить, применяя числа одинарной длины.
Константа Отношение Ошибка Пи 355/113 8.5Е-8 2 19601/13860 1.5Е-9 3 18817/10864 1.1Е-9 е 28667/10564 5.5Е-9 с(скорость света) 24559/8192 1.6Е-9
Таблица 4.2. Дробно-рациональная аппроксимация некоторых общеупотребительных констант.
Округление
Если вы делите 26 на 5, то, применяя числа с плавающей запятой, вы получите 5.2, а при делении 29 на 5 - 5.8. Однако при целочисленном делении в обоих случаях мы получим 5. Другими словами, целочисленное деление производит округление с уменьшением (усечение). Если вы будете складывать результаты деления нескольких чисел, то ошибка округления будет накапливаться и результат будет занижен. Можно при округлении следовать школьному правилу: если на первом месте после запятой находится цифра 5 или больше, то округление производится в большую сторону, если меньше 5- то в меньшую.
Программа для этого выглядит не очень сложно. Мы приводим программу деления с округлением, которая делает все, что нужно: : R/ SWAP OVER /MOD SWAP 2 * ROT / + ; Испытайте ее: 26 5 R/ даст в результате 5, а 29 5 R/ даст в результате 6. Вот что здесь происходит. Делитель был помещен на дно стека, затем скопирован на вершину, чтобы осуществить деление с остатком. Остаток был скопирован, а затем поделен на исходный делитель. Это второе частное должно быть равно 1, если первое частное должно быть округлено, причем в этом случае оно должно быть сложено с первым частным. Иначе второе частное должно быть равно 0 и первое частное должно быть оставлено без изменения. Почему эта программа работает? В терминологии деления с плавающей запятой число следует округлить, если дробная часть частного больше или равна 0.5, т.е. дробная часть должна быть умножена на 2, если она больше или равна 1. В целой форме дробная часть удваивается, конечно, удвоенный остаток помещается поверх делителя. Но бывают случаи, когда вы должны сделать округление после деления; это случается, когда вы хотите, чтобы сумма последовательности частных имела ошибку в сторону увеличения или когда вы скорее склонны переоценить результат, чем недооценить его. Чтобы произвести округление, вы просто добавляете 1 к нормальному частному от деления двух целых чисел, если остаток не равен 0. Вот слово.которое это делает на Форт-83: : RUP/ /MOD SWAP 0= 0= + : Слово 0= будет более подробно рассмотрено чуть позже, вкратце оно возвращает 1, если на вершине стека 0, или 0 - в противном случае. Зная это, вы должны понимать, как работает RUP/.
Упражнения
1. Сделайте следующие упражнения в уме: (а) 5Е5х5Е10 (б) 5Е5х5Е-10 (в) 5Е5х5Е0 (г) 5Е-1х5Е-5 (д) 5Е5/5Е10 (е) 5Е5/5Е-5 (ж) 5Е2 + 2Е3 (э) 5Е2 + 5Е-3 (и) 5Е2 - 2Е-3 (к) 5Е2 - 5Е-3 2. Показательная форма записи может применяться и для целых чисел, но не часто. Мантисса должна быть сохранена второй в стеке, а показатель степени - на вершине стека. Таким образом, для 5Е10 в стеке находятся числа 5 10.
Напишите слово ЕХР*, которое должно перемножать два числа, представленных в стеке описанным способом. Например, если в стеке находятся числа 5 10 6 15, то после умножения в стеке будут находиться числа 30 25. Попробуйте это слово на примерах упражнения 1(а-г). 3. Используя идеи упражнения 2, определите слово ЕХР/для деления чисел, представленных в показательной форме записи. Это упражнение не такое простое, как вам кажется, поэтому загляните в ответ в приложении Д, даже если вы чувствуете, что сделали правильно. 4. Определите слово ТОМ (в_метры), которое складывает длину, выраженную в километрах, с длиной, выраженной в метрах. Слово предполагает, что в стеке содержится величина в километрах и величина в метрах в указанном порядке. Результат, выраженный в миллиметрах, должен оставаться в стеке. 5. Определите слово ТОСМММ (в_см_и_мм), которое берет с вершины стека число, представляющее миллиметры, и преобразует его в сантиметры, располагая результат вторым сверху, поверх которого должно располагаться число миллиметров, т.е. число 12.345 должно быть преобразовано в 1234 и 5. 6. Определите слово ТОКМ (в_км), которое выполняет преобразование обратное тому, что делает слово ТОМ из упражнения 4, т.е. берет значение в миллиметрах, а возвращает в стек значение в километрах и метрах. 7. Определите слово ТОРТ(в_футы) по аналогии с упражнением 4, но для преобразования числа миль и футов в футы. В одной законодательной миле содержится 5280 футов. 8.Определите слово ТОМILS(в_мили) для преобразования футов в мили и футы. метры, принимая коэффициент преобразования 1 фут - 0,305 м. 10. Пользуясь словами и идеями упражнений 6-9, напишите программу TOMETRIC (неметрические) для преобразования миль и футов в километры и сантиметры, используя расположение данных такое же, как в предыдущих упражнениях. 11. Пересчет градусов Цельсия в градусы Фаренгейта производится по формуле F = 32 + 9С/5, Определите слово С->Р для преобразования градусов Цельсия в градусы Фаренгейта, при этом температура по Фаренгейту должна выражаться с погрешностью 0.1 градуса.
Позаботьтесь о сохранении погрешности. 12. Напишите слово для определения 1/10 периметра окружности, диаметр которой равен точно 1/2 см. Результат должен быть получен с погрешностью 1 нанометр (1 нм - 1/10000 см). Используйте дробно-рациональную апроксимацию. 13. Определите слово IN->FT для пересчета дюймов в футы с округлением в сторону ближайшего фута, т.е. 13 дюймов нужно округлить до 1 фута, 20 дюймов - до 2 футов.
Числа двойной длины
Мы уже неоднократно упоминали о числах двойной длины. К примеру, при использовании операции */ промежуточный результат запоминается как число двойной длины. Числа двойной длины записываются в два раза большим числом разрядов, чем числа одинарной длины, т.е. для хранения одного числа в стеке используется 32 бита, или 4 байта памяти. Они используются так же, как и числа одинарной длины, за исключением того, что для арифметических операций с ними применяются другие, хотя и похожие слова. Диапазон представления чисел двойной длины со знаком составляет от -2 147 483 648 до 2 147 483 647, диапазон чисел без знака от 0 до 4 294 967 295. Иногда числа двойной длины называют числами двойной точности или еще 32-разрядные числа в стандарте Форта называют двойными числами. Нам кажется, что термин "числа двойной точности" должен распространяться только на числа с плавающей запятой. 32-разрядные целые числа имеют в два раза большую длину, а величина их, конечно, больше не в два раза, что не определяет их точность. Фактически число "двойной длины" означает только то, что число в двоичной форме занимает в два раза больше разрядов, чем число одинарной длины.
Числа двойной длины представляют собой расширение стандарта языка Форт и других версий, т.е. они не присущи стандартному Форту после его загрузки в компьютер. Расширения языка должны быть загружены самостоятельно. В версии MMSFORTH представление числа двойной длины производится словом DBL-LEN# (двойная^длина). Для других версий Форта нужно обратиться к руководству, чтобы узнать, как это делается, или убедиться, что уже предусмотрено.
Если при загрузке Форта это не производится, то нужно осуществить загрузку, после этого мы попробуем сделать несколько примеров. Для того чтобы их понять, вам нужно знать, что слово D. (произносится как дэ-точка) печатает число двойной длины, оно является эквивалентом слова, (точка). Попробуем ввести 1.23 D. и мы увидим на экране число 123. А теперь введите 123. D. и вы увидите тот же самый результат: 123 Ok Теперь введите 1234567890. D. и будет выведено 1234567690 ok
Но если ввести 1234567890 (без десятичной точки в конце),то произойдет переполнение, потому что число было чересчур велико. Не слишком ли это смущает вас? Для чего в числе была нужна точка? И почему число 1234567890. проходит, а число 1234567890 - нет? Ответ простой. Десятичная точка сообщает Форту, что число нужно рассматривать как число двойной длины, т. е- оно должно быть записано в 32 разряда, или 4 байта. При этом совершенно безразлично, где находится десятичная точка, поскольку она в дальнейшем не используется (вы помните, что числа двойной длины используются в целочисленной арифметике). (Примечание: десятичная точка игнорируется не всегда. В MMSFORTH и других версиях Положение десятичной точки запоминается для того, чтобы произвести масштабирование чисел, как мы вскоре увидим.) И поэтому если десятичная точка отсутствует, то она и не обнаруживается при печати числа. Теперь вам стало понятно, что происходит в наших примерах. Очевидно, что число 1234567890 приводит к ошибке, так как вы не сообщили Форту, что это число двойной длины, а для числа одинарной длины оно слишком велико.
Попробуем еще несколько примеров. Убедитесь, что стек пуст и после этого введите 1.23 . . Вы увидите 0 123 ok В этом случае вы вывели два числа одинарной длины, на вершине стека было число 0 и следующее число-123. Попробуйте ввести 65535. U. U. тогда вы получите 0 65535 ok но если ввести 65536. U. U. то вы увидите 1 0 ok
Вы понимаете, в чем тут дело? Если нет, то пропечатайте результаты последних двух примеров в двоичной форме (2 BASE !).
Тогда вы получите такой результат: 0 1111111111111111 для числа 65535 и 1 0 для числа 65536. Если вы переходите от 65535 к 65536, происходит превышение максимального значения числа одинарной длины (целое число находится на вершине стека), при этом младший бит второго числа в стеке устанавливается в "1". То, что происходит с двумя верхними ячейками стека, аналогично тому, что происходит с двумя байтами в стеке, когда число одинарной длины изменяется с 255 на 256. Другими словами, с числами двойной длины Форт обращается так же, как с числами одинарной длины, но для них используется 32 бита. И при этом можно отображать числа двойной длины как со знаком, так и без знака. (Если это вас смущает, просмотрите материал о хранении чисел в двоичной форме из гл. 3.)
Для чисел двойной длины применяется набор арифметических операций, полностью аналогичный набору для чисел одинарной длины, поэтому мы даже не станем приводить примеры. Если вы не совсем понимаете, что делает тот или иной оператор, посмотрите их определения в приложении А. Вот эти операторы: D+, D-, DMAX, DMIN, DABS и DNEGATE (а в Форт-83 еще и D2/). Имеется также набор операторов для сравнения чисел двойной длины, но они будут рассмотрены в гл. 7. В MMSFORTH и других версиях имеется еще ряд дополнительных арифметических операторов, например D*, D/, D*/, D*/MOD и D/MOD. Они действуют так же, как их эквиваленты для чисел одинарной длины, при этом те слова, которые обеспечивают сохранение промежуточных результатов операций с числами одинарной длины в виде 32-разрядных чисел, в данном случае сохраняют промежуточный результат в виде 64-разрядных чисел (т.е. чисел четырехкратной длины).
В MMSFORTH есть два полезных слова #РТ и HI#. Слово #РТ запоминает положение десятичной точки (считая справа налево) в последнем введенном числе двойной длины. Так, например, 12.345 #PT. выдает число 4, в то время как 1.2345 #РТ. выдает число 5. Вы понимаете, что это слово может пригодиться для масштабирования чисел. Если нет, то из упражнений вам станет ясно, как его использовать.
Слово # РТ полезно также для форматного представления чисел, с которым мы познакомимся в гл. 5.
Слово #НI производит удивительное действие. В MMSFORTH все числа воспринимаются как числа двойной длины, но если в числе нет десятичной точки, то в стеке запоминаются только 16 младших битов числа. Независимо от наличия десятичной точки старшие 16 битов числа запоминаются в слове НI#. Таким образом, если ввести 12345678 HI# D. вы увидите число 12345678. Как работает это слово? Для чисел двойной длины старшие 16 бит запоминаются на вершине стека, слово НI# снимает старшие 16 битов и помещает их в стек. Следовательно, если с клавиатуры вводится число двойной длины, а после него слово Н1#, то число всегда запоминается в виде числа двойной длины.
Для манипуляций в стеке с числами двойной длины имеется набор слов, аналогичных словам для работы с числами одинарной длины: 2DROP, 2DUP, 2OVER, 2ROT и 2SWAP. Для доступа к числам двойной длины в памяти имеются также два слова 2! и 2@. Но эти слова так похожи на соответствующие слова для работы с числами одинарной длины, что лучше всего их рассмотреть в упражнениях.
Упражнения
1. Проверьте, что наибольшее 32-разрядное число без знака равно 4294967295. (Подсказка: вспомните о степенях числа 2.) 2. Рассмотрите табл. 2.2 и постройте аналогичную таблицу для манипуляций в стеке с числами двойной длины. 3. Напишите определение слова 2DROP под именем NEW2DROP. 4. Одинаковы ли по своему действию слова 2DUP и DUP DUP? Если да, то почему? и почему, если нет? 5. Определите слово 2DUP под именем NEW2DUP. 6. Определите слова 2SWAP79 и 2SWAP83 через ROLL. 7. Определите слова 2ROT79 и 2ROT83, используя слово ROLL. 8. Определите слова 20VER83 и 20VER79 через слово PICK. Слова, которые мы приводим в упражнениях 3-9, обычно для достижения быстродействия определены на языке ассемблера. 9. Определите слова 2ROLL83 и 2ROLL79, которые должны действовать аналогично ROLL. 10. Определите слова 2PICK83 и 2PICK79, действующие аналогично слову PICK, для тех, кто пользуется версией MMSFORTH. 11.
Определите слово S-> D для преобразования числа одинарной длины в число двойной длины. {Совет: проанализируйте приведенный пример слова Н1# из MMSFORTH.) 12. Что будет находиться в Н1#, если вы введете число 123? 13. Вот слово, которое возводит число 10 в степень п, если п находится на вершине стека: только ту часть числа двойной длины, которая стоит перед десятичной точкой. И еще напишите слово, которое должно возвращать часть числа, находящуюся после десятичной точки. (Указание: 123.25 - 123 + 25/100.)
Смешанные действия с числами одинарной и двойной длины.
В обоих стандартах (Форт-79 и Форт-83) имеются два обязательных слова для смешанных действий, в которых используются числа одинарной и двойной длины. В расширенных версиях Форта их еще больше.
В Форт-79 есть слова U* и U/MOD (аналогами их в Форт-83 являются UM* и UM/MOD). Вы уже раньше узнали, как напечатать число без знака, и, вероятно, вы понимаете, как складывать и вычитать целые числа со знаком и без знака: никакой разницы, нет, с какими числами вы имеете дело. При умножении имеют значение знаки чисел, кроме того, при умножении больших 16-разрядных чисел может возникнуть переполнение. Слово U* (или UM*) производит умножение двух чисел без знака, возвращая в стек число двойной длины. Попробуйте ввести 1000 1000 U* D. и вы увидите 1000000 ok. Очевидно, что результат представлен 32-разрядным числом. Теперь попробуйте ввести 5 -5 U* D. тогда вы увидите 327655
Число -5 было воспринято как 65531, поэтому полученное произведение является верным для данного числа.
Второй оператор для смешанных действии U/MOD (или UM/MOD) производит деление числа двойной длины (находящегося в стеке вторым) на число одинарной длины, помещая в стек остаток , и частное в виде чисел одинарной длины. Можете проверить это на ваших собственных примерах. Кстати сказать, слова U* и UM/MOD (соответственно UM* и UM/MOD) являются частью основного языка, а не расширения его для чисел двойной длины. Расширяющие слова для смешанных oпераций хорошо иллюстрируют слова MMSFORTH.
В табл. 4. 3 показаны их функции. Из этой таблицы ясно видно, что они делают, а также приведены аналоги для чисел одинарной длины.
Слово Действие М* n n --- d М*/ d n n - d (промежуточ. результат - число тройной длины) M+ d n --- d M- d n --- d M/ d n -- n M/MOD d n -- n n DU* ud ud -- uq DU/MOD uq ud--- ud ud Таблица 4.3. Операторы для смешанных действий Обозначения: n - число одинарной длины; d - число двойной длины; q - число учетверенной длины, т.е. 64-разрядное; u - без знака.
Упражнения
1. Используйте UM* для преобразования числа одинарной длины в число двойной длины. Имеет ли значение знак( можно сделать это еще быстрее, попросту помещая 0 в стек.) 2. Вспомните, как хранятся в стеке числа двойной длины в старших и младших ячейках. Имея это в виду, определите следующие смешанные операторы, которые имеются в MMSFORTH: М*, М+, М/ и M/MOD. 3. В чем различие между U/MOD и M/MOD? а также между U* и М* ? 4. Число двойной длины 123.45 помещено в стек и представляет доллары и центы. Определите слово ->DOLLARS, которое должно возвращать число долларов в виде числа одинарной длины. Определите другое слово ->CENTS, которое будет возвращать число центов в виде числа одинарной длины. Проделайте это упражнение, пользуясь словами из MMSFORTH и стандартными операторами. 5. Определите слово FRAC, которое должно умножать число двойной длины на отношение двух чисел одинарной длины, т.е. выражение 500. 3 5, после которого стоит это слово, должно давать в стеке значение 300 в виде числа двойной длины. Теперь определите это слово, пользуясь смешанными операторами MMSFORTH (последнее определение является тривиальным).
Расширение операций над числами с плавающей запятой
Вследствие приведенных раньше в этой главе соображений во многих реализациях Форт включены расширенные возможности для работы с числами с плавающей запятой. Поскольку они не регламентированы стандартом на числа с плавающей запятой, то слова для операций с числами с плавающей запятой отличаются от версии к версии.
В MMSFORTH имеется хороший набор расширяющих слов, которые мы используем в качестве примера. Если в вашем распоряжении есть версия, в которой также используются числа с плавающей запятой, то она скорее всего похожа на MMSFORTH и вам, вероятно, будет интересно проследить за этим обзором, привлекая документацию вашей версии. Мы уже рассмотрели, что представляют собой числа с плавающей запятой и числа в показательной форме. После этого вы, возможно, захотите перейти к материалу о реализации операций с плавающей запятой в MMSFORTH. Мы предполагаем, что вы уже поняли представление чисел с плавающей запятой, "научную" форму представления чисел, как записываются числа в показательной форме и арифметические действия с ними. Во многих компьютерах имеется встроенный интерпретатор языка Бейсик, выполненный на основе ПЗУ - постоянной памяти (по английски ROM- Read Only Memory, т.е. только считываемая память). Этот интерпретатор, написанный в машинных кодах, производит множество операций над числами с плавающей запятой. Кроме обычных операций умножения, деления, сложения и вычитания в нем имеются программы вычисления трансцендентных функций (например, тригонометрических и логарифмической), а также большое количество операций с целыми числами. Вследствие того, что MMSFORTH первоначально был разработан для работы с ЭВМ TRS-80, модель 1, совместимой с IBM PC, и, поскольку обе ЭВМ имеют встроенный Бейсик, "зашитый" в ПЗУ, обычные операции над числами с плавающей запятой производятся путем вызова машинных программ из ПЗУ.
Так как обе машины могут работать с числами с плавающей запятой, представляемыми 32 и 64 разрядами, то MMSFORTH может оперировать с числами одинарной и двойной точности. Обратив внимание, что эти числа принципиально отличаются от целых чисел Форта одинарной и двойной длины (в связи с чем лучше говорить о целых числах как о числах двойной длины, а не двойной точности). Числа с плавающей запятой одинарной точности обеспечивают 5 значащих разрядов > диапазон 9.9999Е-38 - 1Е38.
Числа с плавающей запятой двойной точности имеют точность 11 значащих разрядов и диапазон 9.9999999999999999Е-38 - 1Е38.,Правила ввода и вывода чисел при использовании программ с плавающей запятой из ПЗУ точно такие же, как правила Бейсика уровня 1 для TRS-80 или Бейсика фирмы Microsoft, и приведены они в документации микрокомпьютера. MMSFORTH и большинство других версий Форт для IBM PC также поддерживают арифметический сопроцессор серии 8087, который может быть установлен в персональных компьютерах и других совместимых ЭВМ. Сопроцессор типа 8087 вместе с MMSFORTH позволяет работать с числами имеющими 16 значащих разрядов мантиссы и порядок от -4932 до 4932. Кроме того, он обеспечивает вычисление трансцендентных функций и всех функций обычной арифметики с плавающей запятой и ряда других. Однако микросхема 8087 выполняет все расчеты не с помощью процедур на машинном языке, а с помощью аппаратных средств, которые представляют собой часть электронных схем процессора 8087. Аппаратная арифметика микросхемы 8087 является очень быстродействующей, она в 100 раз быстрее, чем эквивалентная математика на машинном языке и чаще всего быстрее, чем арифметика целых чисел (см. гл. 8). Мы опишем два варианта реализации в MMSFORTH арифметических операций с плавающей запятой: в ПЗУ и на базе сопроцессора 8087.
Арифметика с плавающей запятой, реализованная на ПЗУ
Краткие сведения о функции арифметики с плавающей запятой, реализованной в MMSFORTH на базе ПЗУ, приведены в табл. 4.4. В этом разделе мы опишем некоторые характерные черты этой арифметики, а в следующем - ее отличия от реализации на сопроцессоре 8087.
Числа с плавающей запятой представляются в стеке с 32 разрядами при единичной точности и с 64 разрядами при двойной точности. Таким образом, все слова, которые применяются с целыми числами двойной длины, можно использовать с числами с плавающей запятой одинарной точности, включая слова для задания констант и массивов, которые будут рассмотрены в гл. 6. Перед вводимым числом с плавающей запятой должен быть знак %, Ввод производится в свободном формате, поэтому % 1-01Е2, % 10.1 и % .10100Е3 будут в результате помещать в стек одно и то же число , 10.1 в старших 32 битах как число с плавающей запятой.
D% будет помещать в стек число с плавающей запятой двойной точности. Арифметические и другие математические действия производятся так же, как с целыми числами, поэтому мы их обсуждать не будем. Например, %.551Е3 % 10 F+ TAN F. произведет суммирование чисел 55.1 и 10, получая 65.1, а затем выведет значение тангенса этой величины (по умолчанию - в радианах), которая равна -1.19366. Что же касается комплексных чисел, то они заслуживают дальнейшего рассмотрения, но мы отложим этот вопрос до реализации операций с плавающей запятой на базе сопроцессора 8087. Использование слов для работы с числами с плавающей запятой asder понятно из упражнений.
Упражнения
1. Оцените значения следующих выражений, вводя числа с клавиатуры: (а) 5.5 + 1200 (б) ln(23/5) (в) sin^2(55) + cos^2(45) (углы - в градусах) (г) длину стороны квадрата с площадью 10 квадратных дюймов (д) площадь круга с радиусом 3.25 дюйма. 2. Напишите слово, которое будет брать два целых числа из стека, вычислять площадь прямоугольника со сторонами, выражаемыми этими числами, и печатать ее, обеспечивая не менее 10 значащих разрядов. 3. Напишите определение слова FABS с именем NEWFABS, используя возведение в квадрат и квадратный корень. 4. Напишите слово для определения гипотенузы прямоугольного треугольника по теореме Пифагора. Следующее упражнение предназначено для более подготовленных. 5. Напишите слово, которое с помощью цикла DO-LOOP печатало бы таблицу значений синуса и тангенса для углов до 45 градусов с шагом 1 градус. То же самое сделайте с шагом 0.1 градуса.
Таблица 4.4. Набор операций над числами с плавающей запятой, реализованных в MMSFORTH на базе ПЗУ*
% ( -) Предшествует вводу числа с плавающей запятой F#IN ( - t) Запрашивает ввод числа с плавающей запятой с клавиатуры F. ( f -) Печатает число с плавающей запятой F.R ( f ширина поля -) Печатает число с плавающей запятой, выравненное вправо в поле указанной ширины F+ ( f1 f2 -- f3) Возвращает сумму двух чисел F- ( f1 f2 -- f3) Возвращает разность двух чисел f1-f2 F* ( f1 f2 - f3) Умножает два числа f1*f2 F/ ( f1 f2 - f3) Делит два числа (f1/f2) FABS ( f - f1) Берет абсолютную величину числа с плавающей запятой FMINUS ( f -- -f) Изменяет знак числа; эквивалентно NEGATE SGN ( f - n) Возвращает -1, если f0 FCOMP ( f1 f2 - n) Сравнивает два числа с плавающей запятой: n=-1, если f1f2 FOG ( f - log(f)) Возвращает натуральный логарифм числа f FOG10 ( f - lg(f)) Возвращает десятичный логарифм числа f EXP < f - exp(f)) Возвращает значение числа е в степени f 10^ ( f - 10^f) Возводит число 10 в степень f X^Y ( f1 f2 - f1^f2) Возвращает f1, возведенное в степень f2 1/X ( f - 1/f) Возвращает число, обратное f FIX ( f1 - f2) Возвращает число с плавающей запятой, округленное до целого INT ( f1 - f2) Возвращает вместо числа с плавающей запятой ближайшее целое снизу CINT ( f - n) Возвращает 16-разрядное ближайшее целое, меньшее f I-F ( n - f) Преобразует 16-разрядное целое число в число с плавающей запятой SQR ( f1 - f2) Возвращает квадратный корень от f1 RND ( f - f) Возвращает случайное число с плавающей запятой, как на Microsoft Бейсике DEGREES ( -) Дает указания Форту принимать углы в градусах RADIANS ( -) Дает указания Форту принимать углы в радианах SIN ( f1 - f2) Возвращает значение синуса от f1 COS ( f1 - f2) Возвращает значение косинуса от f1 TAN ( f1 - f2) Возвращает значение тангенса от f1 ATN ( f1 - f2) Возвращает значение арктангенса от f1 ATN2 ( f1 f2 - f3) Возвращает значение арктангенса угла отрезка, соединяющего начало координат с точкой х,у PI ( - f) Возвращает константу Пи RAD ( - f) Возвращает константу для перевода градусов в радианы L10 ( - f) Возвращает натуральный логарифм 10 DF#IN ( - df) Запрашивает ввод числа с плавающей запятой двойной точности с клавиатуры DF. ( df -) Печатает число с плавающей запятой двойной точности DF.R ( df ширина поля-) Печатает число с плавающей запятой двойной точности, выравненное в поле указанной длины вправо DF+ ( df1 df2 - df3) Возвращает сумму (df1 + df2) DF- ( df1 df2 - df3) Возвращает разность (df1 - df2) DF* ( dfi df2 - df3) Возвращает произведение (df1*df2) DF/ ( df1 df2 - df3) Возвращает частное (df1/df2) DFABS ( df1 - df2) Возвращает абсолютное значение df1 DFMINUS ( df1 - df2) Изменяет знак df1 DSGN ( df - n) Возвращает -1, если df0 DFCOMP ( df1 df2 - n) Сравнивает два числа с плавающей запятой двойной точности, возвращая -1, если df1df2 FDF ( f - df) Преобразует число с плавающей запятой одинарной точности в число двойной точности с плавающей запятой DFF ( df - f) Преобразует число с плавающей запятой двойной точности в число одинарной точности с плавающей запятой CP+ ( Cp1 Cp2 -Cp3) Возвращает сумму двух комплексных чисел CP- ( Ср1 Cp2 -Cp3) Возвращает разность двух комплексных чисел(cp1 ср2) CP* ( Cp1 Cp2 -Cp3) Возвращает произведение двух комплексных чисел (ср1*ср2) CP/ ( Cp1 Cp2 -Cp3) Возвращает частное от деления двух комплексных чисел (ср1/ср2) MAG ( cp - f) Возвращает модуль комплексного числа PHASE ( cp - f) Возвращает аргумент или фазу комплексного числа CPMINUS ( cp - -cp) Изменяет знак комплексного числа с плавающей запятой CONJG ( cр -- ср) Возвращает сопряженное комплексное число с плавающей запятой R-Р ( ср -- f f) Преобразует прямоугольные координаты в полярные Р-R ( f f- ср) Преобразует полярные координаты в прямоугольные
* Набор программ содержит также несколько слов для работы с числами двойной и учетверенной длины, например 4 SWAP, Обозначения: f - число с плавающей запятой; df - число двойной точности с плавающей запятой; ср - комплексное число с плавающей запятой; n - 16- битовое число со знаком; адр - адрес.
Арифметика с плавающей запятой, реализованная на сопроцессоре 8087
Реализация операций с плавающей запятой на основе микросхемы 8087 отличается от реализации на ПЗУ несколькими существенными моментами: 1) вместо стека в памяти ЭВМ используется стек микросхемы, поэтому версии Форта могут иметь различия; преимущество этого стека - в быстродействии, недостаток - в ограниченной глубине стека; 2) большая часть операций выполняется на аппаратном уровне микросхемы 8087, а не на уровне машинных программ; 3) числа в стеке микросхемы 8087 хранятся как 80-битовые, а в памяти как 64-битовые, что дает дополнительные 16 знаков точности по сравнению с 64-битовыми числами, и 9 разрядов для 32-битовых чисел. Диапазон значений порядка от -4932 до 4932 (80 битов), от -306 до 307 (64 бита) и от -38 до 38 (32 бита); 38 (32 бита); 4) скорость операций с плавающей запятой на микропроцессоре 8087 в 100 раз выше, чем на ПЗУ, и для некоторых операций даже превосходит скорость вычислений с 16-разрядными целыми числами; умножение на микросхеме 8087 производится на 45% быстрее, чем с целыми числами, а сложение на 5% медленнее; 5) гарантируется обычно 16 точных разрядов при фиксированном формате мантиссы. Ввод чисел так же, как и в варианте на ПЗУ, производится в свободном формате, за исключением того, что диапазон вводимых чисел увеличивается- А так как в микросхеме имеется отдельный стек, то можно использовать оба стека одновременно без взаимных помех. Например, можно написать выражение тогда получится следующий результат: 291 0.1000000000000000Е60
Это значит, что вычисления с целыми числами и числами с плавающей запятой производятся одновременно и независимо в двух стеках: складываются числа 55 и 236 и перемножаются 1Е50 и 1Е10 и печатаются оба результата.
Конечно, такая запись может привести к путанице и поэтому не рекомендуется. Скорость работы микросхемы 8087 в режиме плавающей запятой удивительна: она превосходит скорость микрокомпьютеров и приближается к скорости универсальных вычислительных машин. Время вычисления 100.000 операций сложения с плавающей запятой составляет чуть больше 10 с, причем большая часть времени затрачивается на организацию зацикливания программы. Очень мощная универсальная ЭВМ Cyber фирмы CDC на языке Фортран затрачивает на это немного более 1 с, но при этом гарантируется только 12 разрядов точности, в то время как на со процессоре 8087 - 16 знаков точности. Становится реальным вычисление таких выражений, как 1000!, что практически невозможно даже на мини-ЭВМ, Практический пример применения операций в плавающей арифметике из работы авторов: нужно было смоделировать процесс изменения свойств воды в озере под действием естественных процессов за период 1000 лет. Для моделирования необходимо было решать численными методами 15 дифференциальных уравнений на каждые 4 ч моделируемого процесса. На суперЭВМ CDC Cyber в режиме разделения времени решение заняло один день. При этом ЦПУ машины было занято приблизительно около часа, причем стоимость машинного времени составила несколько сотен долларов. Решение этой задачи на языке Бейсик в скомпилированной форме заняло бы более одного месяца работы микроЭВМ IBM PC в монопольном режиме. При использовании MMSFORTH на микросхеме 8087 результат был получен менее чем за один день, фактически быстрее, чем на суперЭВМ. Решение немногих задач, подобных описанной, окупают затраты на микрокомпьютер и программное обеспечение и очень экономно расходуют время большой ЭВМ.
Отсутствующие функции плавающей арифметики, реализованные на ПЗУ: P.R, FABS, FMINUS (вместо нее имеется FNEGATE), FIX, INT, CINT, I-P, RND, RAD и L10. Кроме того, исключены слова для манипуляций в стеке с числами учетверенной и двойной длины, так как в стеке микропроцессора 8087 они не требуются.
Вследствие того, что арифметические действия с плавающей запятой реализованы в микросхеме 8087 аппаратными средствами и отличаются от реализации на ПЗУ, набор программ MMSFORTH содержит некоторые новые функции, а другие были исключены. Однако все отсутствующие функции легко можно определить на основе имеющихся, и мы в этом убедимся в следующих упражнениях.
Таблица 4.5. Дополнительный набор функций с плавающей запятой, реализованных на микросхеме 8087, не обеспечиваемых в реализации на ПЗУ
$F. (8f - адр n) Преобразует число с плавающей запятой в адрес, 87> (8f -- n ) Преобразует число с плавающей запятой в 16-ти битовое число >87 (n - 8f) Преобразует 16-битовое целое в число с плавающей запятой ?DEGREES (- n) Возвращает 1, если значение угла вводится в градусах, 0 - если в радианах COS.SIN (8f -- 8f 8f) Возвращает на вершину стека синус, вторым сверху - косинус CP*R (Cp 8f - Cp) Умножает каждую часть комплексного числа на число с плавающей запятой CPNEGATE (Cp1 - -Cp1) см. CPMINUS в табл. 4,4 D87> (8f - d) Преобразует число с плавающей запятой в 32-битовое целое число D>87 (d - 8f) Преобразует 32-битовое целое в число с плавающей запятой DF87> (8f - df) Преобразует число с плавающей запятой из формата микросхемы 8087 в число двойной точности с плавающей запятой формата ПЗУ DF>87 (d - 8f) Преобразует число двойной точности с плавающей запятой из формата ПЗУ в число с плавающей запятой формата микросхемы 8087 F87> (8f - f) Преобразует число с плавающей запягой из формата микросхемы 8087 в формат ПЗУ F>87 (f - 8f) Преобразует число с плавающей запятой из формата ПЗУ в формат микросхемы 8087 FDROP (8f -) Очищает стек микросхемы 8087 FDUP (8f1 - 8f1 8f1) Эквивалент DUP для микросхемы 8087 FNEGATE (8f1 - 8f2) См. FMINUS в табл. 4.4 FOVER (8f1 8f2 - 8f1 8f2 8f1 ) Эквивалент OVER для 8087 FROT (8f1 8f2 8f3 - 8f2 8f3 8f1) Эквивалент ROT в стеке 8087 FSWAP (8f1 8f2 - 8f2 8f1) Эквивалент SWAP в стеке 8087 FVAL (8f1 - $) Возвращает адрес символьной счетной строки, если задано число в формате 8087 LOG2 (8f1 - 8f2) Возвращает логарифм числа с плавающей запятой по основанию 2 ONE (- 8f) Помещает в стек 8087 число 1.0000000
Примечания: обозначения такие же, как в табл. 4.4; 8f - число с плавающей запятой в формате стека 8087; $ - счетная строка (см. гл. 9).
О мнимых и комплексных числах
MMSFORTH - один из немногих языков программирования для микрокомпьютеров, который может работать с комплексными числами. Если вам нужны для работы комплексные числа, то вы, конечно, представляете, что это такое. Тем не менее мы приводим краткий обзор для интересующихся и тех, кто хотел бы узнать об операциях с комплексными числами, даже не в MMSFORTH. Комплексное число представляет собой сумму действительного и мнимого числа, т.е- а + bi, где а называется действительной частью, b - мнимой частью, a i является корнем квадратным из -1. (Действительное число, как отсюда следует, это "нормальное", не мнимое число).Так как не существует такого числа, квадрат которого является каким-либо отрицательным числом, то i не имеет физического смысла и называется мнимым числом или просто мнимым. Тем не менее мнимые числа порождаются в результате некоторых математических действий. И мнимые, и комплексные числа широко используются учеными и инженерами. Комплексные числа проще всего представить как векторы или, еще проще, как точки с координатами (х,у) в системе координат, называемой комплексной плоскостью, плоскостью Гаусса или круговой диаграммой Аржана. Величина а отображает координату х, b - координату у. Модуль, или величина, числа, - это длина отрезка, проведенного из начала координат в данную точку, в то время как угол между осью х и отрезком называется аргументом или фазой. Сопряженное комплексное число - это число, у которого мнимая часть умножается на -1, т.е. сопряженным к a+bi является число а-bi. Слова MMSFORTH MAG (от magnitude- модуль, величина), PHASE (фаза) и CONJG (сопряженный) делают именно то, что от них можно ожидать по названию (см. табл. 4.4). В MMSFORTH действительная и мнимая части числа помещаются в стек так, что на вершине находится мнимая часть. В реализации на ПЗУ они вводятся с помощью слова СР%, хотя их можно так же просто вводить как два числа с плавающей запятой, как это должно делаться в реализации на сопроцессоре 8087 в MMSFORTH.
В таблице 4. 4 показано, каким образом MMSFORTH позволяет выполнять умножение комплексного числа на действительное число, умножение, деление, сложение и вычитание двух комплексных чисел, определение модуля и фазы, а также нахождение сопряженного комплексного числа. MMSFORTH позволяет также преобразовывать представление точки в системе прямоугольных координат в полярные координаты. Последние можно рассматривать как форму обращения с комплексными числами, которая заменяет число с компонентами а и b, выражая его через модуль и аргумент (фазу). (В прямоугольной системе координат точка представляется величинами х,у; в полярной системе координат - величиной отрезка, проведенного из начала координат в данную точку, и углом между этим отрезком и осью х, т.е. фазой.)
Следует заметить, что выражение "действительные числа" употребляется специалистами по компьютерам некорректно, так как у математиков nmn имеет другой смысл. Многие специалисты по компьютерам считают, что действительные числа - это синоним чисел с плавающей запятой, в отличие от целых чисел. Но это совершенно неправильно: любое число, которое не является мнимым или комплексным, является действительным, будь оно целое или с плавающей запятой. Неверная терминология, возможно, обязана своим происхождением условностям, принятым в Фортране, где числа с плавающей запятой называются "действительными" (real). К сожалению, эта терминология проникла и в описание сопроцессора 8087 и ее следует избегать.
Если у вас есть набор программ MMSFORTH для работы с комплексными числами или какая-либо другая версия, вы сможете проделать следующие упражнения.
Упражнения
1. Следующие выражения могут быть вычислены в реализации функций с плавающей запятой на микросхеме 8087. Перепишите их, чтобы можно было работать в версии MMSFORTH, реализованной на ПЗУ: (а) $ 5.5 5 % 6.5 8 + F+ (б) $ 35 10 I-F F+ SIN (в) % -55 FMINUS (г) $ 35 FDUP 5 COS FSWAP SIN F+ . F. 2. Определите слова Форта для микросхемы 8087, сходные по звучанию и функциям со следующими словами, реализованными на ПЗУ: FABS.
CINT, I-F, RAD, L1О. 3. Определите слово FACT. предназначенное для вычисления факториала в-реализации на микросхеме 8087. Попробуйте вычислить 1000! Зафиксируйте время исполнения операции. (Факториал числа 3 равен 3х2х1, факториал 6 - 5х4х3х2х1.) 4. Определите величину ошибки дробно-рациональной апроксимации числа Пи, приведенной в табл. 4.2. 5. Используя комплексные числа, определите слово ANGLE (угол), для того чтобы найти угол между гипотенузой и прилегающей стороной прямоугольного треугольника, если в стеке находятся значения длин обеих прилегающих сторон треугольника (рис.4.1).
6. Аналогично определите слово HYPOT (гипотенуза) для вычисления длины гипотенузы. 7. По определению, величина рН представляет собой взятый со знаком минус логарифм концентрации ионов водорода: pH=-log[H-]. Определите слово, которое по значению в стеке величины рН и абсолютному значению увеличения концентрации ионов водорода, выраженному в форме с плавающей запятой, рассчитывает фактическое увеличение рН. Можете ли вы предложить способ, как сделать это в целочисленной арифметике? 8. Одна из рутинных задач робототехники - преобразовать перемещение плеча, описываемого координатами х.у, в приращения радиуса и угла. Например, на рис.4.1 для перемещения из точки (х1,у1) в точку (х2,у2) требуется приращение угла (угол1-угол2) и приращение радиуса (радиус 1-радиус2). Определите слово ARMMOVE (движение, плеча), которое по заданным в стеке значениям х1,у1,х2,у2 будет выдавать в стек значения величин (угол1-угол2) и (радиус1-радиус2). За положительное приращение угла принимается движение по часовой стрелке. Для решения вам потребуются тригонометрические функции или комплексные числа.
Выводы
Как и обещали в начале этой главы, мы привели обзор основных понятий, связанных с числами. Некоторые из них, как, например, числа двойной длины, должен изучить каждый программирующий на Форте, в то же время числа с плавающей запятой могут потребоваться только тем, кто будет иметь дело с математическими функциями.
Тем не менее после этого обзора вы должны почувствовать, что Форт имеет мощные средства для решения математических научно-технических задач. Не иронично ли, что многие критики Форта считают его слабостью то, "что он не подходит для таких задач? Основанием для критики является в основном использование* обратной польской записи вместо алгебраической и отсутствие операций с дейcтвительными числами (в том числе с плавающей запятой) в стандартах языка и его простейших реализациях. Но ни одна из этих причин не становится трудно преодолимым препятствием для Форта. Применение стека обеспечивает Форту высокое быстродействие, которое так необходимо для математических приложений, а операции с плавающей запятой имеются в большинстве версий Форта. Форт используется в очень широком диапазоне отраслей науки и техники, например в лазерной энергетике, радиоастрономии, технике охраны окружающей среды, физической океанографии, робототехнике. Многие поняли, что Форт более мощный и простой язык, чем "классический" язык науки и техники Фортран. Мы надеемся, что язык найдет еще большее распространение среди тех, кто использует математику в технических дисциплинах.
Ввод и вывод
Ввод и вывод символов
Каким образом информация выводится из компьютера на дисплей и принтер и как ее ввести с клавиатуры? Конечно, вы уже вводили и выводили данные, нажимая клавиши и пользуясь словами. (точка), D., EMIT и т.д. Но имеются и другие возможности. Поскольку форт не предназначен для конкретного типа микрокомпьютера, его слова для операций ввода и вывода - универсальные (большинство версий Бейсика, напротив, приспособлены для определенных типов ЭВМ). Правда, во многих версиях Форта также имеются специальные слова для управления вводом-выводом, чтобы максимально использовать возможности конкретного микрокомпьютера. Мы рассмотрим как стандартные слова, так и, в качестве примера, некоторые слова для осуществления ввода-вывода из MMSFORTH. Некоторые вопросы мы отложим до гл. 9, потому что все подробности ввода чисел и символьных строк при исполнении программ будет проще понять после того, как вы изучите ввод символьных строк. Здесь полезно вспомнить, что буквы и числа вводятся в ЭВМ и выводятся одинаково в виде кодов ASCII. Это объясняется историческими причинами, знание которых поможет вам лучше понять методы ввода и вывода в Форте.
До появления персональных компьютеров ввод и вывод на ЭВМ производился через терминал. В своем простейшем виде терминал был малоинтеллектуальным устройством. Хороший терминал представляет в сущности пишущую машинку, и фактически в 1960-1970 гг., наиболее распространенной была модель электрической пишущей машинки типа IBM Selectric, Другая распространенная модель типа Teletype тоже представляла собой пишущую машинку, но с необычной механикой. Рассмотрим, что может делать пишущая машинка. Она может выводить на бумагу буквы, цифры и пробелы и, кроме того, передвигать с помощью валика бумагу. Перемещение бумаги может быть построчное и, кроме того, постраничное. При некотором умении можно продвинуть бумагу на половину строки или даже перемотать ее в обратную сторону- Конечно, возможно также переместить каретку на одну позицию по строке назад и перебить символ.
Но это почти все, что может делать терминал как пишущая машинка. Клавиатура терминала также подобна клавиатуре пишущей машинки, т.е. у нее есть клавиши для букв и цифр, пробела, перевода строки и возврата на позицию влево. И только двух дополнительных клавиш, которые есть у терминала, нет на пишущей машинке. Это клавиша управления и клавиша спецсимвола Escape (отказ). Для простого механического терминала при небольшом количестве функций было вполне достаточно кодов управления ASCII. Фактически терминалы ЭВМ и коды ASCII были прямо заимствованы из телетайпной техники 1930 - 1950 гг. Но с изобретением видеодисплейных терминалов открылись новые возможности.
Первые видеотерминалы работали так же, как и их механические прототипы. Они печатали символы в нижней части экрана и продвигали строки на экране вверх, подобно листу бумаги в пишущей машинке. Но возможности видеотерминалов были гораздо богаче. Например, они обладали возможностью разделять экран на две половины по горизонтали или по вертикали с различной информацией в каждой части или, например, выделять слова, меняя яркость. На более сложных терминалах, например Tektronix graphics, можно с высокой разрешающей способностью, отображать рисунки линиями. Однако по-прежнему они общаются с управляющей ЭВМ, фактически используя старый код ASCII. (В действительности есть два важных исключения - еще один код для телекоммуникаций, код Бодо, названный по фамилии его изобретателя, пионера в области автоматической телеграфии; кроме того, до сих пор при работе с ЭВМ фирмы IBM используется предложенный ею код EBCDIC.)
Более высокий уровень сложности стал возможен с изобретением персонального компьютера, содержащего в себе все оборудование ЭВМ. Его экран может рассматриваться как экран телевизора, отображающего сложные графические образы в цвете, с набором различных шрифтов, символов, с окнами, в которых отображаются выходные данные различных программ, включая графические, и многое другое. Достаточно одного взгляда на экран персональной ЭВМ Apple Lisa или Mackintosh, чтобы убедиться в их огромных возможностях.
Конечно же, и возможности клавиатуры также усложнились с введением специальных функциональных клавиш, клавиши для перемещения курсора и т.д. Однако проблема состоит в том. что по-прежнему ввод и вывод производятся по стандарту ASCII, хотя в большинстве случаев добавляется еще 127 символов и, таким образом, используется полный, набор из 256 символов, каждый из которых может быть представлен одним из 8-разрядных чисел, так называемым байтом. Другая проблема состоит в том, что почти в любой модели терминала и компьютера используются различные способы.управления функциями экрана. Определенные управляющие коды, например стирание влево, перевод строки, возврат каретки, т.е. те коды, которые применялись для управления телетайпами, оставили стандартными. Но другие были произвольно изменены. Можно ли в свете сказанного создать язык, способный работать на разнообразных ЭВМ? Сделать это чрезвычайно трудно. Поэтому стандарт языка Форт должен ограничивать только ввод-вывод кодов ASCII в расчете на использование простых терминалов, Для конкретных ЭВМ должны быть разработаны расширенные и нестандартные версии языка. Кроме вывода символов кодами ASCII терминал должен управляться также либо кодами ASCII, либо так называемыми Еsс-последовательностями. Мы рассматривали управляющие коды в гл. 3, здесь уместно вкратце упомянуть об Esc-последовательностях.
Код ASCII 27 называется Esc-префиксом или Esc-клавишей (отказ). Его можно ввести с клавиатуры ЭВМ, нажимая клавишу . (Некоторые ЭВМ не имеют специальной Esc-клавиши, в этом случае она имитируется некоторой комбинацией других клавиш; например, ввод кода Esc, если нет специальной клавиши, производится одновременным нажатием управляющей клавиши Ctrl, клавиши Shift (переключение регистров) и .) Назначение клавиши Esc состоит в том, чтобы сигнализировать, что некоторая последовательность символов после Esc должна рассматриваться как команда управления, а не код символа ASCII. Например, последовательность Esc С (27,67) должна произвести очистку экрана терминала и помещение курсора в левый верхний угол.
Печатающее устройство также, как правило, управляется Esc-последовательностями, например последовательность кодов (27,28) дает принтеру Centronics 739 команду продвинуть бумагу на полстроки вперед; (27,30) - на полстроки назад, а последовательность кодов (27,47,48) переводит устройство в режим, при котором следующие байты воспринимаются не как символы, а как графические команды. Ради чего мы приводим здесь это описание терминалов? Только ради того, чтобы принести извинения за отсутствие подробных объяснений, как управлять терминалом вашего компьютера или принтером. Вам нужно практически проверить, как реагирует на Esc-последовательности оборудование вашей ЭВМ. Мы расскажем здесь, как организовать посылку символов из Форта, а вы сами посмотрите, что они делают на вашем дисплее или принтере.
Вывод символов
В конце гл. 3 вы уже видели, что самое необходимое слово для вывода символов - EMIT, которое посылает на экран дисплея символ, соответствующий числу, которое хранится в стеке.' Попробуйте посмотреть, что делают различные управляющие коды, экспериментируя со словом EMIT. Например, если вы введете 48 EMIT 49 EMIT 10 EMIT 50 EMIT то скорее всего увидите 01 на одной строке и 2 - на следующей. Почему? Потому, что 48, 49 и 50 представляют собой соответственно ASCII-коды цифр 0, 1 и 2, а 10 - перевод строки, который при интерпретации обычно сопровождается возвратом каретки. Если вы напечатаете 48 EMIT 49 EMIT 8 EMIT 50 EMIT то увидите 02. Цифра 1 была стерта, так как код 8 представляет команду перемещения курсора на позицию влево, или стирания влево (Backspace). Испробуйте другие управляющие коды, чтобы установить их действие на вашем оборудовании, при этом могут возникнуть некоторые сюрпризы, например в MMSFORTH на машине IBM PC или на TRS-80 команда 27 EMIT приводит к обратной подаче строки, в то время как 12 EMIT очищает экран и возвращает курсор "домой" (т.е. в левый верхний угол экрана). Вы можете также попробовать коды ASCII от 128 и более, которые не являются стандартными и могут выводить странные символы и необычные буквы или продемонстрируют, что будет на экране, если из кода ASCII вычесть 127, т.е.
если игнорировать значение старшего бита.
Можно определить через EMIT два стандартных слова, действие которых столь очевидно, что мы приводим только их определение: : SPACE 32 EMIT ; (пробел) и : CR 13 EMIT 10 EMIT ; (перевод строки + возврат каретки) Не столь очевидно определение стандартного слова "пробелы": : SPACES 0 DO SPACE LOOP ;
Оно выводит пробелы, количество которых определяется числом на вершине стека. Нестандартное слово BL (пробел) включено во многие версии Форта. Оно помещает на вершину стека 32, т.е. ASCII-код пробела. Таким образом, можно дать иное определение пробела: : SPACE BL EMIT ;
Более сложное слово с использованием EMIT - это TYPE (печать). Его подробное описание вы найдете в гл. 9, где мы объясним символьные строки, но, чтобы начать пользоваться им раньше, мы рассмотрим его уже сейчас. Слово TYPE просматривает последовательность однобайтовых (8-разрядных) чисел (символов) из памяти и посылают их ASCII-эквивалент на экран. Для него требуется число на вершине стека, а ниже него - адрес. Слово TYPE выводит указанное число символов, начинающихся с этого адреса, на экран. Рассмотрим пример. Может быть, вам потребуется освежить в памяти счетные циклы DO-LOOP (гл. 1) и слово PAD из гл. 4. Слово I помещает текущее значение параметра цикла на вершину стека. Определим слово : PUTTEST 84 83 69 84 4 0 DO PAD I + С! LOOP ; , которое поместит символы ASCII символьной строки "TEST" в памяти, начиная с адреса, выдаваемого словом PAD. Теперь введите с клавиатуры PAD 4 TYPE и на экране появится строка "TEST". Обратите внимание, что коды ASCII должны быть помещены в стек в обратном порядке. А вот одно из возможных определений слова TYPE; попробуйте разобраться, как оно работает : : TYPE (адр n --) 0 DO DUP I + С@ EMIT LOOP DROP ;
Теперь нам следует познакомиться с символьными строками, о которых более подробно вы узнаете из гл. 9. Чтобы напечатать или сделать что-нибудь еще с символьной строкой, нам надо знать не только входящие в нее символы, но также их количество.
Один из способов запоминания строки, стандартный для Форта, состоит в использовании счетной строки (иногда ее называют нумерованной строкой). Счетная строка представляет собой область памяти, первый байт которой содержит число символов строки, за которым следуют собственно коды ASCII. Мы можем переопределить слово PUTTEST, помещающее строку "TEST" в PAD, следующим образом: : PUTTEST 84 83 69 64 4 PAD С! 4 0 DO PAD I 1+ + С! LOOP ;
4 PAD С! помещает число символов (счетное число) в PAD. Остальная часть определяемого слова размещает строку символов в более старшие адреса. Чтобы посмотреть строку, нужно, ввести с клавиатуры PAD 1+ PAD С@ TYPE
После этого в стеке будет находиться адрес начала строки и на вершине стека - число символов, т.е. стек подготовлен для распечатки словом TYPE. Поскольку этот принцип широко используется, предусмотрены стандартные слова для получения числа символов и адреса символьной строки: : COUNT (адр - адр+1 n) DUP 1+ SWAP C@ ; Поэтому вы сможете увидеть тестовое слово в PAD с помощью PAD COUNT TYPE
В гл. 9 вы увидите, что счетные строки обеспечивают большую гибкость при манипулировании с символьным текстом. Для примера скажем, что эта книга была набрана с помощью редактора текста, написанного на Форте, и эта программа редактора работает не хуже любой подобной программы, написанной на языке ассемблера.
В гл. 1 вы познакомились с более простым способом вывода строк с помощью слова ." (точка-кавычка), т.е. ." This is a test" (Это тест) выведет на дисплей строку "This is a test" (Это тест). Закрывающая кавычка не является словом Форта, это просто символ, который указывает слову ." на необходимость прекращения вывода строки на дисплей. В языке Форт-79 слово ." работает как в определении нового слова, так и вне его, с той лишь разницей, что вне определения строка выводится немедленно, в то время как, находясь внутри определяемого слова, она не выводится, пока определяемое слово не будет исполнено.
В версии Форт- 83 имеется незначительное отличие. Здесь не требуется, чтобы слово ." исполнялось вне определения. Для немедленного вывода строки зарезервировано другое слово .( (точка-скобка), которое выводит символьную строку, ограниченную правой круглой скобкой.)
Оно работает так же, как слово ." в Форт-79 вне определения, немедленно выводя текст, однако внутри определения .( так же выводит немедленно. Так, если ввести : TEST .(This is a test) : на экране будет немедленно выведена строка "This is a test" (Это тест). Таким образом, слово TEST в данном случае ничего не делает.
* Упражнения
1. Определите слово BS (стирание влево) по аналогии со словом CR (возврат каретки) так, чтобы при исполнении слова BS на выходе происходило бы стирание ранее выведенного предшествующего символа. 2. В MMSFORT есть слово PAGE (страница), которое производит стирание экрана и помещает курсор в левый верхний угол. Определите слово PAGE, имея в виду действие управляющего кода 12 в MMSFORTH. 3. Определите слово CRS, которое должно перемещать строки на экране вверх на величину, определяемую числом в стеке, т.е. слово CRS должно действовать как возврат каретки вверх, так же, как слово SPACES действует над пробелами. 4. Определите слово DASHES (черточки), которое выводило бы на экран число черточек, которое задается числом в стеке. 5. Определите семь слов для вывода текста "MAIN MENU" (основное меню). Теперь, пользуясь этим словом и словами, определенными вами в упражнениях 2-4, определите слово MENU, которое выводило бы на экран следующее меню (не забудьте о слове ." ):
----- ОСНОВНОЕ МЕНЮ ----- --------------------------------------- A ------ Это первый вариант выбора B ------ Это второй вариант выбора C ------ Это третий вариант выбора D ------ Это выбор Форта --------------------------------------- ------ ЧТО ВЫ ВЫБИРАЕТЕ ? --------
6. В MMSFORTH есть слово $. (вывод символьной строки), которое выводит на экран содержимое строки, если задать адрес счетной строки.
Дайте определение слова $..
Управление экраном дисплея
Как вы, возможно, догадываетесь, в языке Форт не предусмотрены стандартные слова для управления содержимым экрана дисплея; имеется множество других способов, с помощью которых компьютеры и дисплеи осуществляют это. Однако мы увидим некоторые дополнительные возможности, рассмотрев некоторые слова MMSFORTH, которые предназначены для работы с компьютерами типа TRS-80 и IBM PC. Вы сможете определить такие же слова для вашей машины, если поймете, как она исполняет управляющие коды, а возможно, в вашей версии Форта уже имеются подобные слова. Наиболее важная функция управления экраном состоит в возможности перемещения курсора по экрану. Если вы можете делать это, то сможете сделать почти все. Представьте себе экран как таблицу из символов с определенным числом строк и столбцов. Вам хотелось бы иметь возможность поместить курсор в любую строку и столбец. В MMSFORTH для этого служит слово РТС (put cursor - поместить курсор). Для него номер строки ожидается вторым в стеке, а номер столбца - на вершине стека. Таким образом, 0 0 РТС передвинет курсор в левый верхний угол экрана, в то время как 10 30 РТС поместит его в 11 строку и введет 31 пробел. Содержимое экрана не изменится (кроме того, что на месте курсора и сообщения "ok" прежние символы будут стерты). Указанную возможность можно рассматривать как форму, содержащую пустые места, которые должны быть заполнены. Для примера, пусть меню задает вопрос, ответ на который должен начинаться в строке номер 10 и столбце номер 30, а для текста вопроса отводится 10 пустых мест. Тогда слово, которое поместит курсор и вставит в пустые места пробелы, выглядит так: : FILL-IN 10 30 РТС 10 SPACES 10 30 РТС : (Заполнить) Конечно, слово должно печатать вопрос вместо того, чтобы оставлять пробелы. Приведем другое определение слова PAGE из последних упражнений, которое будет исполняться более медленно предполагается, что экран содержит 16 строк по 64 символа в строке, т.е. всего 1024 символа): : PAGE 0 0 РТС 1024 SPACES 0 0 РТС ; (Страница)
Для экрана с 80 символами на строке и 25 строками число 1024 должно быть заменено на 2000 SO * 25). (Очевидно, что скорость будет выше, если использовать какие-либо управляющие коды, которые предоставляет компьютер или дисплей, например 12 EMIT в MMSFORTH.)
В гл. 12 и 13, где представлен и рассматривается экранный редактор, вы увидите, как может быть определено слово РТС в других версиях Форта. Со словом РТС связано слово GTC (get_cursor, т.е. определить, где находится курсор). Оно помещает в стек номер строки, затем номер столбца. Приведем пример слова, подобного FILL-IN, которое поместит на экране после курсора 10 пробелов, если это возможно : : 10BLANKS GTC 10 SPACES РТС : (10 пробелов)
Слово GTC выдает положение курсора, 10 SPACES перемещает его на 10 позиций вправо, заполняя пробелами все, что находится на этих местах, и слово РТС снова возвращает курсор на старое место, используя имеющуюся в стеке от GTC информацию о положении курсора. При небольшой изобретательности слова РТС и GTC позволят вам делать впечатляющие и "дружественные" подсказки для ввода в программы ответов пользователя, которому остается только вводить их на месте пробелов. Можно практически обойтись без "перелистывания" экрана, если ввод и вывод информации производить на одном и том же месте экрана.
Чем больше возможностей имеет дисплей компьютера, тем более специализированные слова Форта требуются для использования этих возможностей. MMSFORTH совместно с IBM PC показывают хороший пример того, что может быть сделано, но не будем их здесь описывать, поскольку их много и они очень специфичны. Наиболее сложная возможность - это использование так называемых "окон". Они позволяют выделять на экране различные области, в каждой из которых появляются различные текстовые или графические изображения. Например, результат работы одной программы может быть представлен в основном окне, а меньшее окно может быть выделено для вывода листинга программы при отладке либо в одном окне можно помещать данные, вводимые в программу, а в другом - выходные данные.
С помощью программы векторной графики MMSFORTH можно задать несколько окон для отображения выходной графической информации, в то время как другие окна могут быть оставлены для вывода алфавитно-цифровой информации. Цвет содержимого каждого окна и границ окон и даже цвет отдельных слов могут быть заданы независимо. Если в IBM не установлена плата адаптера цветного монитора, тогда можно изменить вид курсора, ввести подчеркивание и т.д. Этим достигается большая гибкость. Другие версии Форта могут обладать подобными возможностями, но лишь немногие работают с окнами. К примеру, очень впечатляет использование окон MMSFORTH на ЭВМ Apple Macintosh.)
Вывод на печатающее устройство (принтер)
Стандарты Форта не определяют слова для осуществления вывода на принтер. Однако имеется много возможностей для стандартизации. В Форте имеются способы передать выходную информацию на принтер или одновременно на принтер и на экран. Мы опишем, каким образом это реализовано в MMSFORTH. Почти наверняка в вашей версии Форта вы найдете эквивалентные слова. Управление принтером реализуется с помощью слов PRINT, PCRT и CRT. После исполнения слова PRINT вся информация, которая передавалась на экран, передается на принтер. После исполнения слова PCRT вывод передается как на экран, так и на принтер, а после CRT прекращается вывод на принтер, информация выводится на экран. Это так просто. Другой способ управления предоставляют слова PRINTER (принтер), SCREEN (экран), ON (включено), OFF (выключено), используемые в контексте: PRINTER ON (вывод_на_принтер) SCREEN ON (вывод_на_экран) PRINTER OFF (выключить_вывод_на_принтер) SCREEN OFF (выключить_вывод_на_дисплей)
Указанные предложения производят несколько неожиданные действия. По идее это также должно быть просто, но не торопитесь, так как все несколько сложнее. Что произойдет, если послать 12 EMIT при разрешенном выводе на принтер (PRINT)? Код 12 для большинства принтеров является управляющим кодом, который заставляет принтер продвинуть бумагу на целую страницу (протяжка листа).
А это, очевидно, не то же самое, что очистка экрана и возвращение курсора в левый верхний угол экрана. Или предположим, что вы посылаете управляющий код перевода строки назад при активизации экрана командой CRT. Как отреагирует на этот код экран дисплея? До тех пор, пока вы передаете алфавитно-цифровой текст и простой перевод каретки, никаких проблем с выводом не возникает. Но если вы делаете всякие замысловатые операции с дисплеем или принтером, используя их управляющие коды, нужно быть очень осторожным при включении и выключении вывода на эти устройства, чтобы предотвратить нежелательную интерпретацию этих кодов. Это может привести к дополнительным осложнениям.
Построение простейших графиков из линий
До появления в составе компьютеров хороших видеографических дисплеев и координатных построителей вывод графических данных приходилось делать на построчное печатающее устройство или знаковый терминал. Действительно, вывод в такой форме и в настоящее время можно запрограммировать и выполнить гораздо быстрее, чем вывод в графическом виде. Идея того, как это делается, очень проста, в особенности для Форта. Вы уже видели демонстрационную программу построения линий в гл. 1. В данном разделе мы более подробно рассмотрим детали этого способа и, что более важно, предложим вам несколько упражнений на построение линейных графиков. Допустим. что вы хотите построить гистограмму , отображающую величины чисел, находящихся в стеке, Прежде всего нам потребуется слово, которое делает высоту столбика пропорциональной величине числа. Определим слово : XS 0 00 88 EMIT LOOP ; (выводить_"Х") печатающее ряд литер "X", длина которого зависит от числа в стеке, это и есть наш столбик. Теперь определим слово : PLOT (Нарисовать_график) CR (Начало с новой строки) DEPTH (Сколько чисел находится в стеке?) 0 (Нижний предел цикла) DO (Начало циклического повторения DEPTH раз) XS (Печатает строку из n литер 'х'; n - из стека) CR (Переходит в начало следующей строки) LOOP (Конец цикла; конец определения) ;
Теперь, если вы напечатаете 5 10 20 30 30 25 8 2 PLOT на экране дисплея, получится следующий график :
XX XХХХХХХХ ХХХХХХХХХХХХХХХХХХХХХХХХХ ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ ХХХХХХХХХХХХХХХХХХХХ ХХХХХХХХХХ ХХХХХ
Он должен быть вам знаком по гл. 1 и, конечно, не лишен недостатков. Числа должны быть помещены в стек в порядке, обратном тому, в каком они должны быть выведены. Длина столбика должна быть не больше ширины экрана дисплея. Существуют, однако, пути обхода этих проблем и способы построения более сложных диаграмм. С некоторыми мы познакомимся на следующих упражнениях и в последующих разделах.
Упражнения
1. Измените слово PLOT (вызов: PLOT1) так, что если число больше ширины экрана, то столбик будет доходить только до края экрана. (Указание: используйте слово MIN.) 2. Переделайте слово PLOT в PLOT2 так, чтобы вывод направлялся не на экран, а на принтер. По окончании вывода оно должно переключить вывод снова на экран. 3. Измените слово PLOT в PLOT3 так. чтобы оно печатало пары значений чисел в стеке литерами Х и Y одно под другим, т.е. если в стеке находятся числа 12 10 8 5, то слово PLOT3 выведет на экран гистограмму YYYYY ХХХХХХХХХХ YYYYYYYY ХХХХХХХХХХХХ Вам потребуется определить слово YS (литеры Y). 4. Измените слово PLOT в PLOT4 так, чтобы кроме вывода гистограммы оно выводило бы номера каждого столбика от 0 до 9 и между номером и началом гистограммы было бы три пробела. На выходе должна быть такая гистограмма 0 ХХХХХХ 1 ХХХХХХХХХХХХ 2 XX 3 ХХХХХХХХХХХХХХХХХХ 5. Измените слово упражнения 3 (назовите его PLOT5) так. чтобы вместо столбиков печаталась точка в конце каждого столбика, т.е. таким образом : 0 Х 1 Х 2 Х 3 X и т.д. 6. Во всех предыдущих программах построения гистограмм числа должны быть не больше числа позиций в ширину экрана. Измените программу упражнения 5 на PLOT6 так, чтобы можно было задавать числа до 1000 и чтобы числу 1000 соответствовал символ в 75-й позиции на строке экрана (считая, что 0 соответствует 1-я позиция). Если нужно, просмотрите раздел о масштабировании в гл. 4.
Не забудьте, что четыре позиции нужно предусмотреть для чисел и пробелов. 7. Измените слово упражнения 3 на PLOT? таким образом, чтобы вместо печати столбиков друг под другом получалась бы печать их рядом. Тогда на выходе должно быть XXXXXXXXXXYYYYY XXXXXXXXXXXXYYYYYYYY 8. Измените слово упражнения 7 на PLOTS, чтобы вместо столбиков печатались только их конечные точки, как показано: Х Y Х Y 9. Измените программу упражнения 8 на PLOT9, чтобы в конце каждой строки литер "Y" печаталась величина, как показано : Х Y5 Х Y8 10. Измените программу упражнения 9 на PLOT10. чтобы допустимыми были значения Х от 0 до 30, а значения Y от О до 1000. Считайте, что в строке 64 символа. 11. Это трудное упражнение. Предположим, что у вас есть пары значений х и у, которые вы хотите отобразить в виде графика зависимости у от х. Пусть значения х рассортированы так, что наименьшее значение находится на вершине стека, а пары значений в стеке расположены в порядке у, х. Например, в стеке может быть пара 30, 10, причем 30 - у, а 10 - х. Теперь предположим, что в стеке имеются числа 30 10 20 7 15 5 10 4 2 1 10 .
При исполнении слова PLOT должна получаться следующая картина : .X 0 1 .X 1 2 . . X 4 10 . X 5 15 . . X 7 20 . . . X 10 30
Таким образом, нулевое значение у должно быть представлено точкой, величина х должна быть отображена расстоянием по направлению вниз, величина у - расстоянием от левого края и после каждой отображаемой точки должны быть напечатаны значения х и у. Вам придется использовать вложенные счетные циклы типа DO-LOOP.
Вывод чисел
Вы уже знаете, конечно, как выводить числа на экран с помощью слов . (точка), U. и D., но это очень примитивно. Предположим, что вы хотите представить таблицу, в которой числа были бы выровнены по крайнему правому разряду, например: 1956 34215 343 23 5555 33333 21965 2 23
Или вы хотите напечатать число со знаком денежной единицы, например $294. Либо вам нужно напечатать число с десятичной точкой (в известном месте, поскольку предварительно вы сделали масштабирование), например, "$294.32".
Форт предоставляет превосходный выбор для отображения чисел в специальных форматах. Можно получить таблицу чисел с помощью печати, выровненной по правому краю. $294.32 можно напечатать, используя форматный вывод (иногда называемый выводом по шаблону). Печать в форме таблицы делается просто, вывод со знаком денежной единицы - более сложно.
Слово R. позволяет печатать число в формате выравнивания вправо. Это означает, что число печатается так, как будто бы его все время сдвигают вправо в поле некоторой определенной длины. Длина поля должна быть положена на вершину стека. Так, если вы введете CR 256 6 .R то увидите 256 (123456) (цифры в скобках не будут напечатаны, они приводятся, чтобы показать позицию печатаемого числа 256 в поле, отведенном для его вывода). Число 256 сдвигается на три позиции вправо и выводится своим последним разрядом в последней позиции б-местного поля. Если вы напечатали CR 256 8 ,R то увидите 256 (12345678) Это значит, что число будет выровнено вправо в поле длиной 8 мест. Пусть теперь вы вводите CR 211 10 .R -5 10 .R 23 10 .R CR 2 10 .R -231 10 .R 256 10 .R CR
Во всех случаях поле будет иметь ширину 10 позиций, выводимые числа будут выровнены вправо в этом поле, и вы увидите 211 -5 23 2 -231 256
Каждое слово CR начинает вывод с новой строки, и каждое число представлено сдвинутым до предела вправо в поле из 10 мест. В результате получается очень аккуратная таблица. Обратите внимание, что знаки "-" отрицательных чисел были учтены. Имейте в виду, что в отличие от. (точки) слово.R не выводит пробел после напечатанного числа. Поэтому CR 256 6.R 9 выдаст на экран: 2569 (1234567)
Слово.R не оговорено стандартами Форта, но нам не известна ни одна версия языка, в которой оно, равно как и слово D.R, отсутствовало бы. Во многих версиях Форта есть еще набор слов, как, например U.R, которое печатает числа одинарной длины без знака с выравниванием вправо. Предполагается, что всем этим словам должна предшествовать на вершине стека длина поля в виде числа одинарной длины, а ей должно предшествовать число, которое нужно напечатать как в случае чисел одинарной, так и двойной длины.
Если в вашем Форте нет слова U.R, то вскоре мы определим его вместе с другими полезными словами.
Форматный вывод чисел
Форматный вывод чисел (вывод по шаблону) наиболее сложный и все же наиболее гибкий способ отображения чисел на экране дисплея или печати на принтере. Он используется также для определения таких слов, как. (точка) и.R, и позволяет вводить в число, например, знак денежной единицы, десятичную точку, предшествующие незначащие нули, производить выравнивание в поле и т.д. Слова форматного вывода фактически преобразуют числа в последовательность кодов ASCII в форме, удобной для вывода на экран дисплея с помощью слова TYPE. Форматный вывод работает только с числами двойной длины. Чтобы применить его к числам одинарной длины, последние необходимо преобразовать в двойные.
Процесс форматного вывода начинается словом . Между этими двумя словами могут быть какие-то другие слова, описывающие, как должно выполняться форматирование. Слово #> оставляет в стеке адрес символьной строки и число символов в ней, подготавливая вывод для слова TYPE. Простейшее форматирование производит слово #S, которое преобразует все разряды числа в символьную строку. Приведем определение слова UD., которое, как ни удивительно, не включено во многие версии Форта: : UD. ( d - ) ( - адр счет ) (Завершает преобразование, помещает в стек адрес и значение счетчика) TYPE ( - ) (Выводит строку на экран) SPACE (Вставляет после числа пробел) ; (Заканчивает определение)
А как обращаться с числами одинарной длины? Достаточно преобразовать их в числа двойной длины. Для этого предусмотрено слово S-D. Чтобы понять его, вспомните, как представляется в стеке отрицательное число: : S-D ( n -- d) DUP 0< IF -1 ELSE 0 THEN ; (одинарное_в_двойное) Еще более изящно определяется это слово в Форт-79 : : S-D (n - d) DUP 0< NEGATE ; Если число одинарной длины в стеке отрицательное, то слово 0< возвращает 1, которую слово NEGATE превращает в -1 в качестве старшей половины числа двойной длины.
В случае, если число положительное, 0< возвращает 0, который словом NEGATE никак не изменяется. Определением слова S-D в Форт-83 будет : S-D ( n - d) DUP 0< ; поскольку в Форт-83 0< возвращает -1, если число отрицательное, или 0 в противном случае (см. гл. 7).
Приведем определение слова U. : : U. ( n -- ) S-D TYPE SPACE ;
Так как слово #> возвращает число символов в символьной строке, мы можем определить также слово UD.R: : UD.R ( d n - ) (Печать_беэ_энака_с_выравниванием_вправо) ROT ROT ( n d ) (Помещает значение длины поля на дно стека) ( n адр счет ) (Форматирует выводимое число) ROT OVER ( адр счет п счет ) - ( адр счет пробелы ) (Помещает перед числом пробелы) SPACES ( адр счет ) (Перемещает курсор на число пробелов) TYPE ( ) (Выводит число) ; (Конец определения)
А вот определение для U.R : ; U.R ( n n --) SWAP S-D ROT OVER - SPACES TYPE ;
Вы можете представить себе, как оно работает по аналогии с UD.R. Понимаете ли вы, почему лучше было бы определить некоторое слово, которое использовалось бы как для определения слова U.R, так и UD.R?
Как это ни удивительно, U.R, UD.R и UD. не включены в большинство версий Форта. Возможно, вы пожелаете добавить их к своему Форту.
Как быть с печатью чисел со знаком7 Для них нам потребуется слово SIGN (знак), которое вставляет знак - (минус) в символьную строку, если число, находящееся на вершине стека, отрицательное. Если вы напечатали 1245. TYPE на экране появится число -1245.
Более полезно определение слова D., которое позволяет выводить числа двойной длины со знаком. : D. ( d - ) SWAP ( мл# ст# -- ст# мл# ) OVER ( ст# мл# -- ст# мл# ст# ) DABS (Берет из стека абсолютное значение двойного числа, оставляя старшую часть исходного числа на дне стека, включая знак) (Заканчивает преобразование) TYPE SPACE (Выводит число) ; (Конец определения)
Почему слово SIGN применяется после #S, а не перед ним? Потому что строка символов при форматировании получается в результате просмотра справа налево. Это значит, что первым преобразуется наименее значащий (младший) разряд, поэтому после исполнения слова #S к строке символов будет добавлен символ знака перед наиболее значащим (старшим) разрядом, где ему и следует быть.
Обратите внимание, что перед SIGN необходимо слово ROT, потому что слово #S не удаляет число из стека, а заменяет его числом 0 двойной длины. Процедуры форматного вывода еще более неудобны для чисел- со знаком. Поэтому для большего удобства желательно определить еще два новых слова: : TYPE SPACE : Для полноты дадим еще определение слова . (точка) : ; . ( n -) S-D TYPE SPACE ;
Как оно работает, должно быть понятно. В большинстве версий Форта. (точка) определена как операция форматного вывода, т.е. очень близко к этому определению- Мы обещали еще научить вас выводить числа в формате с десятичной точкой и символами денежных единиц. Это делается с помощью слова HOLD, которое помещает символ ASCII, заданный его кодом, в символьную строку. Так, например, 12.34 TYPE выведет на экран $1234. Или 12.34 TYPE представит на экране 1234%. Снова обратим внимание на то, что строка формируется в обратном порядке, т.е. в конец помещается символ, встречающийся первым, и наоборот.
Мы также обещали, что вы сможете вводить разделители, например десятичную точку, в выводимое число. Чтобы сделать это, вам потребуется другое слово #. Слово # выталкивает разряды числа по одному справа налево и помещает их в выходную строку. После того как группа разрядов преобразована, можно использовать #S для завершения преобразования. Попробуйте ввести 1234. TYPE и на экране должно появиться $12.34. Цифры 4 и 3 были помещены в выходную строку двумя словами #, операция 46 HOLD поместила в строку десятичную точку, слово #S поместило в строку оставшиеся два разряда 12, а 36 HOLD на вершину строки помещает знак денежной единицы $. Снова обратите внимание, что процесс форматного преобразования происходит справа налево.
Теперь вы можете самостоятельно определить слово для представления чисел в долларах и центах, и так как мы показали, что оно должно отображать числа со знаком, то с помощью этого слова вы сможете записывать долги: : D$. ;
Можно определить другие слова для более сложного вывода.
Здесь приведены все средства форматного вывода.
Фактически форматирующими словами являются только #S, SIGN, HOLD и #, но, как вы увидите из упражнений, они позволяют решать довольно сложные задачи форматного вывода. Следует напомнить, что между могут использоваться любые "правильные" слова Форта. Прежде чем перейти к упражнениям, небезынтересно рассмотреть, как работает слово #. Вот его определение: мы предполагаем, что в стек положено число двойной длины 123, а содержимое стека представлено в десятичной системе, чтобы комментарии были более понятными : : # (123 0) (Начало определения) BASE @ (123 0 10) (Извлекает основание) S-D (123 0 10 0) (Преобразует в число двойной длины) D/MOD ( 3 0 12 0) (Остаток - это последняя цифра. частное - остальная (непреобразованная) часть числа) ROT DROP (3 12 0) (Превращает остаток в одинарное число) ROT (12 О 3) (Помещает на вершину последний разряд) 48 + (12 0 51) (Добавление 48 преобразует число в код ASCII) HOLD (12 0) (Помещает в строку код ASCII) ; (12 0) (Конец определения)
Процесс состоит в отделении разрядов от числа с конца (младшего разряда) по одному с помощью операции D/MOD, преобразовании каждого из них в код ASCII путем добавления десятичного числа 48 и, наконец, помещении кодов ASCII в символьную строку словом HOLD. Слово #S можно определить через слово #, используя цикл, прекращающийся, когда частное станет равным нулю; в этот момент все разряды числа будут отделены. Приводимой слово может выполнить все указанные функции : : #S BEGIN OVER WHILE # REPEAT ;
Поскольку вы еще не изучили конструкцию BEGIN..WHILE...REPEAT, это определение может вам показаться или не показаться лишенным смысла, но главное, что делает #S: - повторяет операцию # до тех пор, пока возвращаемое число двойной длины не станет равным нулю. Материал гл. 8 сделает эту конструкцию более понятной.
Упражнения
1. Определите слово UD$. для печати чисел двойной длины без знака в формате долларов и центов, т.е. 1234. UD$. должно давать на экране $12.34. 2. Определите слово US$. для печати чисел одинарной длины в долларах и центах. 3.
Определите слово S$. для печати чисел одинарной длины со знаком в долларах и центах (включая, конечно, отрицательные числа). 4. Определите слово S$.R для печати чисел одинарной длины со знаком в долларах и центах, выровненных вправо в поле. длина которого находится в стеке, и работающее как R.. 5. Определите слово.L для печати чисел, выровненных влево. Числа со знаком, если таковые присутствуют, должны быть сдвинуты влево, но курсор, который показывает позицию, куда должно помещать следующее число, должен переместиться в поле вправо. Это значит, что после числа должно следовать некоторое количество пробелов. 6. Определите слово .DATE для форматного вывода даты. Так, 122388. DATE н а экране должно напечатать 12/23/88. 7. Определите слово .MDY, подобное .DATE, для печати даты в формате m. 12 d. 23 у. 88 месяц 12 день 23 год 88 8. Определите слово.PHONE (телефон), которое при вводе 824 959 2345. будет выводить номер телефона в формате (824) 959-2345 9. Переменная #РТ в MMSFORTH содержит в себе указатель места десятичной точки в последнем введенном числе двойной длины (считая справа налево). Определите слово FL. для печати числа с десятичной точкой в том же месте, что и во введенном числе. Так, при вводе 12.34 FL. на выходе должно быть напечатано 12.34 , в то время как при вводе 1.234 FL. должно получиться 1.234. Воспользуйтесь счетным циклом DO-LOOP между .
Ввод с клавиатуры
В Форте предусмотрено несколько слов для управления вводом с терминала. Некоторые из них стандартные, другие - расширяющие возможности языка. Здесь мы рассмотрим наиболее простые из этих слов.В Форте имеются достаточно мощные средства для определения других слов, управляющих вводом, однако вам будет проще с ними разобраться после того, как мы рассмотрим символьные строки в гл. 9. Дальнейшее изложение имеет целью дать вам необходимый на данной ступени минимум.
Со словом KEY мы познакомились раньше. После ввода этого слова программа приостанавливается, пока не будет нажата какая-либо клавиша, затем код ASCII выбранного символа помещается в стек.
Слово KEY удобно для того, чтобы выяснить код ASCII нажатой клавиши [ попробуйте ввести KEY. ]; но используется оно чаще всего для ввода одной литеры ответа на вопрос, тогда в зависимости от значения кода клавиши программа будет выполнять какие-либо действия. В MMSFORTH имеется родственное слову KEY слово и ?KEY. Оно обычно используется в цикле. Если ни одна из клавиш не нажата при очередном исполнении ?KEY в цикле, в стек кладется 0, если клавиша нажата, то в стек кладется значение кода ASCII этой клавиши. Таким образом программа выполняет какую-то задачу, пока не будет нажата клавиша, с этого момента программа будет выполнять другую задачу, в зависимости от кода клавиши. Как KEY, так и ?KEY позволяют осуществить выбор вариантов исполнения программы, но в то время как KEY ожидает ввода, слово ?KEY позволяет продолжать исполнение программы, пока не будет нажата клавиша. Очевидно, слово KEY может быть использовано в цикле для ввода строк символов, но более удобным для этой цели является слово EXPECT. Для него необходимо, чтобы в стеке был адрес, начиная с которого будет запоминаться символьная строка, и вторым в стеке - число символов, которое "ожидается". Когда встречается слово EXPECT, исполнение программы приостанавливается либо до ввода ожидаемого числа символов, либо до нажатия клавиши . Испытайте действие приведенного ниже слова : : WHATWORD ." Какое Слово ? " PAD 10 EXPECT ;
Когда вы введете WHATWORD, исполнение программы приостановится после печати на экране вопроса "What is the word?" "(Какое слово?). Теперь введите 10 букв, и, когда вы введете десятую букву, исполнение программы продолжится. Теперь введите PAD 10 TYPE и вы увидите те 10 букв, которые были записаны, начиная с адреса PAD. А теперь введите с клавиатуры 5 символов и после этого нажмите , затем введите 10 PAD TYPE. Вы увидите 5 напечатанных вами символов. То, что будет происходить далее, зависит от версии Форта, с которой вы работаете. Во всяком случае, прекращает действие слова EXPECT, т.е.
ввод.
Слово EXPECT имеет дополнительные возможности, о которых вы узнаете из гл. 9. В МMSFORTH имеется слово IN$, основанное на слове EXPECT, которое имеет аналогичное действие. Это слово ничего не ожидает из стека и помещает все, что было введено с клавиатуры, в строку со счетчиком, начиная с адреса PAD, до нажатия клавиши , оставляя в стеке адрес PAD. введите IN$, потом , а затем строку символов и снова . Теперь если вы наберете COUNT TYPE, то увидите строку, которую ввели. Адрес PAD был оставлен в стеке словом IN$, слово COUNT кладет в стек длину строки, и TYPE печатает ее. Нестандартные слова IN$ и ему подобные являются основными средствами для ввода текста в программу. Вы многое сделаете с их помощью в главе, посвященной литерным строкам.
До сих пор мы рассматривали только ввод и вывод литер и литерных строк. А как быть с числами? Конечно, можно записать их в стек с клавиатуры, перед тем как будет исполнено какое-либо слово или начнет работать программа. Ну а что делать, если числа нужно вводить во время исполнения программы? Это одна из наиболее слабых сторон стандарта Форт или по крайней мере одно из неудобств для программиста. К счастью, в большинстве версий эта проблема решается с помощью специальных слов. В MMSFORTH такими словами служат #IN и D#IN. Оба они приостанавливают исполнение программы и печатают на экране знак вопроса ?. Вводимые с клавиатуры до нажатия клавиши символы воспринимаются затем как число либо одинарной, либо двойной длины, в зависимости от слова, и помещаются в стек. Если используются недопустимые символы или число имеет недопустимую длину, появляется сообщение "Redo" (повторите), разрешая повторение ввода числа.
Попробуем ввести следующее определение : : TEST ." Какое число " #IN ." Вы ввели " . ;
Если в вашей версии слова #IN или эквивалентного ему нет, потребуется некоторая изобретательность. Прием состоит в том, чтобы ввести литерную строку и затем преобразовать ее в число, используя стандартное слово CONVERT (в некоторых версиях имеется эквивалентное ему слово >BINARY).
Достаточно мощное слово CONVERT работает только с числами одинарной длины без знака, и применение его вызывает некоторые трудности. Вы еще убедитесь в больших возможностях слова CONVERT в гл. 9.
Выводы
Проблемы ввода и вывода показывают суть замысла стандартов языка Форт. Они предназначены для обеспечения минимально необходимых средств для конструирования того, что вам может потребоваться, но не оговаривают все детали и свойства, которые вы бы хотели иметь. Например, слова форматного вывода, такие как EXPECT и CONVERT, в свою очередь, можно использовать для определения слов типа UD.R, $IN и #IN. Форт подвергается критике за небогатые возможности вво да-вывода, и, что касается стандартов, - это справедливо. Однако не представляет сложности добавить необходимые вам возможности, и в большинстве коммерческих реализациях языка они предусмотрены. Вам нужно внимательно ознакомиться с документацией на тот вариант языка, которым вы располагаете, чтобы узнать, что он может делать. Кроме того, в гл. 9 мы приведем определение еще нескольких полезных слов.
Хранение чисел в памяти
До сих пор числа, которые использовались Форт-программами, хранились в стеке. Если вы поработали с такими языками программирования, как Бейсик, фортран или Паскаль, это покажется вам достаточно странным, большинство языков не используют стек непосредственно, и числа в них хранятся в переменных. Переменные представляют собой ячейки памяти, которые используются для хранения чисел, в большинстве языков предусматриваются размещение и автоматические манипуляции с числами в памяти. При этом стек фактически также используется, но программисту не приходится им управлять. Рассмотрим следующую программу на Бейсике: 10 А=5 20 В=6 30 А=А+В 40 PRINT A
Число 5 запоминается в ячейке памяти, обозначенной как переменная А, число 6 - в ячейке, отведенной для переменной В. Когда значения А и В складываются, они на самом деле выбираются из памяти, складываются почти так же, как в Форте, а затем результат снова помещается в ячейку, отведенную для переменной А. Хотя стек, может быть, и использовался для операции сложения, программист и не подозревает об этом. Программа на Форте напечатает сумму чисел 5 и 6 следующим образом: 5 6 + .
Все действия будут выполнены в стеке, в данном случае пользователю Форта не обязательно даже знать что-либо о переменных. Программирующий на Бейсике может привести доводы за использование переменных: программисту не приходится следить, где находится число; в то же время программирующий на Форте будет возражать, что для переменных требуется больше места в памяти, обращение с ними требует больших затрат машинного времени и текст программы получается более длинным. Обе стороны правы, и, конечно же, любая большая программа где-то должна хранить числа, если они в данный момент не используются. К счастью, Форт тоже позволяет вам пользоваться переменными (и соответствующими методами хранения чисел), если вы предпочитаете их или они вам необходимы.
Какими же достоинствами обладают переменные, если вы можете обходиться стеком? Переменные служат для трех целей.
Во-первых, если у вас имеется большое количество данных, которые нужно где-то сохранить, пока они не потребуются программе. Во-вторых, с целью упрощения операций в стеке путем запоминания промежуточных результатов в виде переменных, а не в самом стеке. Наконец, в третьих, для присваивания с помощью переменных имени числу или группе чисел. Так же как переменные в алгебраических уравнениях, именованные переменные позволяют пользоваться математическими абстракциями, с помощью которых облегчается формализация алгоритма программы, а написанную программу проще понять. Если у вас есть переменные с именами SECONDS (секунды) и MINUTES (минуты), то гораздо проще помнить, что представляют собой эти числа, чем безымянные числа в стеке.
В более общей формулировке переменная представляет собой наименованную ячейку (адрес) памяти, которая может содержать число. Переменная в Форте- это слово, которое возвращает в стек адрес, по которому может храниться число. Для перемещения содержимого ячеек памяти, имеющих имя, в стек и обратно можно использовать слова @ (взять) и ! (занести), которые вам уже известны из гл.З. Мы рассмотрим сначала основные (но не самые удобные) способы для выполнения этих операций, а затем перейдем к более рациональным методам.
Создание переменных
Где можно надежно хранить переменные в памяти так, чтобы они случайным образом не изменились? Мы уже знаем, что Форт отводит часть памяти для своего словаря, в котором содержатся названия слов и их определения (в том смысле, что определения сообщают машине, какие операции она должна делать при исполнении каждого слова). Мы имеем возможность создавать в словаре слова, которые содержат числа. Когда такое слово исполняется, то единственное, что оно должно делать,- это помещать в стек адрес числа, которое оно содержит, что обеспечивает доступ к нему с помощью операторов ! и @.
Слово CREATE (создать) помещает на вершину словаря имя переменной и еще некоторую информацию (которая, например позволяет Форту искать в словаре другие слова).
Имя и эта информация представляют собой последовательность из нескольких байтов, называемую заголовком указанного слова. Когда исполняется слово, определенное словом CREATE, то в стек кладется адрес, следующий непосредственно после адреса заголовка. Слово ALLOT (от ALLOcaTe- разместить) резервирует определенное число байтов в словаре. Например, CREATE INCHES 2 ALLOT создает в словаре слово с именем INCHES (дюймы) и резервирует, или размещает, 2 байта, в которых может быть помещено слово одинарной длины. Если вы введете 12 INCHES ! слово INCHES положит в стек зарезервированный адрес, а ! запишет в него число 12. INCHES @ . затем возвратит число 12 в стек и напечатает его. Это так просто. Между прочим, слово CREATE имеет более сложные и изощренные применения, которые будут описаны в гл.14,
В этом месте мы совершим небольшое отвлечение в сторону, чтобы познакомиться с некоторыми словами для перемещения чисел между памятью и стеком. Для всех таких слов предварительно нужно задать адрес в памяти, с которым они работают. Вам уже знакомы слова @, !, С@ и С!. Слова 2@ и 2! соответственно извлекают и запоминают числа двойной длины. В некоторых версиях и в MMSFORTH определены также слова 4@ и 4! для работы с 4-байтовыми числами, с помощью которых представляются числа с плавающей запятой. Несколько отличаются слова ? и +!. Слово ? (обязательное в Форт-79, но не включенное в Форт-83) может быть определено просто как : ? ( addr - ) @ . ;
Оно извлекает число из ячейки памяти и печатает его на экране. Аналогично можно определить слово С? (имеющееся в MMSFORTH и других версиях) для извлечения и печати одного байта. Стандартное слово +! добавляет второе число из стека к числу, содержащемуся в ячейке с адресом, который находится на вершине стека. Так, если в ячейке памяти с адресом 22345 содержится число 250, то 5 22345 +! изменит содержимое на 255. Это равносильно следующим действиям : 22345 @ 5 + 22345 ! но выполняется значительно быстрее. Очевидно, что с помощью ALLOT можно резервировать место для размещения более двух байтов.
CREATE INCHES 4 ALLOT отводит место для хранения одного двойного слова, доступного с помощью слов 2! и 2@. Впрочем, более важно то, что в отведенном (резервированном) месте могут храниться несколько чисел, входящих в одно слово. Если выполнить CREATE YARDS 10 ALLOT будет зарезервировано место, достаточное для хранения пяти одинарных слов (10 байтов). Пусть теперь вы определили слова : !YARDS 2* YARDS + ! ; и : @YARDS 2* YARDS + @ ;
Тогда если ввести 23 2 !YARDS то в четвертом и пятом байтах будет запомнено число 23 (если принять нумерацию с нулевого байта), зарезервированное словом YARDS. Операция 2 @YARDS возвращает в стек число 23. Другими словами, вы можете зарезервировать место для хранения последовательности или списка чисел и затем определить слова, с помощью которых к ним можно обращаться. Такой список чисел называется массивом. Массив 1,3,45,671,23, каждое число которого называют элементом, называется одномерным, линейным массивом или вектором. Можно хранить массив в слове YARDS следующим образом: 1 0 !YARDS 3 1 !YARDS 45 2 !YARDS 671 3 !YARDS 23 4 !YARDS а обращаться к числу можно с помощью слова @YARDS. Так, 3 @YARDS . напечатает 671. По некоторым причинам многие считают массивы чем-то сложным и непонятным, на самом деле это просто список чисел и ничего больше.
Слово , (запятая) упрощает создание переменных и массивов. Оно резервирует два байта так же, как 2 ALLOT, а затем запоминает число, находящееся на вершине стека, в отведенных двух байтах. Поэтому CREATE INCHES 7 , будет эквивалентно CREATE INCHES 2 ALLOT INCHES 7 !
В обоих случаях будет создана переменная INCHES, которой будет присвоено значение 7. Слово (запятая) особенно полезно при определении массива. CREATE YARDS 1 , 3 , 45 , 671 , 23 , создает массив YARDS и инициализирует его содержимое гораздо проще, чем было показано раньше. В особенности оно полезно для создания таблиц данных, которые не должны изменяться. Наконец упомянем определенное во многих версиях Форта, и в том числе в MMSFORTH, слово С,.
Оно выполняет то же, что и , (запятая), но резервирует место и запоминает число в диапазоне 0- 255 только в одном байте. (Некоторые процессоры имеют адресацию памяти, не допускающую использование слова С,.)
Упражнения
1. Создайте две переменные, FEET (футы) и INCHES (дюймы). Теперь определите слово F->Т для преобразования числа из FEET в число, выраженное в дюймах, результат должен помещаться в переменную INCHES. 2. Проделайте упражнение 1 для чисел двойной длины. 3. Дайте определение слова 2!, назвав его NEW2! , которое действовало бы как ! , но с числами двойной длины. 4. Дайте новое определение слова +! с именем NEW+!. (В Форте для ускорения оно определено в машинных кодах.) 5. Используя С@, дайте другое (но более медленно работающее) определение слова @, назвав его NEW@. 6. По аналогии определите слово NEW!, используя С! . 7. Определите слово VARSWAP (переставить переменные), которое должно переставлять содержимое двух переменных, адреса которых находятся в стеке. Тогда INCHES FEET VARSWAP занесет содержимое переменной INCHES по адресу переменной FEET и наоборот. (Указание: используйте PAD.) 8. Определите два массива, из 7 элементов каждый, 1WEEK (первая неделя) и 2WEEK (вторая неделя), пользуясь словом , (запятая) для инициализации всех элементов нулями. 9. Определите семь слов !SUN(!BCKp), !МОN(!пнд), !TUE(!втр) и т.д. так, что, если им предшествует число (например, сумма дневной выручки) и имя недели, они записывали бы это число в соответствующий элемент. Таким образом, 5 1WEEK !TUE запомнит число 5 во втором элементе (вторник) массива 1WEEK (первая неделя). 10. Напишите слова, соответствующие словам упражнения 9, которые способны извлекать содержимое переменных и класть их в стек. 11. Определите слово ESWAP (переставить элементы), которое должно переставлять два элемента именованного массива. Например, 1 3 1WEEK ESWAP должно переставить значения для понедельника (MONday) и среды (WEdnesday) массива 1WEEK (первая_неделя). Используйте слово VARSWAP. 12.
Создайте счетный массив однобайтовых элементов CNT (счетчик), в котором должно записываться, сколько раз числа запоминаются в различных элементах массивов 1WEEK и 2WEEK. 13. Определите слова +SUN, +MON и т.д., которые должны добавлять числа к соответствующим элементам именованных массивов. Так, например, 7 1WEEK +THUR должно добавить 7 к четвертому (THURsday- четверг) элементу массива 1WEEK. Эти слова должны также добавлять по единице к соответствующему элементу счетчика CNT. 14. Предположим, что все элементы массивов 1WEEK и 2WEEK были инициализированы нулями и что изменение их содержимого было произведено только с помощью операций +MON, +TUE и т.д. Тогда напишите слово DAY-AVE (сред-нее_за_день), которое выдавало бы в стек среднее значение чисел, добавленных к конкретным элементам массивов 1WEEK и 2WEEK. Так, если элемент 3 из массива 1WEEK содержит 20, элемент 3 из массива 2WEEK содержит 30, а элемент 3 массива CNT содержит 5, то выражение 3 DAY-AVE должно выдать в стек 10.
Вы написали простую, но в то же время достаточно хитроумную программу, заслуживающую внимательного рассмотрения производимых ею действий. Набор задач был составлен так, чтобы в конечном счете получилась программа, которая определяет средние значения выручки за определенный день недели по двум неделям, причем как ежедневные поступления, так и итоги за каждый день хранятся в двух массивах. В этом состояла наша цель. В большинстве языков программирования, чтобы добиться поставленной цели, надо написать программу со всеми деталями и отладить ее как единое целое. На Форте можно написать отдельные программы для каждой задачи и проверить их по отдельности. Разработка и написание программы целиком со всеми деталями чаще всего по блок-схеме называется программированием сверху вниз, и некоторые считают такую методику обязательной. В противоположность этому Форт позволяет разрабатывать программу более гибко и экспериментировать с ней, и вследствие этого каждое слово или задача программируются и проверяются отдельно, чтобы решить некоторую часть общей задачи.
В самом деле, вам не нужно было знать, приступая к упражнениям, какова их конечная цель.
Перемещение и заполнение содержимого массивов
Предположим, что имеется два массива по 20 элементов каждый, 1DATA и 2DATA, и нужно сделать так, чтобы содержимое массива 1DATA было равно содержимому массива 2DATA. Можно проделать это следующим образом: 1DATA 2DATA 20 CMOVE
Слово CMOVE ожидает, что в стеке должно быть два адреса и число (адр1 адр2 n -- ), тогда оно перешлет n байтов, начиная с адреса адр1, на адрес адр2. Причем сделает это очень быстро. Первая буква в слове CMOVE ассоциируется со словом character, т.е. литера, поскольку это слово часто используется для пересылки байтов, представляющих литеры в литерных строках. Слово MOVE (переместить), которое имеется в Форт-79 и большинстве реализации, но отсутствует в Форт-83, действует так же, но пересылает указанное число ячеек, а не байтов. Таким образом, 10 MOVE действует так же, как 20 CMOVE
Слово CMOVE обрабатывает байт за байтом. Это значит, что байт из ячейки с адресом адр1 переносится в ячейку с адресом адр2, байт из ячейки с адресом адр1+1 переносится в адр2+1 и т.д. Теперь рассмотрим случай,.когда адр2 находится между адр1 и адр1+n, т.е. область, в которую производится копирование, перекрывается с областью, откуда производится копирование. Теперь допустим, что первый байт в массиве 1DATA равен 219. Если напечатать 1DATA 1DATA 1+ 19 CMOVE то число 219 будет скопировано из ячейки с адресом 0 в ячейку с адресом 1, затем из ячейки 1 в ячейку 2, затем из 2 в 3 и, наконец, из ячейки 18 в ячейку 19. Другими словами, массив 1DATA будет целиком заполнен байтами со значением 219. А теперь предположим, что нужно установить значения всех элементов массива равными 0. Можно сделать это с помощью CMOVE. Вам нужно просто установить первый элемент массива равным 0, а затем скопировать его во все остальные байты массива. Вот необходимая последовательность действий: 0 1DATA С! 1DATA 1DATA 1+ 19 CMOVE
Действие CMOVE, в данном случае неправильное, можно наглядно показать на диаграмме.
Предположим, что слово CMOVE применяется к 4 байтам, содержащим в начале числа 10,11,12,13, и оно перемещает байт с адресом 0 на адрес 1 и т.д. Процесс будет происходить так, как показано на диаграмме 10 11 12 13 10 10 12 13 10 10 10 13 10 10 10 10
А нам нужно, чтобы из байта с адресом 1 число переместилось в ячейку 2; исходное число байта с адресом 2- в ячейку с адресом 3 и т.д. без наложения байтов друг на друга. 'Это значит, что из начального массива 10 11 12 13 должен в конце получиться массив 10 10 11 12 13. Слово, которое позволяет сделать это, называется (в Форт-79 оно необязательное). Слово
В данном случае перемещение элементов массива производится байт за байтом, но наложения не происходит. Очевидно, слово
Имеется также слово FILL (заполнить), которое заполняет некоторую область памяти байтами с указанным значением. Также 1DATA 20 0 FILL заполнит 20 байтов нулями, начиная с адреса 1DATA. Слово FILL- используется, чтобы занести число в определенное число байтов, начиная с некоторого известного адреса. В MMSFORTH и некоторых версиях есть еще слово ERASE (стереть), которое заполняет память последовательностью из нулей.
Таким образом, программа для предыдущего примера эквивалентна следующей: 1DATA 20 ERASE
Еще одно слово в MMSFORTH BLANK (пробел) заполняет область памяти кодами ASCII "пробел". Это равносильно 32 FILL
Очевидно вы сами можете дополнить приведенные слова собственными, имеющими специфическое назначение.
Упражнения
1. Определите слово FILL (назовите его NEWFILL), используя слово CMOVE. 2. Определите слово ERASE (назовите его NEWERASE), используя слово FILL. 3. Определите слово INITIALIZE (инициализировать), чтобы установить все элементы массива чисел одинарной длины в нуль, т.е. 1DATA 7 INITIALIZE должно установить все семь элементов массива 1DATA в нуль. Определение тривиально. 4. Напишите слово ARR-COPY (копировать массив), которое бы копировало содержимое массива одинарных чисел длиной п элементов в другой массив такой же длины, т.е. 1DATA 2DATA 7 ARR-COPY должно скопировать массив 1DATA в массив 2DATA.
Дайте два определения, используя как CMOVE, так и MOVE. 5. Напишите слово ARR_EXCH (копировать массив), которое производит обмен содержимого двух массивов одинарных чисел размерности n. Таким образом, 1DATA 2DATA 7 ARR-EXCH перенесет массив 1DATA в 2DATA и наоборот. (Указание: воcпользуйтесь словами PAD и MOVE. Возможно, потребуется временно хранить где-либо количество чисел.)
Переменная, константа и связанные с ними слова
До сих пор в этой главе мы рассказывали вам о создании переменных окольным путем. Это делалось для того, чтобы вы привыкли думать о переменных как ячейках памяти, что представляется неудобным, если вы знакомы с другими языками программирования. Существуют, однако, болей простые средства для обращения с переменными и массивами.
Стандартное слово VARIABLE (переменная) применяется для определения имени переменной и резервирования по ее значение двух байтов. Таким образом VARIABLE INCHES Производит те же действия, что и CREATE INCHES 2 ALLOT но немного удобнее, что более важно: применение слова VARIABLE в программе вместо CREATE делает ее более удобочитаемой и понятной. С переменной, которая определена, действуют так же, как с переменной, созданной словом CREATE. (Напомним снова, что слово CREATE включено в словарь Форта не только для того, чтобы создавать переменные, массивы и т.д. Оно также служит и для других целей.)
Для обращения с двойными числами имеется стандартное слово 2VARIABLE. В некоторых реализациях Форта есть другие разновидности слова VARIABLE, например CVARIABLE для хранения байтов, 4VARIABLE для переменных с плавающей запятой. Вам должно, быть понятно, что собственно слово VARIABLE (переменная) определяется следующим образом: : VARIABLE CREATE 2 ALLOT ;
Эта конструкция работает потому, что слово CREATE создает переменную при исполнении, а не при компиляции, т.е. не во время добавления определяемого слова VARIABLE к словарю. Массивы также могут определяться словом VARIABLE. Если ввести с клавиатуры VARIABLE INCHES 8 ALLOT то это будет равносильно CREATE INCHES 10 ALLOT причем вместо 8 используется 10, потому что два байта уже были резервированы словом VARIABLE.
С массивами, определенными любым из двух методов, можно обращаться одинаково.
Мы видим, что определение массивов как переменной словом VAMABLE несколько обескураживает. Оно не имеет никаких преимуществ перед использованием слова CREATE и, кроме того, не делает различия между переменной и массивом, а программа воспринимается труднее.
Похожим на слово VARIABLE является слово CONSTANT (константа). Оно используется для хранения таких чисел, которые внутри программы не будут изменяться (хотя иногда это правило нарушается). Для него требуется число в стеке, а после слова CONSTANT - имя слова, в котором будет храниться это число. При исполнении слова в стек кладется его содержимое. Поэтому после cлова, определенного как константа через слово CONSTANT, не требуется операция @ (извлечение содержимого). Например, если определить 5280 CONSTANT FT/MILE то при исполнении слова FT/MILE в стек будет помещено число 5280. Слова, определенные с использованием слова CONSTANT, лучше всего применять для таких чисел, как коэффициенты пересчета единиц измерения, константы уравнений. Конечно, с таким же успехом можно ввести число непосредственно в программу, но если программа подвергается изменениям, тогда каждое вхождение этого числа также должно быть изменено, в то время как при использовании константы число потребуется изменить только однократно. Кроме того, использование значащего слова вместо числа способствует облегчению понимания программы. Если вы знакомы с языком Бейсик, то можете подметить, что идея константы в языке Форт несколько отличается. В Бейсике константа обычно рассматривается как число, включенное в выражение, например 56.5 в строке 90 А = 56.5 * В
В языке Форт константа- это именованная ячейка памяти, фактически это разновидность переменной, которая посылает в стек не свой адрес, а содержимое.
Для переменных двойной длины в стандарте Форта предусматривается слово 2CONSTANT, во многих версиях (в том числе в MMS.FORTH) имеются слова CCONSTANT (однобайтовая константа) и 4CONSTANT для чисел с плавающей запятой.
Смысл назначения константы- определять величину, которая остается неизменной в программе, теряется, если константа изменяется. Вы должны иметь возможность узнать значение константы из ее определения, не прибегая к поиску того места, где она, изменилась. Это абсолютно обязательно, если Форт-программа должна быть записана в ПЗУ. Поэтому в большинстве случаев константа не должна изменяться. Но иногда бывают случаи, когда необходимо изменить константу. Один из них возникает при отладке программы, другой случай, когда константа по-разному используется в основной части программы и в другой. Наиболее важный случай, когда константа может быть изменена, если таким путем достигается сокращение времени. Для помещения константы в стек требуется несколько меньше времени, чем для извлечения значения переменной, и это нужно иметь в виду, если время исполнения программы для вас становится критичным. Поэтому вам следует знать, как изменить значение константы. В стандарте Форт-79 и большинстве версий, не придерживающихся стандарта Форт-83, обязательным является слово ' (произносится "тик"), которое кладет в стек адрес содержимого слова. Так, * FT/MILE выдает, адрес ячейки памяти, где хранится число под именем FT/MILE, будь оно константой или переменной. Поэтому для изменения значения константы единственное, что нужно сделать,- это поменять содержимое ячейки по этому адресу, например нужно ввести 6076 ' FT/MILE ! чтобы изменить сухопутные мили на морские. Теперь, если ввести FT/MILE, в стек будет заслано число 6076. (Заметьте, что точно такая же процедура (с излишними издержками) может применяться для изменения значения переменной.
В стандарте Форт-83 процедура сложнее, так как слово ' возвращает не адрес содержимого указанного слова, а другой адрес, связанный с этим словом, что может привести к путанице (вскоре вы поймете, что этот адрес полезен, но не для изменения константы). Однако если адрес, извлекаемый словом ', в стандарте Форт-83 известен, то слово >BODY даст нужный нам адрес, т.е.
тот, который извлекается в стандарте Форт-79 словом '. Таким образом, при вводе 6076 ' FT/MILE >BODY ! в стандарте Форт-83 мы получим то же самое, что при вводе 6076 ' FT/MILE ! в стандарте Форт-79. Дополнительное усложнение в Форт-83 состоит в том, что слово ' работает, как описано, только вне определения слов через двоеточие, т.е. оно является немедленно исполняемым. Все хорошо, если вы. хотите изменить константу, когда некоторая часть программы загружена.
Но если необходимо определить слово для изменения константы через двоеточие, то вместо слова ' нужно пользоваться словом [']. Так, например. : MAKE-NAUTICAL 6076 ['] FT/MILE >BOOY ! ; это определение слова, которое производит такое же действие, как слово, определенное следующим образом : : MAKE-NAUTICAL 6076 ' FT/HILE ! ; в стандарте Форт-79, потому что в данном случае не различается действие слова ', стоит ли оно в определении или исполняется непосредственно. Причины таких различий кроются глубоко в тонкостях работы Форт-системы. Короче говоря, несколько слов вроде ' в Форт-79 должны работать по-разному в состоянии компиляции и исполнения, хотя кажется, что их действие одинаково. Такие слова называют зависимыми от состояния. В стандарте Форт-79 разрешены слова, зависимые от состояния, в то время как в Форт-83 не разрешены, поэтому требуются два различных слова: одно для состояния компиляции, другое для исполнения. Подробнее мы на этом остановимся в гл. 15. Здесь, возможно возникнет недоумение, зачем нам потребовались и константы, и переменные? Почему бы не определить слово FT/FURLONG (футы_в_восьмую_часть_мили) таким образом, что когда вы введете 660 FT/FURLONG ! то число будет запомнено, как в примере с FT/MILE. В то же время, если вы просто введете FT/FURLONG, в стек будет выдано число 660 без операции @, как в случае константы. Если подумать, то при этом слово FT/PURLONG должно вести себя двумя различными способами: одним, когда оно используется само по себе, и другим, когда за ним следует операция @.
В MMSFORTH можно определять слова, в которых соединены свойства констант и переменных. Примерно таким способом эта возможность может быть встроена и в другие версии. В MMSFORTH имеется ключевое слово QUAN (от quantity- количество). Если ввести QUAN FT/FURLONG а после этого 660 IS FT/FURLONG то слово FT/FURLONG будет иметь свойства константы в том смысле, что оно возвратит число, если ввести слово. Но в то же время оно сходно с переменной, поскольку слово IS просто записывает число из стека в FT/FURLONG. Если вы хотите узнать, где хранится это число, то предложение AT FT/FURLONG! положит в стек его адрес. Таким образом, 660 AT FT/FURLONG! выполнит то же действие, что и 660 IS FT/FURLONG в то время как AT FT/FURLONG @ равносильно тому, что вы введете FT/FURLONG. Предусмотрены также слова CQUAN (для байтов), 2QUAN (для двойных чисел) и 4QUAN (для чисел с плавающей запятой), назначение которых очевидно. Кроме того, в языке предусмотрены также массивы QUAN и специальные слова типа QUAN для хранения чисел с плавающей запятой сопроцессора типа 8087 и для работы с массивами в расширенной памяти компьютера IBM. Уменьшение времени исполнения программы и требуемого числа ячеек памяти благодаря слову QUAN по сравнению с использованием переменных происходит, если QUAN применяется в программе более двух раз. Если в вашем распоряжении имеется MMSFORTH, то дальнейшие детали вы найдете в его документации.
Упражнения
1. Определите слова СVARIABLE и 4VARIABLE. 2. Определите слово ARRAY, которое при исполнении будет определять массив переменных, состоящий из 16 различных чисел, т.е. массив, с которым можно обращаться так же, как, например, с массивом, созданным предложением CREATE FT/HILE 20 ALLOT за исключением того, что он должен быть создан выражением 10 ARRAY FT/MILE (Заметьте, что перед словом ARRAY в стеке должно стоять количество чисел, а не байтов, которое содержится в массиве.) 3. Определите по аналогии слова CARRAY, 2ARRAY, 4ARRAY. 4. Определите две переменные 1LENGTH (длина) и 2LENGTH для помещения в них значений длины в разных единицах, например в сантиметрах и метрах или дюймах и футах.
Определите константу (1->2), которая переводила бы содержимое переменной из 1LENGTH в единицы длины переменной 2LENGTH, т.е. если в 1LENGTH даны значения в дюймах, а в 2LENGTH в футах, то значение, которое выдаст слово 1->2, должно быть равно 12. Напишите слово для преобразования содержимого 1LENGTH и запоминания его в переменной 2LENGTH. Как изменить программу, чтобы вместо дюймов и футов слово для преобразования единиц длины работало бы с сантиметрами и метрами? Это покажет вам, насколько полезны константы и почему им нужно присваивать значения при исходном определении. 5. Напишите слово с именем X->Y, которое находило бы значение Y, если в стек кладется значение Х в соответствии с уравнением Y = AX + В , считая, что А и В предварительно определены как переменные. Понятно ли вам, как можно изменить действие слова X->Y, не меняя его определения? Понятно ли вам также, почему использование переменных делает программу более гибкой и удобочитаемой ? 6. Если слово INCHES (дюймы)- это переменная, в чем различие исполнения операций INCHES и ' INCHES в стандарте Форт-79? 7. Предположим, что слово TO-INCHES (в_дюймы) представляет константу, в которой хранится коэффициент преобразования единиц, т.е. в нем должно содержаться число 12. если требуется преобразование числа футов в число дюймов, и число 36,- если преобразование числа ярдов в число дюймов. В предположении, что вы пользуетесь стандартом Форт-83, дайте определение двух слов SET-FEET (установить_футы) и SET-YARDS (установить_ярды) для записи соответственно чисел 12 36 в константу TO-INCHES. Можно ли использовать слова ' и >BODY?
О векторном исполнении операторов
Очень важным применением массивов (векторов) является управление программой для избирательного исполнения одного из нескольких возможных слов. Каждое слово имеет свой адрес, связанный с ним, который можно использовать для исполнения слова, фактически не вызывая его по имени. Чтобы понять, как это делается, определим сначала слово : MESSAGE ." Message" ; и затем введем FIND MESSAGE
Это слово определено в Форт-79; в Форт- 83 перед словом MESSAGE нужно ввести ' с клавиатуры либо ['], если слово встречается в определении через двоеточие; слова ' и ['] в Форт-83 действуют так же, как слово FIND в Форт-79. Слово же FIND в Форт-83 имеет совершенно другое назначение (см. гл. 14). Поэтому для Форт-83 нужно заменить слово FIND на ' или ['] в последующем сложении.
Если заглянуть в стек, то мы обнаружим, что в него был помещен некоторый адрес. Теперь, имея этот адрес в стеке, если напечатать EXECUTE (исполнить), вы увидите на экране слово "Message" (сообщение). Слово FIND (или ' и ['] в Форт-83) возвращает адрес, который дает возможность слову EXECUTE выполнить это слово. Таким образом, FIND NESSAGE EXECUTE производит то же самое, т.е. выдает на экран Message что делает слово MESSAGE само по себе. Это означает, что можно исполнить слово не только по имени, но также косвенно с помощью FIND, находя в словаре его адрес и затем используя слово-команду EXECUTE. При векторном исполнении применяется вектор (или переменная), в котором содержатся адреса слов, найденные словом FIND. Затем соответствующий элемент засылается в стек и слово EXECUTE исполняет нужное слово. Рассмотрим пример. Создадим массив из трех элементов: CREATE CHOICE 6 ALLOT и теперь определим три слова : 1PROG ." Program 1" ; : 2PROG ." Program 2" ; : 3PROG ." Program 3" ; Теперь запишем адреса этих слов в массив, вводя с клавиатуры FIND 1PROG CHOICE 0 + ! FIND 2PROG CHOICE 2 + ! FIND 3PROG CHOICE 4 + ! Если теперь введем CHOICE 2 + @ EXECUTE на экране появится сообщение "Program2", т.е. было исполнено слово 2PROG.
Предположим теперь, что вы хотите сделать выбор одной из программ 1PROG, 2PROG или 3PROG, нажимая одну из трех клавиш А, В или С. Вы можете сделать это, вводя коды ASCII клавиш с помощью слова KEY, преобразуя затем их в числа 0, 1 или 2 соответственно, пользуясь которыми можно выбрать и исполнить соответствующий элемент массива.
Следующее слово выполнит эту задачу: : CHOOSE KEY 65 - 2* CHOICE + @ EXECUTE :
Конечно, нужно быть внимательным, чтобы ошибочно не нажать другую клавишу; слово CHOOSE (выбрать) должно быть определено так, чтобы имелась защита от неправильного ввода. Чтобы запомнить адрес слова, которое должно быть исполнено, вместо массива может быть также использована переменная (в конце концов, переменная это всего лишь массив из одного элемента).
Предположим, что вы хотите иметь одно слово, которое исполняло бы одну из нескольких различных задач, например 1TASK, 2TASK и т.д. Когда вы составляете программу, то не знаете точно, какие это будут задачи. Можно в самом начале программы объявить переменную, например WHICH-TASK (какая_задача). Затем можно определить слово : DO-TASK ( addr - ) WHICH-TASK @ EXECUTE , которое исполнит то слово, адрес которого записан в переменной WHICH- TASK. Впоследствии при составлении программы вы определите 1TASK, 2TASK и т.д. Теперь, если вы хотите, чтобы слово DO-TASK (делать_задачу) исполнило задачу 1TASK, нужно ввести FIND 1TASK WHICH-TASK ! и слово DO-TASK- то же самое, как если бы вы ввели 1TASK. С другой стороны, если ввести FIND 2TASK WHICH-TASK ! то тогда DO-TASK исполнит задачу 2TASK. Другими словами, DO-TASK будет исполнять то слово, адрес которого находится в переменной WHICH-TASK. Вводом одного слова можно обеспечить выполнение одной из нескольких различных задач, в зависимости от значения содержимого переменной.
Некоторые Форт-системы очень широко применяют векторное исполнение, используя переменную или константу, которая должна содержать исполнительный адрес базовых слов ядра Форт-системы. Например, слово EMIT (вывести) может быть определено как : EMIT (EMIT) EXECUTE : где слово (EMIT) является константой, которая содержит исполнительный адрес. Вы можете определить теперь новое слово, например PRINTER (печать), которое в соответствии с вашим желанием изменило бы действие слова EMIT (возможно, определенное в машинных кодах), переводя вывод с экрана на печатающее устройство.
Тогда вы можете определить (EMIT) CONSTANT SCREEN чтобы сохранить старое определение слова EMIT. Теперь можно определить слова : PRINTER-ON FIND PRINTER (EMIT) ! : и : PRINTER-OFF SCREEN (EMIT) ' ,
Когда исполняется слово PRINTER-ON, вывод будет идти на печатающее устройство, а после PRINTER-OFF - снова на экран.
Существует множество других способов выполнения подобного рода задач с помощью векторного исполнения, которого мы едва коснулись. Вы найдете новые примеры в упражнениях, а более сложные его применения освещаются в книге Броуди "Thinking FORTH" (1984).
Другое очень полезное применение массива, подобное векторному исполнению,- это поисковая таблица, специализированной формой которой является таблица перекодировки. Предположим, что вы пользуетесь терминалом, который неправильно понимает стандартные коды ASCIl, т.e., например, забой влево, код которого 8, а на вашем дисплее это 22, вместо кода перевода строки 13 у терминала он равен 2, ваша программа посылает 1, чтобы очистить экран, в то время как терминал для этой функции ожидает код 24.
Определим массив следующим образом: CREATE TRANSLATE 24 С, 2 С, З С, 4 С, 5 С, 6 С, 7 С, 22 С, 9 С, 10 С, 11 С, 12 С, 23 С, 14 С, 15 С, 16 С, 17 С, 18 С, 19 С, 20 С, 21 С, 22 С, 23 С, 24 С, 25 С, 26 С, 27 С,
Теперь определим слово NEWEMIT, которое в вашей программе должно будет использоваться вместо EMIT: : NEWEMIT 1- TRANSLATE + С@ EMIT :
Тогда вместо того, чтобы непосредственно выводить на экран коды, которые программа кладет в стек, слово NEWEMIT будет просматривать в таблице TRANSLATE (перевести), каким новым управляющим символом должен быть замещен символ из стека, прежде чем он будет выведен на терминал. Таблицы перекодировки особенно удобны в применении к программам редактирования или управления терминалами, в которых управляющие клавиши могут по вашему желанию выполнять сложные специализированные функции.
Упражнения
1. Определите три слова, которые должны печатать "Dear Sir : ", "Dear Madame : " или "Dear Sir or Madame : " (Глубокоуважаемый сэр, Глубокоуважаемая мадам, Глубокоуважаемый(ая) сэр или мадам).
Определите переменную, которая должна содержать адрес одного из этих трех слов. Теперь определите слово SALUTATION (приветствие), которое печатало бы один из трех вариантов приветствия, в зависимости от значения переменной. Предположим, что вы хотите изменить выводимый текст во время исполнения программы. Как это может быть сделано? 2. Вы пишете программу с меню, которое позволяет по выбору пользователя сделать вывод на экран, нажимая клавишу 1, на печатающее устройство, нажимая клавишу 2, и на оба устройства одновременно, нажимая клавишу 3. В вашей Форт-системе имеются слова PRINT (печать), PCRT (печать и экран). CRT (экран), определенные, как описано в гл. 6. Определите слова, нужные для векторного исполнения, реагирующие на код нажатой клавиши в соответствии с меню. Вам нужно определить массив из трех элементов и инициализировать его адресами для слов PRINT, PCRT и CRT, а затем определить слово с помощью KEY и EXECUTE, которое исполняло бы одно из этих слов, когда нажималась клавиша 1, 2 или 3. 3. Оператор взвешивает болты и записывает их массу в компьютер. Оператору нужно узнать, сколько было болтов, имеющих вес менее 100 г, сколько- вес от 100 до 200 г и сколько- тяжелее 200 г. Кроме того, он хочет знать общий вес болтов каждой группы. Каждый раз после взвешивания болта оператор вводит его вес и затем слово BW (вес_болта). Для каждой группы болтов имеются две переменные: COUNT1 (счетчик1), COUNT2, COUNT3- для счета числа WT1 (вес1), WT2, WT3 -для общего веса болтов. Дайте определение этих переменных и инициализируйте их нулями: а) определите слово CLASS (группа), которое выдавало бы 0, если вес болта меньше 100 г, 1- если он находится в интервале 100-200 г и т.д. т.е. 125 CLASS должно выдать 1. (Указание: используйте оператор деления /.); б) определите массив, который содержит адреса ячеек, где хранятся числа болтов, и другой массив, где хранятся адреса суммарных весов. Заполните массивы соответствующей информацией. Эти массивы представляют собой поисковые таблицы; в) теперь определите слово BW, которое оператор вводит после веса очередного болта.
Это слово должно добавлять 1 в соответствующую переменную счетчика числа болтов и добавлять вес болта к соответствующей переменной суммарного веса. Для него потребуется слово CLASS; г) определите слово SUMMARY (итог), которое должно печатать количество, общий вес и средний вес в каждой группе. Таким образом, при вводе 0 SUMMARY должно печататься количество, суммарный вес и средний вес болтов, весящих меньше 100 г. 4. Напишите программу, которая выполняла бы функции, заданные в упражнении 3, но на этот раз запомните число болтов в трехэлементном массиве COUNTS и веса в трехэлементном массиве WEIGHTS. Это будет более эффективно, чем использование отдельных переменных. Можете ли вы объяснить, почему? 5. Теперь определите три слова, которые печатали бы менее 100 г от 100 до 200 г больше 200 г и поместите их адреса в массив, пригодный для векторного исполнения слов. 6. Определите три слова: CNT (счетчик), WEIGHT (вес) и AVERAGE (среднее) и три константы SMALL (малый), MEDIUM (средний) и LARGE (большой) так, что, если оператор вводит, например, MEDIUM WEIGHT на экране должно появиться сообщение Суммарный вес болтов от 100 до 200 г равен 139 или SMALL CNT выведет Число болтов весом меньше 100 г равно 33 и т .д.
Упражнения 3-6 иллюстрируют несколько принципиальных моментов: 1) Чтобы Форт-программа была полезной, не обязательно, чтобы она работала непрерывно. Чаще всего прикладные Форт-программы лучше всего реализуются путем создания инструментального набора слов, которые могут быть использованы, как в обычном калькуляторе. Так, например, слова BW и CNT, которые являются частью программы, но их исполнение происходит только тогда, когда эти слова вводятся; в то же время массивы постоянно отслеживают соответствующие данные; 2) имеется несколько способов реализации отдельных частей программы. Можно использовать как массивы, так и переменные, чтобы запоминать количество и вес, однако если. как правило, имеется несколько различных групп данных, то более эффективно применение массивов; 3) векторное исполнение основано на использовании поисковой таблицы, в которой записаны адреса слов; 4) поисковые таблицы можно использовать для других целей, например для того, чтобы познакомиться с методами хранения чисел в переменных. 7.
Вы пишете программу на языке Форт для обработки текста. После того как программа написана, вы решаете добавить возможность работы оператора с клавиатуры Дворака вместо стандартной клавиатуры QWERTY (стандарт IBM). Клавиатура Дворака показана на рис. 6.1.
{ & % # ! ( ) @ $ ^ * + [ 7 5 3 1 9 0 2 4 6 8 = " < > P Y F G C R L ? } , ' . / ] A O E U I D H T N S - ~
| : Q J K X B M W V Z \ ;
Рис.6.1 ( см. файл ris6_1.bmp )
а) напишите таблицу перекодировки, которая преобразует символы клавиатуры QWERTY в символы клавиатуры Дворака, т.е. если. например, нажата клавиша К. то в стек должна посылаться литера Т; б) переделайте слово KEY в DKEY, которое должно не просто принимать символ с клавиатуры и выдавать в стек его код: DKEY должно воспринимать входной код как символ клавиатуры Дворака, находить его код в таблице и помещать его в стек. Новое определение должно иметь примерно следующий вид: : DKEY KEY (Слова, с помощью которых производится просмотр); В каком месте вашей программы будет использоваться новое определение DKEY? 8. Теперь вы решили ввести возможность работы оператора по выбору с клавиатуры Дворака или QWERTY. В начале программы нужно ввести меню Введите 1 для клавиатуры Дворака Введите 2 для клавиатуры QWERTY и в зависимости от того, какое число введено: 1 или 2 в переменную ?KBD, должен быть записан адрес, которым определяется тип используемой клавиатуры. Определите теперь слово NEWKEY таким образом, чтобы при вводе 2 оно действовало бы так же, как и KEY, а если была введена 1, то производила бы просмотр таблицы и изменение значения кода как требуется.
Если эти упражнения показались вам трудными, не падайте духом! Они являются примером достаточно высокого уровня программирования. Они также продемонстрировали вам, как можно делать то, что почти недоступно другим языкам программирования. Вам следует внимательно разобраться в этих упражнениях и поэкспериментировать с применением аналогичных приемов, чтобы хорошо прочувствовать возможности векторного исполнения и поисковых таблиц, когда они вам потребуются.
Еще о массивах и матрицах
В MMSFORTH и некоторых версиях включено слово ARRAY (массив), которое упрощает обращение с массивами (это слово не является эквивалентом слова ARRAY, определенного нами выше). В данном случае, если ввести 15 ARRAY DATA-STORE вы организуете массив из 16 элементов с именем DATA-STORE (хранение_данных). Если затем ввести 5 DATA-STORE в стек будет положен адрес пятого элемента (считая с 0). Поэтому 293 5 DATA-STORE ! занесет число 293 в пятый элемент массива и 5 DATA-STORE @ . выведет на экран число 293.
Слово ARRAY может быть определено в других версиях с некоторыми отличиями, поэтому нужно смотреть вашу документацию. Одна деталь может несколько смутить вас. В математике принято нумеровать элементы массива, начиная с единицы, в MMSFORTH нумерация начинается с нуля. Этим объясняется, почему 15 ARRAY DATA-STORE создает массив из 16 элементов: как было сказано, номер последнего элемента равен 15, а так как нумерация начинается с 0, всего получается 16 элементов. В MMSFORTH имеются также слова CARRAY (байтовый массив) и DARRAY (массив чисел двойной длины), назначение которых очевидно по аналогии с CVARIABLE и 2VARIABLE.
Форт позволяет также пользоваться двумерными массивами, т.е. матрицами. Глубокое рассмотрение применения матриц выходит за рамки нашего рассмотрения; интересующимся рекомендуем книгу "Essential Computer Mathematics" (Lipshutz, 1982). Однако здесь мы должны познакомиться с ними вкратце.
Матрицы можно просто представлять себе как таблицу, содержащую некоторое число рядов и столбцов. Рассмотрим, например, таблицу 5 293 1982 823 5 56 99 211 5
Она представляет собой квадратную матрицу размерности 3х3; на диагонали матрицы находятся пятерки. Элементы матрицы обозначаются двумя индексами, показывающими их положение (строка, столбец) следующим образом: x1,1 x1,2 x1,3 x2,1 x2,2 x2,3 x3,1 x3,2 x3,3 Матрица может быть представлена в памяти как линейный массив. В данном случае имеется 9 элементов. Поэтому можно определить CREATE MATA 18 ALLOT После этого программист должен взять на себя ответственность за установление связи между положением элемента в строке и столбце и номером элемента в линейном массиве.
Полезно иметь универсальную формулу. Если обозначить положение элемента в строке через i, а в столбце через j и за начальный (1,1) принять элемент, находящийся в левом верхнем углу, тогда номер элемента в линейном массиве определится как е = n(i-1) +j , где n - число столбцов в матрице. Таким образом, элемент (2,3) в матрице МАТА в линейном массиве будет иметь номер 6. Учитывая это, можно определить слово для вычисления положения элемента в матрице МАТА, если в стеке находятся значения i и j: : MATAADR SWAP 1- 3 * + 1- 2* ; следовательно, чтобы записать, например, число 1234 в элемент матрицы (3,1) , нужно ввести 1234 МАТА 3 1 MATAADR + !
Такова стандартная форма обращения с матрицами в Форте, однако в MMSFORTH и других версиях имеются более удобные средства.
Слово 2ARRAY в MMSFORTH позволяет определить и организовать доступ к элементам матрицы без дополнительных хлопот с обработкой индексов; будьте внимательны и не путайте слово 2ARRAY (двумерный массив, матрица) и DARRAY (линейный массив чисел двойной длины). Чтобы определить матрицу 2МАТА размерности 3х3, нужно ввести 2 2 2ARRAY 2МАТА Числа, определяющие размерность матрицы, которые помещаются в стек, должны быть на 1 меньше числа строк ц столбцов, так же как при создании линейного массива число в стеке должно быть на 1 меньше его размерности. После того как матрица (в данном случае 2МАТА) определена,адрес ее элемента можно найти, вводя имя матрицы, если номера строки и столбца уже находятся в стеке. Таким образом, чтобы напечатать элемент (2,3) матрицы 2МАТА, нужно ввести 1 2 2МАТА ? Понятно ли вам, почему числа в стеке на 1 меньше фактических индексов элемента матрицы?
В MMSFORTH имеются также слова 2ARRAY и 2CARRAY. Их назначение очевидно. В гл. 11 будет рассмотрено, как можно определить такие слова.
Векторы и матрицы широко используются в разделе математики, который называется линейной алгеброй и занимается решением систем уравнений. Они также широко используются в науке и технике, в экономике для решения систем дифференциальных уравнений.
В некоторых языках программирования, например Фортране, АПЛ, фактически имеется целый набор стандартных функций для работы с матрицами. Более сложные применения матриц выходят за рамки нашего обзора, однако есть множество задач повседневной практики, в которых полезно применяются матрицы, обычно в том же виде, как для хранения табличных данных. Мы рассмотрим несколько таких приложений в следующих упражнениях.
Упражнения
Упражнения 1-4 похожи на то, что мы уже делали, тем не менее проделайте их для подготовки к упражнению 5, которое показывает матрицу как таблицу. Считайте, что можно пользоваться словами MMSFORTH для работы с массивами. 1. Определите массив из 10 элементов под именем NUMBERS (числа). Теперь определите слово COUNT (счетчик), которое должно добавлять единицу к элементу матрицы, в зависимости от введенного веса, находящегося в пределах 0- 100. Если вес от 1 до 10, то нужно прибавить к элементу 1, если вес от 9 до 20, то к элементу 2, если между 19 и 30- к элементу 3 и т.д. (Указание: используйте операцию деления /.) 2. Теперь определите второй массив, WTSUM (суммарный вес> и слово !W, которое будет добавлять вес к соответствующему элементу массива WTSUM так же, как числа добавляются к массиву NUMBERS. 3. Определите слова .TOT-#S (суммарное количество), .TOT-WT (суммарный вес) и .AVE-WT (средний вес)," которые соответственно будут печатать суммарное количество, общий вес и средний вес по группам в одной строке. Примените форматный вывод. (Используйте цикл DO, если же вы не уверены, что сможете это сделать, обратитесь к ответам.) 4. Определите слово !WT таким образом, чтобы каждый раз, когда оно исполняется, исполнялось бы слово !WT и при этом суммарный вес, число деталей и средний вес по группам представлялись бы на экране друг под другом в виде таблицы из трех строк и 10 столбцов. 5. Определите матрицу размерности 2х10, в каждом столбце которой содержится весовая группа, а в строках- общее количество и суммарный вес соответственно. Теперь проделайте упражнения 1-4 снова, используя не линейный массив, а матрицу; при этом необходимо, чтобы все соответствующие элементы матрицы обновлялись после каждого исполнения слова !W. 6.
Может оказаться очень удобным дополнительный стек. Создайте массив из 160 элементов с именем NEWSTACK, который вел бы себя как дополнительный стек. Теперь определите переменную STACKPOS (положение в стеке) для расчета текущего положения указателя стека, т.е. адреса вершины стека. Инициализируйте переменную в STACKPOS, чтобы она указывала на первый элемент в NEWSTACK. Затем определите два слова: XPUSH и ХРОР ; XPUSH (послать в стек) должно брать число из обычного стека и засылать его в NEWSTACK, изменяя соответственно указатель. Слово ХРОР должно изымать число из стека NEWSTACK и помещать его в обычный стек, вновь изменяя указатель. Модифицируйте слова XPUSH и ХРОР, назвав их PUSH и POP, используя слова МАХ и MIN так, чтобы указатель не мог выйти за границы массива стека. 7. Определите слова NEWDROP, NEWDUP и NEWSWAP, которые делали бы то же самое, что и слова DROP. DUP и SWAP в обычном стеке.
О разном
Кроме переменных, создаваемых программистом, имеются различные переменные в самой Форт-системе, например BASE (основание системы счисления). Исторически переменные, которые являются частью самого языка Форт, называются переменными пользователя, поскольку в многопользовательской системе каждый пользователь может иметь свой собственный набор этих переменных, Мы понимаем, что это не совсем удачное название, поскольку в большинстве языков программирования переменная пользователя определяется им самим, а переменные, являющиеся частью самой системы, например константа число п (3.1415...), называются системными переменными. Тем не менее, следуя терминологии Форта, мы будем называть переменные, входящие в Форт-систему, пользовательскими переменными. На самом деле пользовательские переменные это совсем не то же самое, что переменные, которые определяет пользователь. Переменные, которые вы определяете, в качестве части своего определения могут включать число. Переменная пользователя содержит число, которое может храниться в какой-либо.удаленной от определения ячейке, и на практике не нужно беспокоиться о том, где хранится эта переменная, потому что она ведет себя точно так же, как и переменные, определенные программистом.
Можно рассматривать переменную пользователя как константу, значение которой представляет адрес, по которому хранится значение переменной. Поэтому к ней применимы операции @ (извлечь содержимое) и ! (записать), как к обычным переменным.
Второй вопрос также относится к терминологии. Вы уже заметили, что некоторые слова языка Форт используются только для определения других слов. К ним относятся : (двоеточие), VARIABLE (переменная), CONSTANT (константа), QUAN и ARRAY (массив). Такие слова называются определяющими словами. Они применяются для составления программ. Слово CREATE (создать)- также определяющее слово, но это особое определяющее слово, потому что оно может быть использовано для создания других определяющих слов. Вы уже видели, как с помощью слова CREATE можно определить другое определяющее слово VARIABLE, которое, в свою очередь, используется для определения других слов. Как вы увидите в гл. 11, применяя его совместно со словом DOES>CREATE, можно полностью изменить характер работы вашего языка Форт.
Следует сказать несколько слов о стиле программирования. Если вы пишете программу, нужно поделить ее на логические секции, каждая из которых имеет определенное назначение. Довольно существенно определить заранее все константы, переменные и массивы, по крайней мере в начале каждой программной секции, а еще лучше- в самом начале программы. Объявления констант и переменных (так иногда называют их определение) группируются вместе, что облегчает внесение исправлений и модификацию программы, если это необходимо.
Еще одно замечание о стиле программирования. Большинство новичков в Форте стараются использовать переменные неоправданно часто. Это в особенности присуще тем, кто знаком с языками программирования, где переменные обязательны. Всегда при использовании переменной быстродействие получается ниже, чем при использовании стека. И это одна из главных причин предпочтительного использования стека в Форте. Хотя может показаться, что переменными пользоваться проще, так как при этом не нужно следить за тем, что делается в стеке, за это приходится платить ценой увеличения затрат памяти и времени.
Практически переменные (или константы и массивы) следует использовать для достижения одной из следующих целей: 1) для размещения больших объемов данных, которые невозможно хранить в стеке из-за того, что длина стека непомерно увеличивается; 2) для размещения чисел, которые используются неоднократно в разных сильно разнесенных секциях программы, или 3) для улучшения удобочитаемости программы.
Фактически есть еще одна причина для использования переменных. Если вы пишете программу, которой воспользуетесь всего несколько раз и которая без введения переменных потребует очень сложных манипуляций в стеке, то, может быть, использование переменных будет окуплено сокращением времени написания программы в ущерб времени ее исполнения. Но нужно сознавать, что если это делать постоянно, то развиваются вредные привычки в программировании.
Выводы
Хотя в отличие от других языков программирования Форт применяет стек для большинства манипуляций с числами и передачи аргументов из одного слова в другое, это не исключает использования переменных (а также констант и массивов), как и в других языках- Форт также обеспечивает более глубокое управление этими средствами хранения данных, позволяя создавать собственные конструкции с помощью слова CREATE. А с помощью слов типа CMOVE, ' , FIND и EXECUTE можно сделать то, что вообще невозможно в других языках программирования.
И в отличие от других языков переменные и массивы в Форте не только запоминают данные, но также сами управляют программами и языком. Векторное исполнение программ дает мощное средство программирования, которого нет в других языках. Например, переопределение слов EMIT или KEY равносильно тому, чтобы совершенно изменить действие операторов PRINT и INPUT в Бейсике. Это в некоторой степени должно объяснить вам, почему во введении мы говорили, что Форт дает большую мощность в управлении вашим компьютером, чем другие языки. Вы еще убедитесь в этом более ощутимо в последних главах книги, где речь идет о создании новых определяющих слов и использовании Форт-ассемблера.Но пока мы и не рассмотрели множество более простых свойств языка.
Обязательной принадлежностью любого языка программирования являются управляющие структуры, т.е. процедуры типа IF...THEN (если- то), которые позволяют принимать решения о дальнейшем ходе программы на основании определенных условий. Мы рассмотрим такие управляющие структуры в гл. 7.
Операторы сравнения и ветвления
Одной из наиболее важных задач, которую должен уметь делать любой язык программирования высокого уровня, является выполнение некоторых операций на основании истинности или ложности некоторых условий. Например, если два верхних элемента в стеке равны, должен быть выполнен один оператор, но если они не равны, то должно быть сделано что-то другое. Такие условные операции, возможно, проще понять на примере из Бейсика, потому что их структура близка к естественному языку. Если в выражении 100 IF A=B THEN X=Y ELSE GOSUB 300 переменные А и В равны, то значение переменной Х устанавливается равным значению Y; в противном случае выполняется некоторая подпрограмма, начинающаяся в трехсотой строке программы. Это позволяет программе выполнять разные действия при различных обстоятельствах. Подобные конструкции, которые управляют потоком (прохождением) программы, называются управляющими структурами; они включают в себя конструкцию IF...THEN , счетные циклы, которые вкратце уже рассматривались, и другие средства для осуществления переходов в программе, рассматриваемые в данной и последующей главах.
В отличие от Бейсика конструкция IF...THEN сравнивает числа не в виде переменных, а в стеке и в Форте каждое слово конструкции - это фактически подпрограмма. Конечно, отличается также и постфиксная форма записи. Поэтому условное исполнение и ветвление в языке Форт записываются на языке Форт несколько по-другому. Приведенному выше выражению на Бейсике в форте будет эквивалентна следующая конструкция: А @ В @ = IF Y @ X ! ELSE DOTHAT THEN
Конечно, на практике такое большое количество переменных в Форте никогда не используется. Рассмотрим приведенную конструкцию более внимательно. Предложение А @ В @ извлекает значения двух переменных и кладет их в стек. Операция - возвращает значение, зависящее от того, равны или не равны эти два числа (заметим, что - является оператором сравнения, а не присвоения, как на Фортране или в Бейсике). Если числа в стеке равны, то говорят, что условие истинно, тогда в Форт-79, MMSFORTH и большинстве других версий в стек возвращается 1, а в Форт-83 - число -1 (т.е. 16-разрядное число, у которого все разряды равны 1, или FFFF в шестнадцатеричной системе счисления).
Если числа не равны, то говорят, что условие ложно, при этом во всех версиях Форта в стек возвращается 0. Величина, возвращаемая в стек оператором сравнения, называется значением истинности, булевым флагом или просто флагом. В языке Форт любое ненулевое число всегда считается истинным и 0 всегда ложным. В данном случае если перед IP находится истинное (ненулевое) значение, то исполняются слова, находящиеся между оператором IF и ELSE, слова между ELSE и THEN пропускаются и затем продолжается исполнение той части, которая следует за словом THEN. Если перед IF находится ложное значение (0), исполнение перескакивает на слово, которое следует после ELSE и продолжается до слова THEN.
На рис. 7.1 проиллюстрирована эта идея. Обратите внимание, что конструкции IF-THEN и IF... ELSE... THEN могут быть использованы только в определениях через двоеточие. Вскоре мы более подробно обсудим конструкцию IF...ELSE...THEN, а пока познакомимся с некоторыми другими операторами сравнения.
Проверка истинности
Теперь кратко остановимся на булевом флаге. То, что в Форт-83 (и других языках) оператор сравнения возвращает значение флага -1, а не 1, имеет определенное основание. При обнаружении истинности условия число -1 или шестнадцатеричное FFFF, т.е. содержащее во всех разрядах единицы, оказывается иногда более удобным для использования его с булевскими операциями. Предположим, например, что требуется заменить число нулем, если флаг имеет значение ложь, и оставить без изменения, если флаг имеет значение истина. Если числа находятся в стеке, причем флаг на вершине, то оператор AND ("И") выполнит эту задачу в Форт-83, но не в Форт-79, для которого потребуется конструкция 0 = IF DROP 0 THEN
Хотя подобные случаи не так уж часты, иногда программа может дать выигрыш по времени, пользуясь этой особенностью флага. (При необходимости вспомните действие оператора AND в гл. 3.) Если посмотреть с более общих позиций, то в Форт-83 значение истина имеет не только сам флaг, но и каждый его разряд, что может оказаться полезным при операциях поразрядного сравнения.
Обратимся теперь к операторам сравнения; слово = (равно) - только одно из нескольких предусмотренных в языке Форт. Имеются также операторы сравнения для одинарных чисел и чисел двойной длины со знаком и без знака. В табл. 7.1 приводится сводка этих операторов. Большинство из приведенных операторов в комментариях не нуждаются. Некоторые особенности имеют операторы U< (и по аналогии DU
Операторы U< и UD< применяются для сравнения больших чисел, чтобы они не рассматривались как отрицательные. Как и для арифметических операций, при использовании чисел без знака требуется некоторая осмотрительность.
Особого внимания заслуживает оператор 0=. Он всегда меняет результат сравнения на обратный. каким образом, число, не равное нулю, превратится в 0, в то время как 0 превратится в 1 (или в -1 Форт-83). Для повышения удобочитаемости программ в Форт-79 слово 0= имеет стандартный синоним NOT (не), который действует аналогичным образом. (В Форт-83 оператор NOT действует по другому, как было описано в гл. 3.) Если целью сравнения является выяснение того, действительно ли в стеке находится нуль, то более оправдано применение оператора 0=, в то время как оператор NOT уместнее, если целью является изменение значения истинности на обратное. Заметим, что, если в Форте нет слова <>, его можно заменить конструкцией = NOT или = 0=. Точно так же оператор - (минус) будет возвращать значение 0 или не 0, как оператор <>. Запомните, что операторы сравнения снимают числа из стека. Поэтому, если вам потребуются эти числа для дальнейшей работы, их необходимо скопировать в стеке. Обычно это делается с помощью операций OVER OVER или 2DUP.
Таблица 7.1. Операторы сравнения *
Имя Операнды Определено ли Результат (возвращаемое значение) слова в стеке стандартом?
= n1 n2 да истина, если n1 = n2 <> n1 n2 нет истина, если n1=/=n2 < n1 n2 да истина, если n1 < n2 > n1 n2 да истина, если n1 > n2 = n1 n2 да истина, если n1 >= n2 0= n да истина, если n = 0 0< n да истина, если n < 0 0> n да истина, если n > 0 D= d1 d2 да истина, если d1 = d2 D< d1 d2 да истина, если d1 < d2 D0= d да истина, если d = 0 U< u1 u2 да истина, если u1 < u2 DU< ud1 ud2 да истина, если ud1=ud2 * Нестандартные слова включены в MMFORTH.
Прежде чем продолжить рассмотрение применения операторов сравнения, познакомимся с логическими операторами AND, OR и XOR. Они позволяют комбинировать несколько условий. Пусть, например, вы хотите выполнить какую-то операцию только в том случае, если для переменных А, В и С выполняются условия А=С и В=С. А @ С @ = В @ С @ = AND IF ...
Если обе пары переменных (первая и вторая) равны, то оператор AND, обнаружив в стеке 1 1 (или -1 -1), возвратит в стек 1 (или -1). Если одна или обе пары не равны, в стеке будет по крайней мере один 0, тогда оператор AND возвратит 0. Аналогично используется логический оператор OR. Допустим, что вы хотите выполнить какое-то действие, если или А=С, или В=С, или А=В=C. Это можно сделать следующим образом: A @ C @ = B @ C @ = OR IF ...
Если либо одно, либо другое равенство (либо оба) истинны, тогда по крайней мере одно условие истинно, поэтому оператор OR обнаружит в стеке хотя бы одну 1 (или -1, т.е. число со всеми разрядами, равными 1) и выдаст в стек значение истина. Если оба равенства ложны, то оператор OR, увидев в стеке два нуля, возвратит в стек значение ложь (нуль). Наконец, пусть необходимо, чтобы какое-либо действие выполнялось только в том случае, если одно равенство выполняется, а другое нет. Это можно сделать так : A @ C @ = B @ C @ = XOR IF ...
Оператор XOR возвращает в стек значение истина, если в стеке есть флаги истина и ложь (разряды в одном числе установлены в 1, а в другом не установлены). Комбинируя операторы сравнения с логическими операторами AND, OR и XOR, можно выполнять всевозможные комбинации сравнений. Если вы знакомы с булевой алгеброй и диаграммами Венна, то представляете, как изображать на них всевозможные сочетания условий для облегчения реализации большого разнообразия условного исполнения программ в комбинации с конструкцией IF...ELSE...THEN.
Упражнения
1. Пусть в версии языка, с которой вы работаете, есть только один оператор сравнения . Определите 0=. Определите =. Определите <>. Определите 0-.
Все введенные вами определения снабдите префиксом NEW. 2. Пусть числа а, b, с и d находятся в стеке в указанном порядке. Напишите последовательность слов, которая выдавал бы флаг истина тогда, и только тогда, когда выполняются следующие условия: а) а=b И c=d б) а=b ИЛИ с=d в) a=b И c<>d r) a=b ИЛИ c<>d д) a<>b И c<>d e) а=b ИЛИ c=d, но не одновременно ж) а>b И c b > с . 3. Используя слово MOD, дайте определение слова ?REM=0 (остаток равен 0?), которое возвращает флаг истина тогда, и только тогда, когда второе число в стеке делится без остатка на число, находящееся на вершине стека. 4. Дайте определение слова ?REM, которое будет возвращать в стек флаг ложь при условиях упражнения 3 и истина в противном случае. 5. Определите слово ?OPPOSITE (?противоположные), которое возвращало бы флаг истина тогда, и только тогда, когда оба числа по модулю были бы равны, но имели противоположные знаки. Определите слово NEW=, используя только О= и арифметический оператор. 6. Определите слово D= , пользуясь только арифметическим оператором и словом 0=. 7. Пользуясь соглашениями Форт-79, определите слово СОМР, которое будет возвращать -1, если число отрицательное, 0, если оно равно 0, и 1, если оно положительное, не применяя конструкции IF.
Операторы IF, ELSE и THEN
Операторы сравнения мало полезны без других операторов, которые реагируют на значение флага, возвращаемого в стек операторами сравнения. Наиболее важным из наших условных операторов является слово IF, используемое в конструкции IF...ELSE...THEN, рассмотренной в начале этой главы. Другие важные слова, реагирующие на флаг проверки условия, это UNTIL и WHILE, применяемые в конструкциях BEGIN-UNTIL и BEGIN...WHILE...REPEAT; эти конструкции позволяют зацикливать программу до тех пор, пока значение флага есть истина или ложь. Слово BEGIN и связанные с ним слова мы рассмотрим более внимательно в гл. 8.
Еще два условных оператора, имеющихся во многих версиях Форта, это ?DUP и ABORT", к ним мы еще вернемся в этом разделе.
Действие конструкции IF...ELSE-THEN можно в сущности представить следующим образом (см. также рис. 7.1) : ( флаг ) IF ( число в стеке не равно нулю, исполнить слова, стоящие здесь) ELSE ( если нуль, исполнить эти слова) THEN ( в любом случае продолжить отсюда)
Слово ELSE не является обязательным и часто опускается. Если оно отсутствует, то остается только одна возможность: исполнить слова, находящиеся между IF и THEN, если флаг, который оператор IF видит в стеке, является истинным. Со словом ELSE имеются две возможности : исполнить слова, находящиеся между IF и ELSE, если флаг имеет значение истина, или слова, заключенные между ELSE и THEN, если флаг имеет значение ложь. В обоих случаях исполнение продолжается после слова THEN. Лучше всего можно понять эти идеи на примере.
В гл. 6 вы познакомились с программой, в которой оператор вводил вес болтов, а затем слово WT. После этого число классифицировалось по величине и изменялись соответствующие переменные. Допустимый вес находился в диапазоне 0 - 300 г. Предположим, что, вы хотите предотвратить ввод оператором неверного числа, которое или очень велико, или мало. Можно сделать это, проверив величину числа с помощью операторов сравнения, а потом принять это число либо выдать сообщение об ошибке в соответствии с конструкцией IF... ELSE... THEN. Чтобы осуществить это, переименуйте прежнее слово WT в (WT) (заключением слова в скобки чаще всего подчеркивают, что оно является частью другого слова со сходным названием) и определите новое слово следующим образом: : WT DUP 0< OVER 299 > OR 0= IF (WT) ELSE DROP ." Вышли за пределы диапазона" THEN ;
Заметим, что необходимо включить ELSE DROP для исключения неверных данных из стека перед словом (WT). Слово WT можно немного упростить, убрав оператор 0м следующим образом: : WT -DUP 0< OVER 299 > OR IF DROP ." Вышли за пределы диапазона" ELSE (WT) THEN ;
Вообще слово 0= перед конструкцией IF...ELSE...THEN вовсе не требуется, так как точно такого же результата можно добиться, переставляя слова между IF и ELSE со словами, находящимися между ELSE и THEN.
Конструкции IF...THEN могут быть вложенными, т.е. их можно использовать внутри таких же конструкций. В слове WT мы классифицировали значения, пользуясь делением на 100 (/) и последующим векторным исполнением. Можно сделать то же, применяя оператор IF. Пусть, если вес меньше 100 г, должна выполняться задача 1TASK, если вес от 100 до 200 г - задача 2TASK, и задача 3TASK, если вес от 200 до 300 г. Можно сделать так; : (WT) DUP 100 < IF 1TASK ELSE DUP 200 < IF 2TASK ELSE DUP 300 < IF 3TASK THEN THEN THEN DROP ;
Метод векторного исполнения с операцией деления на 100 может показаться более изящным, но он не намного быстрее и, кроме того, не будет работать, если разбросы веса в каждой группе неодинаковы. Заметьте, что в данном случае мы использовали как IF...THEN, так и IF...ELSE...THEN конструкции. Первые два оператора IF нужно использовать с ELSE, потому что, если условия ложны, число должно переходить в следующий класс с большими значениями веса. А так как слово (WT) является частью слова WT, третий оператор IP используется без ELSE, потому что число больше 300 быть не может. Немного дальше мы покажем, что можно также применить конструкцию выбора одной из нескольких возможностей (переключатель).
Некоторые программисты строго придерживаются манеры выделять вложенные операторы IF...THEN, подчеркивая их вложенность, поэтому запись определения (WP) в предыдущем примере вызовет у них возражение, так как она плохо сформатирована, и они отдадут предпочтение такой форме записи: : (WT) DUP 100 < IF 1TASK ELSE DUP 200 < IF 2TASK ELSE DUP 300 < IF 3TASK THEN THEN THEN DROP ; Решайте сами, стоит ли ради наглядности программы занимать больше места на диске.
Вот еще один пример. Пусть нужно выполнить операцию D0IT, используя остаток от деления на 22 только тогда, когда число в стеке не кратно 22, т.е. если n 22 MOD возвратит в стек ненулевое значение. Вот как это можно сделать : : ?22-MULTIPLE 22 MOD DUP IF DOIT ELSE DROP THEN ;
Часто функция оператора IF может быть успешно выполнена без оператора сравнения.
В этом случае что- то должно быть сделано или не сделано в зависимости от того, находится ли в стеке нуль или не нуль. Сочетание ELSE DROP необходимо для того, чтобы убрать из стека нуль, порожденный оператором DUP. В подобных случаях очень полезно слово ?DUP, поскольку оно кладет в стек копию числа только в том случае, когда оно не равно нулю, заменяя два слова ELSE DROP. Вот, например, короткое определение слова ?22-MULTIPLE (кратно ли 22?): : ?22-MULTIPLE 22 MOD ?DUP IF DOIT THEN ;
Если в стеке находится нуль, слово ?DUP ничего не выполняет, следовательно, применяя его, мы избавляемся от заботы очищения стека от оставленного нуля. ?DUP не дает никакого выигрыша, если перед оператором IP стоит оператор сравнения. Но если перед IF следовала арифметическая операция и при ненулевом значении результата должна быть выполнена другая программа, тогда использование ?DUP экономит время - особенно в цикле. Применение оператора MOD совместно с IF весьма полезно в циклах DO-LOOP, когда нужно что-то выполнить с определенным интервалом. Например, : 7MULTIPLES? 1+ 0 SWAP 1 DO I 7 MOD 0= IF 1+ THEN LOOP . ." раз число 7 содержится в данном числе " CR ; при исполнении 38 7MULTIPLES? выведет в результате 5 раз число 7 содержится в данном числе
Хотя данный пример тривиален, в нем показан прием, который пригодится для составления более сложных программ, чем определение кратности одного числа другому.
Некоторые замечания о структурном программировании
Переход на исполнение определенных операций (т.е. подпрограмм) является одним из наиболее важных механизмов языка программирования. В таких языках, как Бейсик, допускается переход в любое место программы, куда вы хотите, не обязательно к четко оформленной задаче или подпрограмме. И возврат не обязательно должен происходить в точку, из которой произошел переход. Оператор Бейсика GOTO nnn где nnn - номер строки, позволяет делать переход в программе произвольно, с возвратом или без возврата в исходную точку, как вам кажется удобнее.
Это потенциально может привести к запутанной программе, в которой трудно проследить ход действий, что затрудняет ее отладку и модификацию.
Альтернативным методом является структурное" программирование. Упрощенно структурное программирование заключается в том, что программа составляется таким образом, что когда она уходит на подпрограмму, то ее исполнение затем продолжается с точки, следующей сразу же после точки ухода на подпрограмму, и в подпрограмме имеется только одна точка входа. Язык Форт поощряет структурный подход в программировании, так как любое слово Форта по существу является подпрограммой, у которой обычно имеется только один вход и один выход. Другие языки, например Паскаль или Модула-2, также обязывают к применению структурного программирования ввиду того, что вход в подпрограмму может быть произведен с помощью вызова именованной процедуры, возвращающей управление в точку, из которой она была вызвана. Это обстоятельство может показаться неудобным, потому что каждый переход на подпрограмму требует самостоятельной именованной процедуры. Форт в этом отношении обладает большей гибкостью. Поскольку все, что делается на языке Форт, исполняется подпрограммами, т.е. словами-операторами Форта, ли программы структурные по своей природе. (Исключением, как мы увидим в дальнейшем, является слово EXIT, которое позволяет немедленно прекратить исполнение слова.) Кроме того, поскольку в любой подпрограмме некоторые слова можно переделать, дать им более короткие имена, использование переходов становится не столь обременительным, как в Паскале.
Упражнения
1. Определите слово NEWABS (абсолютное значение), используя конструкцию IF...THEN. (Совет: воспользуйтесь словами 0< и NEGATE). 2. Определите слово, подобное / (т.е. деление нацело), которое должно выдавать сообщение об ошибке при попытке деления на нуль. 3. Определите слово ТYРЕ
Прекращение исполнения задания
Обычно исполнение слова Форта продолжается до тех пор, пока не встретится последнее слово в о определении.
Форт- программа исполняется до конца слова, которое вызвало его исполнение. В некоторых случаях нужно прекратить исполнение слова или программы досрочно. Завершение исполнения слова возвращает управление слову, которое запустило программу, в противоположность этому преждевременное прекращение программы возвращает управление терминалу.
Сначала мы познакомимся с тем, как можно выйти досрочно из исполнения слова с помощью оператора EXIT. Пусть вы определили : ADD + EXIT . ; : SUM 3 4 ADD 5 6 ADD ." Суммы" ;
Если вы запустите слово SUM, а потом посмотрите, что находится в стеке, то увидите, что, хотя обе пары чисел были введены и просуммированы, ни один из результатов не выведен на экран, по тому что операция ADD была окончена раньше, чем встретилось слово . (напечатать). А сообщение "Are the sums" (суммы) напечаталось. Дело в том, что, когда завершилось исполнение слова ADD, программа SUM еще не завершилась и управление было передано слову SUM. Теперь попробуйте тот же пример, заменив в нем EXIT на QUIT. Вы не увидите завершающего программу сообщения, но если заглянете в стек, то увидите там только первую сумму. Слово QUIT не только прекратило операцию ADD, оно завершило исполнение программы передав управление клавиатуре. Если слово QUIT сохраняет содержимое стека, то слово AUORT делает очистку стека.
Из данного примера не видно явно никакой пользы от новых слов EXIT, QUIT и ABORT. Зачем же нужны слова, которые только прекращают действие программы или слова до их завершения? Ответ состоит в том, что эти слова используются обычно в конструкциях IF...THEN, о чем мы вскоре расскажем. Назовем лишь два самостоятельных употребления этих слов. Во-первых, ни QUIT, ни ABORT не оставляют на экране сообщения "ok", если они стоят в конце программы. Вспомним пример программы WT, в которой числа вводились после того, как вы печатали WT и , и на экране получалось что-то вроде 223 WT ok 16 WT ok 59 WT ok
Можно сделать ввод более красивым, добавив в конце определения слова WT фразу SPACЕ QUIT.
Тогда после каждого WT на экране мы увидим 223 WT 16 WT 59 WT
Слово ABORT будет делать то же самое, но, кроме того, еще очищать стек. Использование слов ABORT и QUIT может быть полезным для подавления сообщения "ok".
Помимо этого, самостоятельное применение указанных слов может оказаться полезным при отладке программы. Предположим, что вам нужно проверить, не приводят ли к ошибке слова, которые вы ввели в конце какого-либо определения через двоеточие. Тогда можно сделать, чтобы эти слова игнорировались бы с помощью слова EXIT (разумеется, то же можно сделать, помещая эти слова в круглые скобки). Можно также применить QUIT, чтобы прервать исполнение программы каком-то месте и посмотреть содержимое стека или переменных. Слово ABORT в этом применении менее полезно, так как оно очищает стек, который нас, безусловно, интересует. Как уже было сказано, EXIT, QUIT и ABORT чаще всего используют в конструкции IF...THEN. Пусть нам нужно обеспечить возможность останова исполнения программы в определенном месте. Используйте для этого слово : ?ABORT " Нажмите S для прекращения программы" KEY 83 = IF " ok" CR ABORT THEN ;
Если ?ABORT вставить в какое-либо место в программе, пользователь будет иметь возможность остановить ее исполнение. A ."ok" CR введены в программу для того, чтобы напечатать сообщение "ok" и сделать возврат каретки, чего не делает слово ABORT. Очевидно вы можете заменить его словом QUIT, если хотите сохранить содержимое стека.
Слова ABORT и QUIT очень часто используются для обнаружения ошибок. Нам совершенно нежелательно разрешать деление на 0 (что может произойти из-за ошибки в вашей программе). Вот такое слово, которое можно вставить в программу непосредственно перед оператором деления / для того, чтобы выйти из вашей программы, если произойдет такая ошибка: : 0/? DUP 0= IF ." Ошибка деления на 0" DROP QUIT THEN ;
Заметим, что DROP можно убрать, если использовать ?DUP, и тогда получим : 0/? ?DUP 0= IF ." Ошибка деления на 0" QUIT THEN ;
Вам может потребоваться также досрочное завершение программы при достижении какого-либо заранее определенного условия. Представим себе, что есть программа, которая принимает данные от удаленного компьютера по телефонной линии, и нужно, чтобы она останавливалась, если будет получен управляющий код Ctri-C ( код ASCII равен 3). Можно после каждого входного символа вставить проверку с помощью слова : ?CTLC DUP 3 = IF ." Прекращение по дистанционному запросу " ABORT THEN ;
Говоря вообще, слова ABORT и QUIT наиболее полезны для слежения за условиями, при которых исполнение программы должно быть прекращено. Эти условия обычно связаны с некоторыми типами ошибок, например делением на 0, но могут быть, конечно, и другого рода. Можно напомнить, как мы говорили в гл. 1, что в Форте забота о проверке наличия ошибок целиком возлагается на программиста. И мы показали, как она может быть сделана в простых случаях. Слово EXIT имеет более тонкие применения, чем ABORT и QUIT. Оно прекращает исполнение слова и возвращает управление туда, откуда это слово было вызвано. Оно бесполезно вне конструкции IF...THEN, поскольку всегда возможно прекратить исполнение более естественным путем, завершая определение слова точкой с запятой. (Кстати, EXIT фактически является частью определения слова ;.) Предположим, например, что у вас есть слово, которое позволяет оператору ввести число, а затем запомнить сумму в переменной TOTAL. Вы хотите, чтобы ввод нуля игнорировался. Используя EXIT, можно дать такое определение : : GET# #IN ?DUP IF TOTAL +! ELSE EXIT THEN ; но по своему действию ELSE EXIT THEN ; ничем не отличается от THEN ;
Таким образом, EXIT следует применять ограниченно или не применять вовсе внутри одной единственной конструкции IF...THEN. Вспомним наше определение слова (WT) : : (WT) DUP 100 < IF TASK1 ELSE DUP 200 < IF TASK2 ELSE DUP 300 < IF TASKS THEN THEN THEN DROP ;
Теперь предположим, что слова TASK1, TASK2 и TASK3 прибавляют по единице к переменным 1COUNT, 2COUNT и 3COUNT каждый раз, когда вес попадает в соответствующий диапазон значений.
Но вы не хотите, чтобы слово TASK исполнялось, если в каком-либо диапазоне общий счет превысил 200. Иначе говоря, вы хотите прекратить исполнение, если количество случаев попадания в какой-либо диапазон превышает 200. Представляем слово, которое решает эту задачу : : (WT) DUP 100 < IP 1COUNT @ 199 > IF EXIT THEN TASK1 ELSE DUP 200 < IF 2COUNT @ 199 > IF EXIT THEN TASKS ELSE DUP 300 < IF 3COUNT @ 199 > IF EXIT THEN TASK3 THEN THEN THEN DROP ;
Этот пример показывает, что если использование слова EXIT в единственной конструкции IF...ELSE приводит к появлению лишних бесполезных слов, то во вложенных конструкциях IF...ELSE слово EXIT становится существенно необходимым.
Прежде чем закончить этот раздел, укажем на менее употребительное слово из Форт-83 и других версий - ABORT". Это слово ищет флаг в стеке и исполняется, если его значение истина, т.е. слово ABORT" содержит внутри себя конструкцию IF. Слово ABORT" выдает сообщение, которое следует после ", а затем исполняет операции присущие слову ABORT. Его назначение состоит в том, чтобы обнаруживать ошибки и давать об. этом сообщение. Вот, например, определение слова 0/? с использованием слова ABORT": : 0/? ?DUP 0= ABORT" Ошибка деления на 0 " ;
Приведенные ниже упражнения покажут некоторые более полезные на практике применения рассмотренных слов,
Упражнения
1. Определите слово ?END для оптимального выхода из программы, которая задавала бы следующие вопросы и выполняла бы соответствующие вашим ответам действия: Do you want to quit? (Y/N) Вы хотите закончить работу? и если введен ответ Y(да), то Do you want to save the stack? (Y/N) Вы хотите сохранить стек? (Да/Нет) 2. Если слово ?223 вводится непосредственно (не входит в определение через двоеточие), имеются ли какие-либо различия в его действии при следующих трех способах его определения : : ?223 223 = IF 1 COUNT +! ELSE QUIT THEN ; : ?223 223 = IF 1 COUNT +! ELSE EXIT THEN ; : ?223 223 = IF 1 COUNT +! THEN ; 3.
Как изменится действие слова из определения 2, если ?223 вызывается из другого определения через двоеточие? 4. Имеются ли различия, если в определении следующего слова используется DUP или ?DUP ? : : 0? DUP 0= IF ABORT" Число равно нулю " THEN ; 5. Дайте новое определение слова ?0, используя слово ABORT. 6. Определите слово =IF-ABORT, которое прекращает исполнение программы, если два верхних числа в стеке равны между собой. 7. Определите слово +RANGE-ABORT, которое прекращает работу программы и сообщает об ошибке, если сумма двух чисел в стеке будет превышать максимальное число одинарной длины без знака. 8. Определите слово *+RANGE-ABORT, которое прекращает работу программы, если либо сумма, либо произведение двух чисел в стеке превысят максимальное число одинарной длины без знака. 9. Определите слово STACK-TOO-BIG, которое выдавало бы сообщение об ошибке и прекращало исполнение программы, если в стеке больше 15 чисел. 10. Дайте новое определение следующих слов, не используя слова EXIT: а) : 1TASK 0= IF DOTHAT ELSE EXIT THEN : 6) ; 2TASK 0= IF EXIT ELSE DOTHAT THEN ; в) : 3TASK 0= IF DOTHAT ELSE EXIT THEN DOOTHER ; r) : 4TASK 0= IF EXIT ELSE DOTHAT THEN DOOTHER :
Это послужит убедительным доказательством, что слово EXIT совершенно не нужно в невложенных конструкциях IF...THEN.
Множественный выбор ветвления
До сих пор в этой главе мы рассматривали конструкции IF...THEN и IF...ELSE...THEN, которые позволяют делать переход на исполнение одной из двух ветвей программы в зависимости от того равно или не равно нулю число в стеке. Однако вам может потребоваться программа, которая может разветвляться по альтернативным путям в зависимости от того, какое из нескольких чисел находится в стеке. В гл. 6 мы показали один из вариантов решения этой задачи. Векторное исполнение программы - это одно из средств реализации ветвления по нескольким путям. Если компоненты вектора представляют собой адреса слов, то данное слово может быть исполнено, если взять eго адрес и применить к нему оператор EXECUTE.
Таким образом, слово, на исполнение которого уходит программа, зависит от одного из нескольких чисел, которое находится в стеке, когда выбирается конкретный адрес. В гл. 6 мы хранили адреса слов в массиве и использовали слова FIND или ', в зависимости от применяемой версии Форта. Давайте снова рассмотрим пример из гл. 6. Пусть слова 1TASK, 2TASK и 3TASK определены. Можно задать вектор CHOICE (выбор) следующим образом: CREATE CHOICE 6 ALLOT FIND 1TASK CHOICE ! FIND 2TASK CHOICE 2 + ! FIND 3TASK CHOICE 4 + !
Теперь если мы определим слово DOCHOICE (сделай_выбор) : DOCHO1CE 1- 2* CHOICE + @ EXECUTE ; то исполняемое ветвление будет зависеть от числа, которое находится в стеке, когда запускается программа DOCHOICE.
Эта конструкция носит название выбор по целому. Если в стеке находится 1, то выполняется 1TASK, если 2- то 2TASK и т.д. Такая конструкция выбора по целому, основанная на векторном исполнении, едва ли не самое простое, что может быть придумано в стандартном языке Форт. Разнообразным реализациям структуры выбора по целому был посвящен целый выпуск журнал "FORTH Dimensions" (1980- Т. 2, вып. 3). В некоторых коммерческих версиях Форта реализован сложные конструкции выбора по целому. Мы рассмотрим слова NCASE и ACASE из языка MMSFORTH. Проще всего для понимания слова NCASE привести простой пример. Пусть вы хотите выполнить 1TASK, 2TASK или 3TASK в зависимости от того, какое из чисел: 20, 40 или 60 находится в стеке, а если в нем встречается какое-либо другое число, нужно, чтобы исполнялось слово OTHER (другая). Вот как это можно сделать : NCASE 20 40 60 " 1TASK 2TASK 3TASK OTHERWISE OTHER CASEND Оператор NCASE снимает число из стека и просматривает, какому из следующего за ним ряда сел оно равно. Если обнаруживается совпадение, то исполняется соответствующее слово из пере численного списка, если совпадение не обнаружено, то исполняется слово или последовательность слов, находящихся между OTHERWISE (иначе) и CASEND (конец_выбора). Слово OTHERWISE и любые слова после него не являются обязательными, т.е.
если в данном случае вы не хотите, чтобы альтернативным путем было использование слова OTHER, то можете использовать программу NCASE 20 40 60 " 1TASK 2TASK 3TASK CASEND А теперь рассмотрим более сложный, но и более полезный пример. Пусть у вас есть программа, в которой используется слово KEY для ввода чисел. Вы хотите, чтобы она печатала все, что является алфавитно-цифровыми знаками, при вводе CtrI-C (код ASCII 3) очищала бы экран с помощью слова CLS, при вводе Ctrl-G (код ASCII 7) включала бы звуковой сигнал (с помощью слова ВЕЕР) и при Ctrl-P (код ASCII 16) вывод переключался бы на принтер (с помощью слова PRNT), Это может делать следующая программа : : GETKEY KEY DUP NCASE 3 7 16 " CLS BEEP PRNT OTHERWISE DUP 32 < IF DROP ELSE EMIT THEN CASEND ;
Надеемся, что вы поняли, как она работает.
Одно из самых полезных применений слова NCASE состоит в обслуживании запросов, связанных с нажатием определенных клавиш при ответе на предложения из меню. Слово NCASE будет работать правильно только с числами, не превышающими по величине максимального значения одного байта (от 0 до 255), и оно устроено так, что фактически игнорирует старший байт 16-разрядного числа. Это значит, что результат будет один и тот же, если NCASE обнаружит число 0A или AB0A (в шестнадцатеричном представлении), т.е. оно ищет совпадение с 0A в одном из следующих после него чисел. Аналогичным образом числа, следующие за NCASE, могут быть больше, чем представляются одним байтом, при этом старший байт числа будет игнорироваться. Таким образом, NCASE 55A3 2221 AC55 " . . . будет исполняться так же, как NCASE A3 21 55 " . . .
Следовательно, поскольку вы уверены, что старший байт числа никогда не может повлиять на выбор, определяемый словом NCASE, можно применять числа больше 255. Слово ACASE в MMSFORTH близко по назначению слову NCASE. Но оно реагирует не на число, а на символы ASCII. Приведем пример его синтаксиса: ACASE NKT" 1TASK 2TASK 3TASK OTHERWISE OTHER CASEND
Слово ACASE просматривает стек, и если оно обнаруживает букву N, то исполняется 1TASK, если букву К- то 2TASK, а при Т уходит на 3TASK.
Если нет совпадения ни с одной буквой, выполняется слово OTHER. Так же как и в NCASE, слово OTHERWISE, слово или слова, следующие за ним до слова CASEND, не являются обязательными. Заметьте, что не должно быть пробела между буквами или между буквами и кавычкой, если только вы не используете пробел для обозначения одного из вариантов выбора. Применение слова ACASE сходно с применением и NCASE, поэтому вместо разбора примеров предлагаем вам освоить его применение на упражнениях.
Упражнения
1. Создайте конструкцию выбора по целому, пользуясь векторным исполнением, которая выполняла бы арифметические действия над вторым и третьим числами в стеке по следующим правилам. Если в стеке находится 1, то слово ARITH выполняет сложение +; если 2, то вычитание -; если 3 или 4. то соответственно операции умножения * или деления /. Измените слово ARITH, пользуясь конструкцией IF так, что если число в стеке не попадает в диапазон чисел от 1 до 4, то в стек помещается 0. 2. Дайте другое определение слова ARITH, используя слово NCASE (включите выдачу нуля в стек, если используется неправильное число). 3. Определите слово NEWARITH, используя ACASE, так чтобы оно ожидало нажатия клавиши. Если будет нажата клавиша "+", числа нужно сложить; если нажата клавиша "-'', то вычесть; если нажата клавиша "*" или "/", то умножить или разделить соответственно. Если нажата неверная клавиша, то в стек должен выдаваться нуль. 4. Переделайте определение NEWARITH в NEWARITH1, используя конструкцию IF...ELSE...THEN. После того как вы это сделаете, вы оцените пользу слова ACASE. 5. Переделайте NEWARITH, как описано в упражнении 3, чтобы при нажатии неправильной клавиши печаталось сообщение " Неверный ввод " и исполнение прерывалось. Проделайте это, применяя слова ABORT и ABORT". 6. Можете ли вы придумать ситуацию, когда предпочтительнее использовать не NCASE и ACASE, а векторное исполнение?
Выводы
В некотором смысле возможность ветвления делает программу более эффективной.
Ветвление позволяет одной программой выполнять различные действия при различных условиях. Но в то же время применение ветвления заставляет программиста проявлять находчивость. Применение ветвления может привести к исключительной сложности программы. Таким образом, ветвление требует тщательного продумывания при составлении программы. Без ветвления исполнение программы представляется последовательностью операций, выстроенных в линию. При каждом запуске программы одно слово исполняется за другим в одной и той же последовательности. Действительно, если не пользоваться ветвлением и векторным исполнением, то, хотя при этом может получаться длинная и практически неудобная для чтения программа с большой избыточностью, для нее не потребуется определять какие-либо новые слова, кроме имен программ. Альтернативы, обеспечиваемые с помощью ветвления, требуют определения новых слов и, таким образом, создают дополнительные сложности при разработке программы. Ветвление более, чем что-либо другое, требует тщательной проработки, т.е. программист должен предусмотреть все возможные варианты выбора. Много времени и терпения может потребоваться, если, написав программу, программист обнаруживает неучтенные варианты разветвления, так как при этом часто требуется переписать основные куски программы. Хотя структура языка Форт способствует облегчению решения таких проблем, но чем больше вариантов (ветвлений) нужно включить в программу, тем тщательнее она должна быть продумана.
Необходимость такого планирования вносит противоречие в подходе к разработке программ между программированием сверху вниз, при котором обдумываются все детали, прежде чем перейти к составлению текста программы, и программированием снизу вверх, при котором программист сразу же начинает определять и проверять действие новых слов, разумно полагаясь, что его интуиция не приведет, к ошибке. Как мы увидим в гл. 13, хорошая методика программирования на Форте включает в себя оба подхода одновременно, привлекая к разработке программы интуицию, творчество и испытывая чувство удовлетворения от проделанной работы.
Организация циклов
Возможно, одним из наиболее полезных свойств компьютера является его способность многократно и с очень большой скоростью повторять операции. Циклы составляют часть программы, в которой повторяются одни и те же действия. Почти в каждой программе так или иначе применяются циклы. Как вы увидите в гл. 15, сам Форт работает как бы в бесконечном цикле.
В предыдущих главах мы пользовались наиболее распространенной формой цикла типа DO-LOOP. Однако мы еще не использовали все его возможности. Кроме того, существуют и другие типы циклов с еще большими возможностями, например циклы типа BEGIN...UNTIL, которые вызывают повторение программы только до тех пор, пока не будет удовлетворяться некоторое определенное условие. Сначала мы рассмотрим, как работают счетные циклы DO-LOOP и некоторые их применения.
Циклы типа DO-LOOP
Вы уже знакомы с простейшей формой счетного цикла: : 10LOOPS 10 0 DO 5 . LOOP , Слово 10LOOPS при исполнении напечатает на экране число 5 10 раз (т.е. от 0 до 9), а когда будет достигнут счет 10, циклическая программа прекратится. Между прочим, так же, как и конструкцию IF...THEN, счетный цикл можно использовать только внутри определений через двоеточие. По-видимому, самый простой счетный цикл, имеющий практическое применение, это : PAUSE 2790 0 DO LOOP ; ( Пауза) который приостанавливает программу, пока исполняется пустой цикл. Таким образом, слово PAUSE приостанавливает программу на время 2790 циклов или на 1/10 с на IBM РC в версии MMSFORTH; слово PAUSE может быть вставлено в другой цикл для получения еще больших задержек. (Если вы привыкли к языку Бейсик, то для вас будет неожиданностью, насколько быстрее выполняются циклы в Форте.) Очень важным моментом является то, что циклы могут вставляться в другие циклы. Если вы определите слова : 1LOOP 5 0 DO ." Внутренний цикл" LOOP ; и : 2LOOP 5 0 DO CR ."Внешний цикл" CR CR 1LOOP LOOP ; тогда каждый раз, когда будет исполняться цикл в программе 2LOOP, она напечатает в начале строки свое сообщение "Внешний цикл", сделает два перевода строки, а затем напечатает сообщение из программы 1LOOP 5 раз, пока будет исполняться эта программа; так будет повторяться 5 раз, следовательно, сообщение "Внутренний цикл" будет напечатано 25 раз.
Циклы, находящиеся внутри других циклов, называются вложенными; глубина вложенности может быть такой, какой она практически потребуется. Ради интереса попробуем, пользуясь циклами, сравнить скорость работы языка Форт по отношению к Бейсику. Цифры, приведенные здесь, относятся к IBM PC для МMSFORTH и версии Бейсика фирмы Microsoft. Первая программа цикла на Бейсике 10 DEFINT I,C 20 FOR I=1 ТО 10000 30 NEXT I выполняется 11.5 с (заметьте, что для корректности по отношению к Бейсику мы пользуемся целыми числами). Если мы напишем программу : TEST 10000 0 DO LOOP : то она будет исполняться слишком быстро, поэтому для определения времени ее работы мы будем вынуждены включить ее во вложенный цикл: : TESTA 10 0 DO TEST LOOP ; который исполняется в течение 3.6 с, поэтому слово TEST, которое повторяет пустой цикл 10000 раз, исполняется за 0.36 с. Таким образом, счетный цикл на Форте работает в 32 раза быстрее, чем на Бейсике. Рассмотрим теперь некоторые арифметические операции. Если мы вставим 25 C=10 в первую программу, то ее исполнение удлинится до 17.5 с. Программа на Форте, эквивалентная ей в первом приближении: : TEST 10000 0 DO 10 DROP LOOP ; исполняется за 0.71 с. Теперь в строке 25 на Бейсике поместим 25 С=10+10 тогда выполнение программы займет 23.1 с. Следовательно, исполнение 10000 сложений целых чисел занимает 23.1 - 17.5, или 5.6 с. На Форте для исполнения программы : TEST 10000 О DO 10 10 + DROP LOOP : потребуется 1.13 с, т.е. 1.13 - 0.71 " 0.42 с на 10000 операций сложения. Следовательно, Форт быстрее Бейсика по операции сложения в 13 раз. Аналогичное испытание показывает, что для операции умножения на Бейсике требуется 5.7 с, а на Форте - 0.7 с, т.е. в 8 раз меньше. Другими словами, для операций с целыми числами Форт работает примерно в 8 - 30 раз быстрее, чем Бейсик. Еще более интересно сравнить операции над числами с плавающей запятой для процессора Intel 8087. При замене строки 25 на 25 D=10E10 программа работает 21.4 с; если а строке 25 будет 25 D=10Е10+10Е10.
то программа исполнится уже за 28.2 с, т.е. время исполнения 10000 сложений чисел с плавающей запятой составляет 28.2 - 21.4 = 6.8 с. Программа на Форте : TEST 10000 0 DO % 10Е10 FDROP LOOP ; потребует 0.79 с, а программа : TEST 10000 0 DO % 1Е10 % 1Е10 F+ FDROP LOOP ; исполняется за 1.23 с, т.е. 10000 сложений чисел с плавающей запятой происходят за 1.23 - 0.79 = 0.44 с, в 15.5 раза быстрее, чем на Бейсике, и менее чем на 5% медленнее операции сложения целых чисел на Форте. Умножение на Бейсике занимает 16.8 с, на Форте - 0.46 с, т.е. в 16 раз быстрее и фактически на 34% быстрее, чем умножение целых чисел на Форте (мы уже говорили в гл. 4, что некоторые операции с плавающей запятой на процессоре 8087 выполняются быстрее, чем с целыми числами). Извлечение квадратного корня на Бейсике производится за 14.5 с, а на Форт - за 0.87 с, т.е. в 39 раз быстрее. На самом деле эти сравнения не очень корректны по отношению i Форту, поскольку последний совместно с процессором 8087 имеет в 100 раз больший диапазон представления чисел и более чем в 4 раза лучшую точность по сравнению с числами одинарной точности в языке Бейсик. Если на Бейсике производится умножение чисел двойной точности, то фактор преимущества в скорости для Форта равен 48, и это, несмотря на больший диапазон и повышенную точность. Умножение на современной быстродействующей универсальной ЭВМ типа CDC Cyber выполняется в 20 раз быстрее, чем на Форте с микропроцессором 8087. Однако затратив менее 10i долл., можно увеличить скорость ваших вычислений больше, чем при переходе на универсальна ЭВМ стоимостью около миллиона долларов.
Возвращаясь из нашего экскурса, мы должны рассмотреть индекс счетного цикла. Индекс представляет собой текущее значение счетчика, начинающего счет с нижнего значения и при каждое повторении цикла изменяющегося на заданную величину приращения. (Индекс помещается на вершину стека словом I, наименование которого происходит от Index, или, если вам больше нравится, указывающего, что выполняется I-й цикл.) Таким образом,программа : SHOW-INDEX 10 5 DO I .
LOOP ; напечатает на экране 5 6 7 8 9
Заметьте, что числа следуют только до 9. Так происходит потому, что индекс цикла инкрементируется словом LOOP и, когда он достигает крайнего значения, в данном случае 10, происходит вы ход из цикла без возврата к слову DO еще раз. Очевидно, что если повторение исполнения цикла производится с помощью DO, то в стеке должны быть по крайней мере два числа. Верхнее число служит начальным значением индекса цикла, с которым происходит вход в цикл. Второе число на стеке - это предельное значение параметра цикла, по достижении которого с помощью инкрементирования индекса происходит выход из цикла. Если мы определили слово как ; SHOW-INDEX 7 2 DO I . LOOP ; то оно выдаст на экране 2 3 4 5 6 Обе последние программы повторяются по 5 раз, но дают разные результаты вследствие различных начальных и конечных значений индекса цикла. Задание диапазона изменения индекса - это важный момент, потому что значение индекса часто используется для вычислений внутри цикла. Имеются некоторые отличия в способе завершения счетного цикла в стандартах Форт-79 и Форт-83. Рассмотрим слово : 1LOOPTEST 5 5 DO I . LOOP ; в Форт-79 слово 1LOOPTEST попросту напечатает число 5 и программа остановится, напротив, в Форт-83 вы увидите 5 6 7 ... 32766 32767 -32768 -32767 ... -1 0 1 2 3 4 т.е. будут напечатаны 65535 чисел. Теперь рассмотрим пример другой программы: : 2LOOPTEST -32766 32766 DO I U. LOOP ; которая в Форт-79 также напечатает только одно число 32766 и остановится, в то время как в Форт-83 она выдаст 32766 32767 32768 32769
Согласитесь, что эти различия приводят в замешательство.
Что же происходит? Что касается Форт-79, то объяснение простое. Слово LOOP добавляет 1 к индексу и после этого проверяет, не нужно ли выйти из цикла, если индекс достиг или превысил величину предельного значения параметра цикла, рассматриваемого как число со знаком. Поэтому обе тестовые программы в Форт-79 печатают только одно число - начальное значение индекса и прекращают работу.
Что касается Форт-83, то его реакция более хитроумная. Он принимает решениe выйти из цикла не на основании факта равенства или превышения индексом предельного значения параметра цикла, а, скорее, на основании факта перехода от состояния, когда предел не был достигнут, к состоянию, когда индекс достигает значение предела. В первом случае 1LOOPTEST, инкрементируя индекс, изменяет его с 5 на 6, поэтому необходимое изменение состояния не происходит, следовательно, цикл должен продолжаться со значениями индекса 7, 8 и т.д. После достижения индексом значения 32767 добавление единицы приводит к изменению знака числа, так как число без знака 32768 представляет собой число со знаком -32768, но опять же изменение состояния не происходит. Каждый раз при исполнении цикла индекс увеличивается на 1 до тех пор, пока он не достигнет значения 4, а затем изменится на 5, тут и происходит выход из цикла.
Действие второй тестовой программы в Форт-83 можно проанализировать аналогичным образом, Добавление единицы к 32766 приводит к превышению предела, но переход от состояния индекса ниже предела к состоянию, когда он равен пределу или превышает его, не происходит, поэтому цикл продолжается до тех пор, пока не произойдет переход от -32767 к -32766 или, если рассматривать числа без знака, от 32769 к 32770. Это как раз необходимый переход состояния, поэтому цикл прекращается. Важно помнить, что если в Форт-83, начальное значение индекса равно или превышает предельное значение, то цикл будет продолжаться до тех пор, пока индекс не приблизится к пределу снизу. Форт-83 работает именно таким образом, в особенности если используются числа без знака; это обеспечивает возможность обращения с помощью индексации циклов ко всем 64К ячейкам памяти.
Следует также помнить о том, что Форт-79 выходит из цикла, принимая во внимание значение числа со знаком, поэтому программа : 3LOOPTEST 3 -3 DO I . I U. LOOP ; выдает -3 65533 -2 65534 -1 65535 0 0 1 1 2 2 Очевидно при первом инкрементировании величина индекса без знака превышает значение предела, но слово LOOP следит за условием с учетом знака.
Форт- 83 даст такой же результат, но не из-за того, что рассматривается число со знаком или без знака, а потому что происходит переход значения индекса от величины ниже предела к величине, равной пределу. Эти тонкости прекращения цикла могут быть особенно обескураживающими в случае, когда начальное значение индекса равно пределу, в частности в языках Бейсик, Фортран и других. Например, программа на Бейсике 10 FOR I = 5 ТO 5 20 PRINT I 30 NEXT I не будет ничего делать, потому что индекс цикла равен пределу и тело цикла игнорируется (операторы PRINT I и NEXT I никогда не исполняются). С другой стороны, мы видели, что в программе 1LOOPTEST, которая также начинается со значения индекса, равного пределу, слова внутри цикла исполняются, печатая 5, хотя зачастую это нежелательно. Хуже того, программа 1LOOPTEST в Форт-83 исполняется 65535 раз, пока не произойдет переход от 4 к 5. Очевидно, что вы этого вовсе не хотели. Можно избежать неудобств с помощью конструкции IF-THEN. Если не исключена возможность, что в цикле типа DO...LOOP начальное значение индекса больше или равно пределу, то вы можете защитить программу, например, таким образом: ... OVER OVER >
IF DO ( тело цикла) LOOP THEN ...
Если начальное значение индекса больше предела, цикл не будет исполняться. Циклы в Форте не так изящны, как в Бейсике, но, как вы уже видели, они работают существенно быстрее. Следует иметь в виду еще одну деталь: слово I может встречаться только в определениях слов, которые включают в себя циклы DO-LOOP. Поэтому не будут работать следующие определения: : IN-LOOP I . ; : TRY-LOOP 5 0 DO IN-LOOP LOOP ; Слово IN-LOOP выдаст вам какую-нибудь бессмыслицу. Во многих версиях Форта предусмотрено необязательное слово I', которое должно работать следующим образом. Если мы переопределим слова предыдущего примера: : IN-LOOP I' . ; : TRY-LOOP 5 0 DO IN-LOOP LOOP : то слово IN-LOOP напечатает значение индекса цикла, т, е. что мы хотели бы увидеть в первом примере. Таким образом, слово I' возвращает значение индекса цикла, если оно используется внутри слова, которое вызывается из цикла.
В большинстве реализации Форта слово I' исполняется только в том случае, если оно используется в слове, вызываемом непосредственно из цикла, поэтому нельзя написать программу : 2IN-LOOP I' . ; : 1IN-LOOP 2IN-LOOP ; : TRY-LOOP 5 0 DO 1IN-LOOP ; И поэтому TRY-LOOP выдает что-нибудь несуразное. Причину этого вы поймете в следующей главе. В большинстве версий слово I' вызывается непосредственно внутри цикла, где оно выдает предельное значение индекса цикла. Так, программа : SHOWLIMIT 5 0 DO I . I' . CR LOOP ; напечатает 0 5 1 5 . . . 4 5
Для получения значений индексов вложенных циклов используются еще два слова. Если слово I выдает значение индекса внутреннего цикла, то J возвращает значение индекса внешнего по отношению к нему цикла, а К - индекс следующего внешнего цикла. (Слово J входит в стандарты Форт-79 и Форт-83, а слово К предусмотрено только в некоторых версиях.) В качестве примера рассмотрим программу : TEST 5 0 DO 5 0 DO 5 0 DO I . J . К . CR LOOP LOOP LOOP ; Запустив слово TEST, вы получите на экране 0 0 0 1 0 0 2 0 0 . . . 4 0 0 0 1 0 1 1 0 . . . 0 0 1 1 0 1 . . . 4 4 4
Возможность пользоваться вложенными циклами и их индексами обеспечивает широкий диапазон применений, одним из которых является обращение с двумерными массивами. Пусть у вас имеется матрица MATRIX размерности 5х5 в версии MMSFORTH. Программа .MATRIX (печать_матрицы), которую мы приводим, напечатает эту матрицу по строкам с аккуратно оформленными столбцами: : .MATRIX 5 0 DO 5 0 DO I J MATRIX @ 6 .R LOOP CR LOOP ;
Внешний цикл обеспечивает переход от строки к строке, в то время как внутренний цикл перебирает элементы матрицы в строке, извлекает их содержимое и печатает значения элементов матрицы, производя выравнивание их вправо в поле шириной шесть позиций. С помощью циклов можно сделать еще многое, но, прежде чем продвигаться дальше, рассмотрим еще один пример, а потом перейдем к упражнениям. Циклы очень важны в программировании, но мы можем только слегка затронуть рассмотрение их возможностей.
Другие примеры мы приведем в следующих главах. Давайте попробуем написать программу, которая возводит в степень второе число в стеке, при этом показатель степени указывается числом на вершине стека. Таким образом, 2 4 ** должно выдать число 16. Вот эта программа: : ** ( n1 n2 - n1^n2 ) OVER SWAP 1- 0 DO OVER * LOOP SWAP DROP ; Рассмотрим возведение числа 2 в 4-ю степень. Если числа 2 и 4 находятся в стеке, то последовательность операций OVER SWAP 1- возвратит в стек 223. Затем 0 DO запустит цикл, который будет исполняться 3 раза, а в стеке останется число 2. Затем после каждого исполнения цикла в стеке будет наблюдаться следующее: 2 4 2 8 2 16 Предложение SWAP DROP производит уничтожение в стеке числа 2, которое остается после завершения цикла. Данный пример показывает три общих проблемы применения циклов. Во-первых, часто необходимо переставить данные в стеке, прежде чем войти в цикл. Во-вторых, поскольку цикл многократно использует число, обычно нужно применять на вершине стека операции DUP, OVER и др. И, в-третьих, по завершении цикла в стеке может что-то оставаться, поэтому в конце программы может потребоваться оператор DROP. Но не будем торопиться, посмотрим, что произойдет, если мы захотим возвести число в первую степень? Программа ** выполнит один цикл и выдаст в результате квадрат числа, т.е. введете ли вы 1 или 2, получится один и тот же ответ. Это как раз пример неудобства проверки индекса в Форте на предельное значение, о котором мы уже говорили выше. Но выход есть. Сделаем определение ** таким: : ** ( n1 n2 - n1^n2 ) OVER SWAP 1- 0 OVER OVER <>
IF DO OVER * LOOP ELSE DROP DROP THEN SWAP DROP ; Предложение OVER OVER <> IF разрешает запустить цикл только в том случае, если показатель степени больше 1. В противном случае значение показателя степени и его копия снимаются со стека двумя словами DROP, заключенными между словами ELSE и THEN. Этот пример приведен для того, чтобы показать как можно вообще избежать запуска цикла, если оба значения параметра цикла равны между собой.
Еще более эффективной будет следующее определение слова **: : ** ( n1 n2 -- n1^n2) DUP <>
IF OVER SWAP 1- 0 DO OVER * LOOP SWAP DROP THEN ; Предоставляем вам самостоятельно разобраться, как работает это определение и почему оно лучше, Но и это не все. Осталась еще одна проблема. Любое число, возведенное в нулевую степень, принимается равным 1. Это также должно быть учтено в программе **. Поэтому окончательное определение ** будет таким: : ** ( n1 n2 - n1^n2 ) ?DUP 0= IF DROP 1 ELSE DUP 1 = IF DROP ELSE OVER SWAP 1- 0 DO OVER * LOOP SWAP DROP THEN THEN ; В гл. 16 вы познакомитесь с определением слова ** на языке ассемблера, которое работает значительно быстрее. Следующие упражнения позволят вам еще больше узнать о циклах DO-LOOP.
Упражнения
1. Опишите слово ASCIITAB. предназначенное для того, чтобы напечатать коды ASCII и их алфавитно-цифровые эквиваленты. Перед тем, как использовать это слово, в стеке должны находиться верхнее и нижнее значения кодов ASCII. Тогда 65 68 ASCIITAB должно печатать 65 А 66 В 67 С 68 D 2. Модифицируйте программу ASCIITAB и назовите ее ASCIICHAR. так чтобы она печатала коды и соответствующие и' символы в следующей форме: 33 34 35 36 37 38 39 40 41 42 ! " # $ % & ' - . / ... 113 114 115 116 117 118 119 120 121 122 q r s t u v w x y z по 10 кодов в строке, показывая все печатные символы с кодами от 32 до 122. 3. Опишите слово ZERDIAG, которое обнуляет диагональ матрицы, т.е. результатом ее работы должно быть что-то вроде 0 3 6 7 8 2 0 9 4 3 5 5 0 9 8 1 2 8 0 9 6 1 6 9 0 4. Опишите слово SUMVOL, которое должно вычислять сумму чисел в каждом столбце матрицы размерности 5х5 из чисел длиной в 1 байт, если задан адрес матрицы. Пять значений суммы должны быть выданы в стек. 5. Напишите слово Х^5, которое должно возводить число, находящееся на вершине стека, в 5-ю степень. 6. Полезно было бы иметь эквивалент слова ** для чисел двойной длины D**, которое выдает в результате число двойное длины, если входное число имеет одинарную длину.
Опишите слово D**. 7. Напишите слово DUMP1, которое должно, начиная с известного начального адреса в памяти, представлять на экране следующие 160 байтов в шестнадцатеричной системе в виде таблицы из 10 строк по 16 чисел в каждой. 8. Измените слово DUMP, назвав его DUMP2, так чтобы в начале каждой строки слева был показан адрес первого байта в данной строке. Используйте представление чисел с выравниванием вправо, так чтобы все числа были выровнены в столбцах. 9. Измените слово DUMP, назвав его DUMP3, таким образом, чтобы оно печатало 10 строк (всего 80 байт), в каждой из которых сверху печатались бы числа, а под ними - соответствующие им символы ASCII. Так как символы с кодами меньше 32 нельзя напечатать, замените их точками. 10.Создайте переменную SPCS (от spaces - пробелы). Опишите слово для подсчета пробелов в текстовом массиве размером 1024 байта, начинающемся с адреса, указанного числом в стеке; результат должен быть записан в SPCS. 11. Создайте массив ALPHA, число элементов которого равно числу букв алфавита. Напишите слово ALPHACOUNT (число_букв), которое должно анализировать указанное количество алфавитно-цифровых байтов и записывать число встретившихся в массиве букв в элементах массива ALPHA. Тогда фраза 32201 500 ALPHACOUNT будет просматривать массив из 500 байтов, начинающийся с адреса 322201. и считать число различных байтов, причем число встретившихся литер "А" будет запоминаться в нулевом элементе массива, число литер "В" - в первом и т.д. Желательно превратить все литеры нижнего регистра в литеры верхнего регистра, чтобы не учитывать коды меньше 65 и больше 90. 12. Опишите слово ALPHAPLOT для представления в виде гистограммы частоты появления литер в массиве ALPHA, при этом каждый столбик постройте из соответствующих букв. 13. Не заглядывая в ранее приведенные определения, опишите слово .S.
Еще о циклах типа DO-LOOP
В некоторых случаях нужно, чтобы шаг приращения индекса в цикле был равен не единице, а большему числу. А иногда необходимо, чтобы цикл проходился в обратном направлении.
В таких случаях нужно использовать слово +LOOP. Испробуйте следующую программу: : SKIPLOOP 10 О DO I . 2 +LOOP ; тогда вы увидите на экране 0 2 4 6 8 Индекс в цикле инкрементируется на 2, т.е. на то число, которое +LOOP находит в стеке. Теперь определим слово : COUNTDOWN 0 10 DO I -2 +LOOP ; (обратный счет) Использование слова COUNTDOWN приводит к выводу на экран чисел 10 8 6 4 2 0 Другими словами, индекс получает приращение -2, пробегая от 10 до 0. Обратите внимание, что нуль также был напечатан. Если +LOOP декрементирует индекс (обратный цикл), то цикл заканчивается только тогда, когда индекс оказывается меньше предела в Форт-79 или когда он совершает переход от значения, равного пределу, к значению на единицу меньше предела в Форт-83, в то время как при возрастающем индексе (прямой цикл) выход из цикла происходит, когда индекс становится больше или равен пределу. Заметьте также, что различия в критериях для выхода из цикла +LOOP в Форт-79 и Форт-83 такие же, как и для выхода из цикла LOOP. В последующих упражнениях мы рассмотрим применение циклов с +LOOP.
Бывают случаи, когда необходимо выйти из цикла раньше, чем индекс достигнет значение предела. Чтобы сделать это, применяется слово LEAVE. Попробуйте этот практически бесполезный пример: : LEAVE-LOOP 10 0 DO I . I 5 = IF LEAVE THEN ."Loop" LOOP ; На Форт-79 исполнение LEAVE-LOOP (выйти_из_цикла) дает 0 Loop 1 Loop 2 Loop 3 Loop 4 Loop 5 Loop Обратите внимание, что слово "Loop" (цикл) было напечатано после номера 5, хотя слово LEAVE встретилось раньше и уже было исполнено. Так происходит потому, что в Форт-79 LEAVE просто устанавливает значение предела равным значению индекса, исполняет все, что осталось в цикле, а затем выходит из цикла в том месте, где LOOP проверяет величину индекса. На Форт-83 слово LEAVE-LOOP при исполнении выдает на экране 0 loop 1 loop 2 Loop 3 loop 4 loop 5
Слово "Loop" после номера цикла 5 отсутствует. Это объясняется тем, что на Форт-83, когда встречается слово LEAVE, оно, не обращая внимания на индекс и предел, делает скачок на слово, которое следует сразу же после LOOP.
Различия такого рода незначительные и достаточно тонкие, но, если их не осознавать, они могут вызвать недоумение, когда с ними сталкиваешься. (Мелкое замечание: I. I 5 = в программе LEAVE-LOOP можно записать иначе: I DUP. 5 =, но предыдущая запись предпочтительнее, поскольку она более понятна, а по скорости работы обе конструкции одинаковы.)
Приведем определение более полезного слова GET$. Это слово принимает счетную строку литер с клавиатуры, помещая длину строки в PAD, за которой следуют литеры строки: : GET$ 0 PAD С! 256 0 DO KEY DUP 13 = IF DROP LEAVE ELSE DUP EMIT PAD C@ 1+ PAD C! I PAD + 1+ C! THEN LOOP ;
По адресу PAD вначале записывается нуль, затем запускается цикл, ожидающий ввод с клавиатуры с помощью KEY до 256 символов. Если KEY вводит код 13 (код возврата каретки, одновременно запуска программы), то слово LEAVE выводит программу из цикла или подготавливает к выходу в зависимости от того, в какой версии, Форт-79 или Форт-83, применяется программа. В противном случае следующие после ELSE слова посылают на экран принятый байт и запоминают его в соответствующем месте после адреса PAD, а также увеличивают значение счетчика введенных символов на 1. Можно проверить работу этого слова, если ввести PAD COUNT TYPE. (Заметим, что лучшее определение слова GET$ будет дано в гл. 9.) Другие применения циклов с +LOOP и LEAVE лучше всего рассмотреть в упражнениях.
Упражнения
1. Опишите слово .ARR для печати содержимого одномерного массива чисел одинарной длины, начальный адрес которой и его длина находятся в стеке. 2. Опишите аналогичное слово .SQARR для печати содержимого квадратной матрицы чисел одинарной длины (но не байтов, как в предыдущем примере) в виде строк и столбцов, если адрес и число строк (столбцов) находятся в стеке. 3. Повторите упражнения 1 и 2, но для чисел двойной длины. 4. Преобразование температуры по Фаренгейту в значение, выраженное в градусах Цельсия, производится по формуле С = 5(F - 32)/9 Напишите слово, которое создает таблицу из двух колонок величин, выраженных в градусах Фаренгейта, преобразованных в величины, выраженные в градусах Цельсия через 10 градусов от 0 до 200 градусов Фаренгейта.
Меньшие значения должны стоять в таблице сверху. Переделайте это слово для печати в четыре колонки. 5. Переделайте определение слова из упражнения 4, чтобы большие значения находились сверху таблицы. 6. Напишите слово FINDCHAR (найти_символ), которое будет просматривать последовательность из 1024 литер и выдавать адрес, по которому впервые встречается литера, код ASCII которой помещается на вершине стека. Начальный адрес должен быть вторым в стеке. 7. Опишите слово $=, которое будет просматривать две счетные символьные строки, адреса которых находятся в стеке, и возвращать значение истина, если обе строки идентичны, и ложь в противном случае. Сначала должны быть проверены длины строк, и, если они равны, строки нужно сравнивать посимвольно, применяя цикл, который должен прерываться, если обнаружено несовпадение, чтобы сократить время. 8. Опишите слово $FIND для поиска строки в символьном массиве из 1024 элементов, которое выдает адрес, если заданная строка обнаружена и 0 в противном случае. Адрес начала блока должен быть вторым в стеке, адрес заданной счетной строки поиска должен быть на вершине стека. Используйте цикл для поэлементного сравнения строки с содержимым массива. и, конечно, если какой-либо символ не совпадает, нужно перейти к следующей строке, начинающейся на следующем символе. Считайте, что первый байт в массиве содержит длину строки.
Стек возвратов
Мы уже раньше упоминали о том, что в Форте имеются фактически два стека. Одним из них мы пользовались. Это стек данных, иначе, стек параметров или стек пользователя. Стек возвратов используется главным образом самой Форт-системой. Его основное назначение состоит в слежении за адресом возврата при исполнении слова. Кроме того, в большинстве реализации языка в нем также сохраняются значения пределов для циклов и текущего значения индекса (хотя стандарты этого не требуют). Его также можно использовать для временного хранения чисел. При этом важно помнить, что, если стеком возврата пользуется программист, его нужно восстанавливать в исходное состояние до окончания определения слова или до выхода из цикла.
Для операций в стеке возвратов предусмотрены три стандартных слова: >R (в_стек), R> (из_стека) и R@ (выдать_содержимое). Слово >R берет слово из стека параметров и помещает его в стек возвратов, слово R> забирает число из стека возвратов и помещает его в сек параметров, а слово R@ выдает копию содержимого стека возвратов в стек пользователя, не изменяя содержимое стека возвратов. Приведенные слова используются в стеке возвратов в основном для временного хранения чисел, которые во время исполнения слова могут потребоваться несколько раз. Это может сильно упростить некоторые манипуляции в стеке и зачастую позволяет обойтись без переменных для хранения чисел. Приведем пример. Пусть вы хотите извлечь однобайтовые элементы 5, 7, 11 и 12 из массива с именем DATA в стек пользователя. Вот как это можно определить: : @ELEMENTS ' DATA 5 + С@ ' DATA 7 + С@ ' DATA 11 + С@ ' DATA 12 + С@ ; Однако многократное повторение ' DATA снижает быстродействие. Быстрее будет работать следующая программа: : @ELEMENTS ' DATA >R R@ 5 + С@ R@ 7 + С@ R@ 11 + C@ R> 12 + С@ ;
Обратите внимание, что в последний раз адрес массива DATA берется из стека возвратов с помощью R>, поэтому в нем не остается ничего лишнего, когда мы завершаем исполнение слова по ;. Использование стека возвратов в счетных циклах также очевидно. При исполнении операторов, находящихся внутри цикла, верхний предел цикла лежит вторым в стеке возвратов, а текущее значение параметра цикла, т.е. индекс, - на вершине стека возвратов. Попробуйте исполнить следующее слово: : TRY-IT 5 0 DO R@ . R> R@ . >R SPACE LOOP ; Вы увидите 0 5 1 5 2 5 3 5 4 5 Выражение R@. печатает число с вершины стека возвратов, т.е. индекс, поэтому R@ . можно использовать вместо I . . Затем R> снимает число с вершины стека возвратов, a R@. извлекает число, которое было вторым в стеке возвратов, и печатает его. Это число 5, т.е. верхний предел цикла: Затем >R снова помещает индекс в стек возврата. Попробуйте теперь действие следующих слов: : I-BAD R@ ; (неверное_I) и : TRY-THIS 5 0 DO I-BAD .
LOOP :
Если вы исполните слово TRY-THIS, то увидите какую- то чушь. Но мы только что сказали, что R@ - то же самое, что I, где же ошибка? Вспомните, мы говорили раньше, что стек возвратов используется для экономии внешних ячеек памяти, когда слово вызывается из определения через двоеточие. При обращении к I-BAD адрес возврата помещается в стек возвратов, его вы и видите. Другими словами, когда вызывается I-BAD, адрес возврата помещается на вершине стека возвратов - выше, чем текущее значение параметра цикла (индекса). Поэтому правильное определение I должно быть таким: : I R> R@ SWAP >R : Слово I должно сначала снять число с вершины стека возвратов, потом извлечь содержимое индекса, а затем заменить число на вершине стека возвратов.
Теперь мы сможем дать определение I'. Просмотрите снова слова TRY-LOOP и IN-LOOP несколькими страницами назад и вспомните, что I' вызывается внутри слова, включенного в цикл, чтобы извлечь индекс. IN-LOOP помещает в стек возвратов число (адрес) сверху индекса, а I' еще выше помещает другой адрес. Поэтому, чтобы извлечь индекс, нужно вначале снять из стека возвратов два числа. Правильное определение I' будет таким: : I' R> R> R@ SWAP >R SWAP >R ; Вам должно быть понятно, как работает это определение по аналогии с I. Понимаете ли вы, почему, если использовать его внутри цикла, оно возвращает значение верхнего предела параметра цикла?
Вот так (в упрощенном виде) работает счетный цикл в Форт-79. Слово DO переносит первые два 'пела из стека параметров в стек возвратов. Когда встречается слово LOOP, оно сначала прибавляет I к числу, находящемуся на вершине стека возвратов, а затем сравнивает результат со вторым числом в стеке возвратов, т.е. с пределом на равенство либо превышение. Если это произошло, то управление передается слову, которое стоит после LOOP. Если же индекс не равен пределу и не превосходит его, то управление возвращается слову, которое стоит после DO. Цикл типа +LOOP работает в Форт-79 аналогично, но вместо добавления 1 к числу, находящемуся на вершине стека возвратов, добавляется число, находящееся на вершине стека параметров.
Оперируя стеком возвратов, мы можем определить другие полезные слова для работы с циклами, но лучше всего рассмотреть их в упражнениях.
Упражнения
1. Опишите слово 4PICK, пользуясь стеком возвратов для временного хранения чисел, которое должно печатать и удалять из стека параметров четвертое сверху число. Почему в определении нельзя использовать цикл DO-LOOP? 2. Используя стек возвратов, дайте возможное (но не эффективное) определение DUP с именем DUP1. 3. Что находится в стеке возвратов на первых четырех местах сверху вложенного цикла? Исходя из этого, дайте определение слова J (под именем J1). Не забудьте, что само слово J выдает число в стек. 4. Напишите определение слова К. 5. Опишите слово J', которое выполняло бы функции слова I', т. е. слово, в котором используется J', помещенное во вложенный цикл глубины 2, должно давать значение индекса внешнего цикла. 6. Дайте определение слова LEAVE, назвав его NEWLEAVE, которое будет правильно работать на Форт-79 (вы должны установить значение индекса цикла равным значению его предела). 7. Как можно изменить предел цикла из тела цикла? 8. Вы хотите, чтобы в цикле, завершающемся по LOOP. по достижении индексом значения 10 он изменялся на 15. Какие слова нужно включить в дело цикла, чтобы сделать это? 9. Опишите слово, которое заставляет индекс цикла изменяться на n, где n - число, которое находится в стеке в момент исполнения слова. 10. Что будет делать следующее слово: : DUMB 5 1 DO R> DUP . 1- >R LOOP ; (Оно очень коварно; будьте готовы к перезагрузке вашего компьютера.)
Для тех, кто знаком с дифференциальным исчислением
Упражнения
1. Поэкспериментируйте с разными значениями N в приведенном примере. Программа не работает, если N меньше 200. Почему? Если вы пользуетесь версией Форт, позволяющей работать с плавающей запятой, перепишите программу для чисел с плавающей запятой; она будет работать с меньшими начальными значениями. 2. Напишите программу, которая определяет, через какое время популяция в приведенном примере удваивается.
Используйте LEAVE. 3. Прирост популяции при ограничении на ресурсы описывается уравнением dN К - N ---- = rN ----- dt К где r - максимально возможная скорость роста и К - максимальная популяция, которая может сохраняться при ограниченных ресурсах. Напишите программу, моделирующую процесс роста популяции при начальном значении 1000, г, равном 1/200, и К, равном 3000. 4. А эта задача для честолюбивых. Просмотрите программы для построения гистограмм из гл. 5 и напишите программу для представления роста популяции к упражнению 3.
Циклы с неопределенным числом повторений
Циклы типа DO-LOOP имеют ограниченные пределы, задаваемые из стека. Однако часто нужно иметь циклическую программу, выход из которой зависит от выполнения некоторого условия, которое ею проверяется. Слово LEAVE позволяет это делать, однако существует более удобная форма цикла - цикл с неопределенным числом повторений, который включает слова BEGIN...UNTIL или BEGIN...WHILE..REPEAT. Как и цикл DO-LOOP, неопределенные циклы могут быть исполнены только в определениях через двоеточие.
Рассмотрим вначале цикл BEGIN-UNTIL. Слово BEGIN отмечает начало цикла. Если в стеке находится 0 (флаг ложь), то, когда его встречает слово UNTIL, исполнение повторяется со слова, которое стоит после BEGIN. Иначе говоря, цикл BEGIN-UNTIL повторяется до тех пор, пока в стеке перед словом UNTIL не встретится число, не равное нулю. Для примера определим слово : TIL-10 BEGIN DUP . 1+ DUP 9 > UNTIL DROP ; Исполняя 5 TIL-10, мы увидим 5 6 7 8 9 То есть программа печатала число, находящееся на вершине стека, увеличивала его на единицу до тех пор, пока оно не стало равным 10, и тогда она вышла из цикла.
Приведем более полезный пример. Предположим, что вы сделали в 1989 г. вклад 500 долл. из расчета 10% годовых. Когда на вашем счету будет 1000 долл. или более и сколько будет каждый год? Можно рассчитать это с помощью программы VARIABLE TOTAL 500 TOTAL ! : ?YEAR BEGIN TOTAL @ 10 / TOTAL +! 1+ TOTAL @ 1000 >
UNTIL TOTAL @ . . 500 TOTAL ! ; Обратите внимание, что.
как и в случае цикла DO-LOOP, слова, находящиеся между BEGIN и UNTIL, будут исполнены хотя бы один раз, потому что проверка условия производится последним словом в цикле.
Более гибкой является другая конструкция неопределенного цикла BEGIN...WHILE...REPEAT. Она подобна BEGIN...UNTIL, за исключением того, что REPEAT будет повторять цикл до тех пор, пока в стеке слово WHILE встречает ненулевое значение. Если флаг, предшествующий WHILE, имеет значение истина, то цикл исполняется до слова REPEAT, а затем вновь возвращается к слову BEGIN. Если флаг имеет значение ложь, исполнение переходит на слово, которое следует за REPEAT, покидая цикл. На рис.8.1 схематически показана эта идея.
Рассмотрим другое определение слова TIL-10 из предыдущего примера: : TIL-10 BEGIN DUP 10 < WHILE DUP . 1+ REPEAT : Как и раньше, при исполнении 5 TIL-10 получим 5 6 7 8 9. Но разница есть. Если в первом примере вместо 5 мы введем 15, то будет выведено только число 15, потому что цикл исполнится по крайней мере один раз. Если ввести 15 перед BEGIN во втором примере, то WHILE передаст управление после слова REPEAT и число 15 выведено не будет. Иначе говоря, если конструкция BEGIN...UNTIL требует исполнения находящихся между ними слов по крайней мере один раз, то конструкция BEGIN...WHILE...REPEAT позволяет пропустить все без исполнения, если это необходимо. Вот еще один пример. Это слово, которое разрешает ввод только цифры: : GET# CR ." Введите цифру " BEGIN KEY DUP DUP 48 < SWAP 57 > OR WHILE ." Неверно, должна быть ЦИФРА " REPEAT ; Фрагмент, предшествующий WHILE, будет исполняться всегда, по крайней мере, один раз, а слово после WHILE никогда не будет исполняться при выходе из цикла.
В сущности BEGIN...WHILE...REPEAT можно использовать вместо конструкции IF...THEN (хотя и очень неэффективно). Немного подумав, вы увидите, что BEGIN WHILE ... 0 REPEAT будет действовать так же, как IF ... THEN Почти точно так же BEGIN ... 0= WHILE REPEAT будет давать тот же результат, что и BEGIN ...
UNTIL разумеется, более медленно. Вероятно, чаще всего используют конструкцию BEGIN...WHILE...REPEAT для того, чтобы исключить даже однократное исполнение находящихся внутри слов, пока не будет удовлетворено некоторое условие, как в примере TIL-10. Другое общепринятое применение бесконечного цикла - для того, чтобы какой-либо фрагмент не исполнялся, когда начнет удовлетворяться некоторое условие, как в примере слова GET#. Вот более сложный пример программы, которую вы можете использовать как учебное пособие для освоения кодов ASCII. Она позволяет проверить знание до 20 символов: : ASK CR ." Какой код имеет литера " ; : TOOBIG CR ." Число очень велико. Попробуйте еще." ; : TOOSMALL CR ." Число очень мало. Попробуйте еще. " ; : GUESS 20 О DO CR ." Нажмите любую клавишу буквы или цифры " KEY DUP EMIT >R BEGIN ASK R@ EMIT #IN DUP R@ <>
WHILE R@ < IF TOOSMALL ELSE TOOBIG THEN REPEAT CR ." Вы УГАДАЛИ!!! " R> DROP LOOP ;
После небольшого анализа вы поймете, как работает эта "угадайка". Этот пример незатейливой обучающей программы GUESS показывает, что, применяя цикл BEGIN...WHILE...REPEAT, можно написать программу значительно проще, чем используя только цикл BEGIN...UNTIL.
Упражнения
1. Определите слово .S под именем NEW.S, используя цикл BEGIN... 2. Определите слово ST-SUM для суммирования содержимого стека, используя цикл BEGIN... Дайте другое определение с именем ST-SUM1 с помощью цикла DO-LOOP. 3. Измените программу ?YEAR, назвав ее ?YEAR1. чтобы она печатала год, увеличение вклада за год и сумму на счете в конце каждого года. 4. Пусть популяция мышей увеличивается на 0.05, или 1/200, за каждый день. Напишите слово, позволяющее рассчитать, через сколько дней популяция удвоится. Используйте при этом цикл BEGIN... и LEAVE. 5. Дайте новое определение слова GET$. приведенного выше, с использованием цикла BEGIN... и без ограничения длины строки 256 символами. 6. В MMSFORTH предусмотрено слово Y/N, которое печатает (Y/N)? и останавливается в ожидании нажатия клавиши.
Если нажата клавиша "Y", в стек кладется 0. если "N" - то 1. Если нажимают любую другую клавишу, ожидание продолжается. Определите слово Y/N с именем Y/N1.
Выводы
Циклы представляют собой управляющие структуры, как и рассмотренные в гл. 7 конструкции IF...THEN. И так же, как конструкция IF...THEN относится к фундаментальным средствам управления программным потоком, циклы представляют главное средство повторного выполнения программ, для чего в основном и используются компьютеры. Самой важной причиной могущества компьютеров является их способность манипулировать с битами информации с очень большой скоростью. Если бы не существовало циклов, скорость их работы не приносила бы практической пользы. Иначе говоря, циклы важны для составления практически полезных программ. По существу, язык Форт (либо другой язык с диалоговым режимом, программа редактора и многое другое) представляет собой бесконечный цикл, который периодически посматривает на клавиатуру и исполняет то, что вы приказываете.
Известны циклы двух типов: такие, как DO-LOOP с определенными извне пределами, и неопределенные циклы типа BEGIN...UNTIL, в которых пределы устанавливаются внутри цикла. Язык Форт обращается с циклами по крайней мере не хуже других языков программирования. Заметьте, между прочим, что неопределенные циклы могут быть также, что называется, "бесконечными". Например, следующий цикл никогда не закончится: : INFINITY BEGIN KEY EMIT 0 UNTIL ; Слово UNTIL никогда не встретит значение флага истина, поэтому цикл не прекратится никогда. Следовательно, циклы могут быть опасны, если вы допустите подобную оплошность.
Фактически известен еще один тип цикла, который позволяет организовать так называемую рекурсию. Рекурсия попросту состоит в том, что слово имеет возможность вызывать само себя, что в некоторых случаях может быть очень полезным. Однако для ее правильного использования необходимо хорошо понимать внутреннее устройство самого языка Форт, и мы вернемся к этому вопросу в гл. 15.
Символьные строки
В предыдущих главах мы употребляли термин "строка", предполагая, что его смысл совершенно очевиден. Строка (символьная строки) - это последовательность алфавитно-цифровых символов, которая запоминается в байтах памяти в виде последовательности кодов ASCII. Таким образом, строка "STRING" будет запоминаться в памяти в виде последовательности десятичных значений байтов: 83 84 82 73 78 71 STRING
Любой язык программирования в какой-то степени может обращаться со строками. Ввод и вывод (даже чисел) производится с использованием строк, листинг и исходный код программы существуют в виде строк, так же как и сообщения оператору. Даже мнемоника языка ассемблера представляется символьными строками. Когда в гл. 5 вы пользовались форматным выводом (), вы также преобразовывали числа в строки.
Но помимо того, что строки используются для осуществления ввода-вывода, вывода меню в программах и приглашений-подсказок, хороший язык программирования позволяет производить со строками некоторые операции в памяти как со специальным типом данных. Это дает вам возможность выполнять такие действия, как ввод строк с клавиатуры, посылку строк на экран и принтер, хранение их в строковых переменных, добавление одной строки к концу другой, извлечение части строки из более длинной строки, сравнение строк и т.д. Программы для управления базами данных, программы для обработки текстов, редактор и многие другие манипулируют с большими объемами строковой информации. Примером языка, очень удобного для работы со строками, может служить Бейсик, неудобен для обработки строк Фортран. Другие языки программирования располагаются где-то между ними, В соответствии со стандартами Форт - не очень хороший язык для работы со строками, но, к счастью, он настолько гибкий, что не составляет большого труда добавить в него мощные слова для строковых операций. Мы покажем вам, как определить некоторые слова такого рода.
Различные версии языка Форт, поставляемые фирмами, предоставляют пользователям широкие возможности обработки строк, некоторые из них мы покажем вам на примере версии MMSFORTH.
Вы увидите, что можно определить почти любое слово, которое вам может потребоваться для работы со строками.
Для описания строки требуются две компоненты: содержимое (текст) строки и ее длина. В английском языке длина строки задается с помощью разделителей, например пробелов, кавычек, точек и запятых. Такой же способ может быть принят и для компьютера. Например, заключительная кавычка в выражении ." This is a string " (Это строка) не является словом языка Форт. Это - разделитель. Слово ." знает, что строка заканчивается там, где в тексте встретится знак кавычки. Разделителем является также закрывающая скобка в комментариях. В качестве разделителя в Форте можно использовать любое значение байта, однако чаще всего применяется пробел, которым при вводе должны быть разделены слова (или числа). Другой способ задания строки состоит в указании ее длины числом. Давайте обсудим этот способ более подробно.
Строки счетной длины, их ввод с клавиатуры
Строка, длина которой указана числом, называется в Форте счетной (реже говорят нумерованной строкой). Число символов, или счет, обычно указывается в байте-счетчике, который помещается перед текстом строки (тем самым длина строки ограничивается 256 символами). Строка предыдущего примера будет запомнена в памяти в виде счетной строки так: 06 83 84 82 73 78 71 STRING Но как же ввести строку в компьютер? Кроме слова KEY, с которым мы познакомились в гл. 5, для ввода строк предусмотрено еще несколько слов: EXPECT, WORD и QUERY.
Рассмотрим сначала слово EXPECT. (Коротко о нем говорилось в гл. 5.) Давайте создадим слово $IN (ввести_строку), которое будет печатать знак вопроса и ждать вашего ввода строки с клавиатуры. Когда вы нажмете клавишу ввода , строка будет запоминаться в виде счетной строки, а адрес ее начала (адрес байта-счетчика) будет помещен в стек. Сначала установим, имеется ли в словаре вашей версии Форта слово BLANK (пробел). Это слово не является обязательным в стандартах, но включено почти во все версии Форта.
Если же это слово отсутствует, то его можно легко определить: : BLANK ( адр n ---) 32 FILL ;
Напомним, что FILL заполняет n байтов, начиная с адреса адр, кодом ASCII, находящимся на вершине стека, в данном случае кодом пробела 32. Таким образом, слово BLANK заполняет n байтов пробелами, начиная с указанного адреса. Нам потребуется также слово 2DUP; если у вас нет его в системе, определите его так: : 2DUP ( n1 n2 - n1 n2 n1 n2) OVER OVER ;
По мере продвижения нашего определения слова $IN мы будем нуждаться для проверки в словах COUNT и TYPE (что мы уже видели в гл. 5). Напомним, что слово TYPE предполагает в стеке адрес строки (не счетной) и число символов в строке и после этого выводит указанное число символов из строки. Слово COUNT просматривает содержимое указанного адреса и возвращает в стек находящийся там байт и, кроме того, адрес первого байта строки, подготавливая стек к использованию слова TYPE. Слово COUNT можно определить следующим образом: : COUNT ( $адр - $адр+1 n) DUP 1+ SWAP C@ ; Поэтому если $адр - адрес байта-счетчика строки, то при исполнении $адр COUNT TYPE на экран будет выведена строка. Обратите внимание, что мы обозначили адрес байта-счетчика как $адр (иногда применяется обозначение $). Очевидно, что это такой же адрес, как любой другой, но, чтобы подчеркнуть, что по этому адресу хранится строка, мы используем символ $.
Будем определять $IN в несколько этапов, забывая с помощью FORGET предыдущие определения. Сначала определим : $IN PAD 1+ 255 BLANK ; и испытаем это слово, вводя $IN PAD 1+ 255 TYPE в результате вы увидите на экране 255 пробелов - это все, что содержит строка на данный момент. Обратите внимание, что мы собираемся создать строку со смещением на один байт относительно адреса буфера PAD, чтобы зарезервировать место для байта-счетчика. Теперь забудем старое определение: FORGET $IN и сделаем новое: : $IN PAD 1+ 255 2DUP BLANK ." ? " EXPECT ; Испытаем новую версию, введя слово $IN. После этого на экране мы увидим знак вопроса, после которого не будет слова "ok".
Форт ожидает, что вы будете вводить строку, и, когда вы нажмете клавишу ввода , эта строка будет записана в память. Попробуйте ввести $IN ? THIS IS A TEST (ЭТО ТЕСТ) (Знак вопроса "?" был напечатан компьютером: вы ввели "THIS IS A TEST" ("Это тест".) Теперь введем PAD 1+ 14 TYPE Тогда вы увидите на экране THIS IS A TEST Вы приказали компьютеру, чтобы он вывел 14 символов, начиная с адреса PAD+1, на экран, и убедились, что это были символы, введенные словом EXPECT. Слово EXPECT ищет адрес и указанное в стеке число символов. Мы указали адрес и PAD плюс 1, так как именно этот адрес и число 255 были указаны в определении слова $IN и остались в стеке благодаря слову 2DUP. Слово EXPECT ожидает, когда будут введены символы строки. Если ввод прекращается с помощью клавиши , то словом EXPECT будут запомнены только те символы, которые были введены до нажатия клавиши . Если при вводе будет превышено указанное число символов (в данном случае 255), то слово EXPECT будет действовать так же, как если бы вы нажали клавишу ввода . Но наше слово еще не закончено, так как не было запомнено число введенных символов. Снова забудем предыдущее определение FORGET $IN и дадим, наконец, полное определение: : $IN ( - $адр) PAD 1+ 255 2DUP BLANK ." ? " 2DUP EXPECT -TRAILING PAD C! DROP PAD; Здесь ключевое слово "TRAILING. Этому слову нужен в стеке начальный адрес и число символов, так же как словам BLANK и EXPECT; в данном случае указанный адрес и число были запомнены в стеке с помощью слова 2DUP, непосредственно предшествующего слову EXPECT. Слово -TRAILING заменяет затем байт длины строки числом символов до первого пробела "в хвосте" строки символов. В действительности слово -TRAILING просматривает байты, отступая от указанного адреса на указанное в стеке число байтов вперед, и вычитает единицу, если обнаруживает пробел. Это повторяется до тех пор, пока не встретится первый символ, не являющийся пробелом. Поэтому число, которое -TRAILING возвращает в стек, является длиной (счетом) строки.
Фактически - TRAILING отнимает пробелы, следующие после строки. Длина строки затем запоминается словом $IN по адресу ячейки PAD (с помощью PAD С!). На этом заканчивается формирование счетной строки. Остается с помощью DROP удалить адрес PAD плюс 1, который кладется в стек словом -TRAILING, и заменить его на PAD, т.е. адрес байта-счетчика.
Заслуживают упоминания некоторые детали. Стандарт Форт-79 допускает, чтобы слово EXPECT записывало до двух нулей (байтов со значением 0) в конце строки символов. Если ваша версия Форта делает это (как, например, версия MMSFORTH), то необходимо в слове $IN сразу же после -TRAILING вставить операцию 2 -. В Форт-83 нулевые байты не разрешаются. Вам нужно проверить, как устроен ваш Форт. Другая деталь: число символов, собираемых словом EXPECT в Форт-83 и других версиях, запоминается в переменной SPAN. Это позволяет упростить определение слова $IN на Форт-83: : $IN ( - $адр) PAD 1+ 255 ." ? " EXPECT SPAN @ PAD С! PAD ;
Другими словами, на Форт-83 здесь слово -TRAILING не нужно для подсчета длины строки.
Разумеется, слова EXPECT и -TRAILING имеют много других применений. Мы можем увидеть некоторые их применения из нескольких упражнений.
Упражнения
1. Вспомните слово EMIT (см. гл. 5), а потом дайте определение слова NEWTYPE, используя цикл DO-LOOP. 2. Вспомните слово KEY из гл. 5 и дайте определение слова EXPECT, используя цикл BEGIN...WHILE...BEPEAT. Сделайте так, чтобы ваша версия слова EXPECT запоминала число символов строки в переменной SPAN. Понимаете ли вы, почему здесь нельзя использовать счетный цикл? Не забудьте предусмотреть возможность появления символа стирания влево (код 8). 3. Предположим, что слово EXPECT завершает несчетную строку по крайней мере одним нулевым байтом. Определите слово, которое будет выводить строку на экран, если известен адрес первого символа. Используйте цикл BEGIN...WHILE...REPEAT. 4. Зарезервируйте 258 байтов в массиве с именем $SPACE, используя слова CREATE и ALLOT. Определите слово GET$.
которое работало бы так же, как $IN, но запоминало бы строку в переменной $SPACE. 5. Определите слово ADD$, которое будет ожидать ввода строки, а затем присоединять ее к концу строки, находящейся в массиве $SPACE. He забудьте скорректировать байт-счетчик с помощью слова - TRAILING после того, как строка будет помещена в переменную. Очевидно, что суммарная длина обеих строк не должна превышать 255 байтов, поэтому, если строка получится слишком длинной, ее необходимо урезать. 6. Дайте новое определение слова ADD$ под именем ADD$1 такое,чтобы вторая строка игнорировалась, если суммарная длина обеих строк превышает 255 байтов, т.е. строка в $SPACE не должна изменяться. 7. Просмотрите материал о форматном выводе в гл. 5 и обратите внимание на то, что слово #> оставляет в стеке адрес несчетной строки и байт-счетчик. Теперь определите слово, которое должно брать из стека число двойной длины, представляющее число центов, преобразовывать его в счетную строку вида $123.45 и запоминать строку в массиве $SPACE. Используйте слово CMOVE. 8. Определите слово LEFT$, которое предполагает наличие в стеке адреса строки, поверх которого находится байт-счетчик. Заданное в стеке число символов должно быть затем перемещено из указанного адреса по адресу PAD + 1, а новая длина строки должна быть записана в PAD. Слово LEFT$ должно оставлять в стеке адрес новой строки, т.е. PAD. Таким образом, если в $SPACE находится строка "The quick brown fox", тогда после операций $SPACE 9 LEFT$ в стеке будет оставлен адрес счетной строки "The quick".
Ввод с помощью слова WORD
WORD - это слово, преобразующее в счетную строку все, что во входном потоке следует после него, и возвращающее в стек адрес начала этой строки (байт-счетчик). Слову WORD нужен в стеке код ASCII байта-разделителя (например, 32 - пробел или 34 - кавычка "), оно запоминает все, что встречается, вплоть до этого разделителя. Попробуйте ввести следующее определение : SHOW-SILLY 34 (") WORD CR COUNT TYPE ; а затем введите предложение SHOW-SILLY This is a silly example"
( Это глупый пример) Тогда вы увидите на экране текст "This is a silly example", повторенный строкой ниже введенного. Строка "This is a silly example" была запомнена словом WORD, а затем распечатана с помощью фрагмента COUNT TYPE. Слову WORD известно, что нужно прекратить запоминание строки, когда оно встретит в стеке разделитель-кавычку (так как 34 - это код ASCII для кавычки). Если вы введете SHOW-SILLY silly example" xyz
то будет выдано сообщение об ошибке, потому что после того, как будет исполнено слово SHOW-SILLY, Форт будет пытаться интерпретировать xyz как слово языка Форт. Слово WORD прекращает ввод также при нажатии клавиши . Поэтому SHOW-SILLY This is a test
произведет такое же действие. Слово Форт также игнорирует ограничитель в начале строки, т.е. если ввести SHOW-SILLY """"This is a silly example"
то вы увидите то же самое, что и в первом примере. А вот пример уже далеко не глупый: : .( 41 WORD COUNT TYPE ; IMMEDIATE
Он представляет собой определение слова Форт-83.( (точка-скобка). Вам должно быть понятно в нем все, за исключением слова IMMEDIATE (немедленно), которое просто помечает, что последнее определяемое слово должно быть исполнено сразу, как только оно встретится, независимо от того, находится ли оно внутри другого определения через двоеточие или нет. Таким образом, слово.( будет выводить на экран все, что следует после него, вплоть до ) (скобки, код ASCII которой равен 41) даже внутри определения-двоеточия.
В большинстве версий строка, которая запоминается словом WORD, помещается в верхней части словаря. Ее адрес возвращается в стек словом HERE (здесь), и в соответствии с общепринятым в Форте жаргоном мы будем обращаться к адресу вершины словаря с помощью слова HERE. Новые слова добавляются в словарь с ячейки HERE, а слова , (запятая), ALLOT (зарезервировать) и др. резервируют место, начиная с адреса HERE.
Кроме того, слово WORD используется Форт-системой для разбора или разделения слов во входном потоке, который помещает выделенные слова в HERE.
Поэтому если после ввода строки словом SHOW-SILLY было введено что-либо еще, то вновь введенная строка будет искажена. Практически это означает, что нужно сразу использовать то, что введено и запомнено словом WORD, или защитить до дальнейшего ввода. Попробуйте ввести следующую последовательность операторов: 32 WORD TESTING COUNT TYPE
Почти наверняка в ответ вы увидите на экране текст "TYPE", а не "TESTING". Причина здесь в том, что, хотя строка "TESTING" была записана словом WORD в ячейку HERE, слово WORD использовалось Форт-системой для интерпретации операторов COUNT TYPE и так как последним, что встретилось во входном потоке, была строка "TYPE", она и была выведена на экран. На самом деле, если вы введете HERE COUNT TYPE то также увидите на экране текст "TYPE". Строка "TYPE" была преобразована в счетную строку и напечатана потому, что она была запомнена с адреса HERE, когда интерпретатор пользовался словом WORD. Если вы этого не увидели на экране, значит, ваша версия Форта - одна из немногих, которая не запоминает строку, введенную словом WORD, в ячейке HERE. Если после ввода строки словом WORD происходит ее искажение последующим вводом, то что же хорошего в этом слове? Можно ответить так: строка должна быть либо защищена, либо перемещена в памяти, прежде чем будет сделан новый ввод. Например, строку можно было бы переместить по адресу PAD, как в следующем слове: : SAVE-STRING 34 WORD DUP C@ 1+ PAD SWAP CMOVE ; Теперь введите SAVE-STRING This is a test of SAVE-STRING" PAD COUNT TYPE
Тогда вы увидите на экране текст "This is a test of SAVE-STRING" (это тест слова SAVE-STRING). Операция 1 + нужна здесь потому, что нужно переместить не только символы строки, но также и байт-счетчик. Строка, которая теперь находится в PAD, может быть перемещена куда угодно, и с ней можно обращаться так же, как со строкой, запомненной словом EXPECT в предыдущих примерах. Но еще можно очень просто защитить строку, которая вводится словом WORD, особенно если в вашей версии Форта, как в большинстве других, строка помещается на верху словаря.
Можно создать строковую константу, т.е. константу, которая вместо числа содержит строку. Вот как можно это сделать: ; $CONSTANT CREATE 34 WORD C@ 1+ ALLOT ; Действие $CONSTANT аналогично действию SAVE-STRING, за исключением того, что строка не перемещается в памяти, защищается в словаре с помощью слова ALLOT - так же, как в гл. 6 делается защита массива. Теперь можно использовать слово $CONSTANT следующим образом: $CONSTANT HOWDYDO Comment allez vous? "
(Как дела? /франц./) Если вы введете HOWDYDO COUNT TYPE на экране появится текст "Cominent allez vous?" ("Как дела?" (франц.)) Не написать ли нам разговорник, пользуясь языком Форт?
Слова SAVE-STRING, $CONSTANT, HOWDYDO и подобные им слова можно, очевидно, использовать не только для ввода с клавиатуры, но также и с диска. Заметьте, однако, что нельзя просто ввести CREATE HOWOYDO 34 WORD Comment allez vous? " C@ 1+ ALLOT так как С@ 1+ ALLOT наложится на фразу, которую вы собирались сохранить. Почти во всех случаях слово WORD должно использоваться в определениях через двоеточие.
Если в вашей версии Форт строка, вводимая словом WORD, запоминается не в ячейке HERE, то слово $CONSTANT можно определить следующим образом: : $CONSTANT CREATE 34 WORD COUNT DUP 1+ ALLOT HERE SWAP CMOVE ;
Т. е. строка должна быть перемещена оператором CMOVE в HERE, чтобы защитить ее с помощью ALLOT.
В сочетании со словом WORD часто используется слово QUERY. В Форт83 это слово не обязательное (оно входит в Форт-79), тем не менее имеется почти во всех версиях. Слово QUERY работает почти так же, как EXPECT (оно и определяется через EXPECT), но в отличие от последнего оно запоминает строку не с того адреса, который вы указываете, или используя указанную длину строки, а в текстовом входном буфере (TIB) длиной 80 байтов - специально отведенной области памяти. Этот буфер используется для хранения входного потока информации с клавиатуры, и слово QUERY используется Форт-системой как раз для приема входного потока.
В Форт-83 и большинстве диалектов адрес начала текстового входного буфера возвращается словом TIB (от Text Inpul Buffer). Попробуйте ввести TIB 11 TYPE и вы увидите, что на экран выводится "TIB 11 TYPE". Вы просто вывели 11 символов из текстового входного буфера, т.е. то, что вы в него ввели. Теперь попробуйте ввести определение : SILLY-QUERY TIB 80 BLANK 0 >IN ! ." ? " QUERY CR TIB 80 TYPE ; Как и $IN, слово SILLY-QUERY делает паузу, чтобы вы что-либо ввели, например фразу "The quick brown", а затем печатает то, что вы ввели. Но, кроме того, оно еще печатает сообщение об ошибке. Это происходит потому, что после того, как SILLY-QUERY выведет то, что было введено, Форт пытается интерпретировать все находящееся во входном буфере как Форт-слова и, естественно, он не может распознать текст "The". Вы можете избежать этого, если сразу же после TYPE введете в определение TIB 80 BLANK, чтобы очистить буфер. Возможно, единственная загадочная часть определения - это О >IN !. Слово >IN - это переменная пользователя, в которой содержится число, показывающее, на какую глубину использован буфер ввода IN в 0 для того, чтобы после QUERY либо интерпретатор, либо слово WORD выполняли свои действия с начала буфера. Слово QUERY само по себе мало что дает пользователю (хотя, конечно, оно очень важно для осуществления ввода в Форт), вместо него обычно используется слово EXPECT. Слово QUERY почти всегда используется в сочетании со словом WORD. Слово QUERY заполняет текстовый входной буфер, после чего слово WORD производит разбор текста, т.е. оно разделяет входной поток на меньшие строки с учетом указанного разделителя. Следовательно, WORD можно использовать для разделения входного потока на более мелкие части. Попробуйте ввести следующее определение: : SILLY-ТОО TIB 80 BLANK 0 >IN ! ." ? " QUERY 32 WORD CR COUNT TYPE TIB 80 BLANK ;
Введите SILLY-ТОО и в ответ на вопрос "?" введите текст "The quick brown fox".
Тогда в следующей строке вы увидите, что выводится текст "The". Код 32 указывает слову WORD, что разделителем считается пробел, поэтому слово WORD преобразовало текст "The" в счетную строку, а COUNT TYPE вывело эту строку. Конечно, этот пример глупый также, поскольку он не делает ничего, кроме вывода части того, что вы ввели, на экран. Как и при других применениях слова WORD, входной поток должен быть запомнен либо защищен раньше, чем определение будет исполнено. Вот два более полезных слова: : PARSIT TIB 80 BLANK 0 >IN ! ." ? " QUERY 4 0 DO 44 WORD DUP C@ 1+ PAD 10 I * + SWAP CMOVE LOOP TIB 80 BLANK ; : SHOWIT 4 0 DO PAD I 10 * + COUNT TYPE LOOP ;
Слово PARSIT разделяет четыре строки, каждая из которых может содержать до 10 символов, которые отделены друг от друга запятой (код ASCII 44). Вам должно быть понятно, как оно работает, по аналогии с другими словами, которые мы определили в этой главе. (Слово PARSIT работает, только если ввести его с клавиатуры, поскольку слово QUERY работает только с клавиатурой, но не работает с диском.) В гл. 10 мы увидим, что разбор используется для выделения строковых данных из блоков.
Упражнения
1. Определите слово $VARIABLE, которое, если ему предшествует число, будет резервировать указанное им количество байтов в памяти для запоминания именованной строки, т. е. 256 $VARIABLE $SPACE резервирует 256 байтов для счетной строки в переменной $SPACE. 2. Определите слово $!, которое, если в стеке имеются два адреса $адр1 и $адр2, будет перемещать счетную строку из адреса $адр1 в $адр2. Тогда если строка запомнена в PAD, то : PAD $SPACE $! переместит строку в переменную, определенную в упражнении 1. 3. Определите слово $GET, которое будет перемещать вводимую после него строку в ячейку с адресом, указанным в стеке. Так, например, PAD $GET The quick brown fox..." будет помещать счетную строку "The quick brown fox..." в память, начиная с PAD. 4. Определите слово $", которое должно помещать следующую после него строку в память, начиная с PAD, и возвращать в стек адрес, где сохраняется строка.
Таким образом , $" This is a string," будет выводить на экран "This is a string" и помещать в стек адрес PAD. 5. Как бы вы запомнили строку, вводимую словом S" в переменную $SPACE, пользуясь словами, определенными в этих упражнениях? 6. 10 строк запомнены в строковых константах с именами $$1, $$2,..., $S10. Определите слово $CHOICE, которое, если указывается номер, будет печатать соответствующую строку. Например, 4 $CHOISE будет печатать строку, хранящуюся в $4. Может быть, вам потребуется вспомнить векторное исполнение из гл. 6. 7. Определите четыре строковые переменные с именами 1STRING, 2STRING и т.д. Теперь определите слово, аналогичное PARSIT, которое будет помещать четыре строки с разделителем # в эти переменные, а не в разные места после PAD, как это делало слово PARSIT. (.Указание: используйте вектор и $!.) 8. Как вы думаете, что происходит со значением переменной >IN, когда исполняется слово WORD? Модифицируйте слово, которое вы определили в упражнении 7, чтобы оно печатало значения переменной >IN каждый раз при исполнении слова WORD. 9. Определите слово, эквивалентное (, под именем ((. Оно должно работать как внутри, так и вне определения-двоеточия.
Расширенный набор строковых операций в MMSFORTH
К настоящему времени мы уяснили, что, хотя Форт сам по себе не обладает широкими возможностями работы со строками, в нем есть несколько слов, необходимых для определения практически любого слова для работы со строками, какое вам только потребуется.
Как вы уже видели, многие слова, нужные всем, можно определить и включить в любую версию Форта. К сожалению, только в некоторых версиях предусмотрены такие слова, расширяющие возможности языка. Одной из версий является MMSFORTH. Она имитирует большинство операторов для обработки строк, имеющихся у Бейсика, представляя собой превосходный пример, каким должно быть хорошее программное обеспечение для работы со строками. Если даже вы не располагаете версией MMSFORTH, вам следует ознакомиться с его словами; знать о них очень полезно, и они могут дать вам некоторые идеи для создания слов, которые могут потребоваться.
Мы дадим описание каждого слова MMSFORTH для работы со строками и некоторые примеры их использования. В некоторых случаях определения слов будут достаточно очевидны, и вы можете попробовать сами дать их определения. Но мы не будем приводить определения этих слов, потому что для некоторых слов требуются приемы, которые вы еще не изучали (в частности, программирование на ассемблере), а также вследствие того, что они защищены авторским правом на MMSFORTH. В конце концов, авторы MMSFORTH имеют право получать вознаграждение за свою работу. Сводка слов MMSFORTH для работы со строками приведена в табл.9.1. В некоторых случаях их действие настолько очевидно, что мы не будем описывать их подробнее. В других случаях мы дадим примеры применения некоторых слов.
Таблица 9.1. Слова MMSFORTH для работы с символьными строками (Все строки принимаются счетными.)
$CONSTANT ( --) ( строковая константа) Используется в форме $CONSTANT NAME "This will be a constant" (Это должна быть константа) Во время исполнения слова NAME (имя) в стек кладется адрес строки. Действие слова такое же, как слова $CONSTANT, ранее описанного в этой главе.
$VARIABLE ( n --) (строковая переменная) Резервирует N байтов для запоминания строки в массиве. Используется в формате 256 $VARIABLE NAME Действие слова NAME такое же, как в случае константы $CONSTANT.
$. ( $адр --) Выводит на экран строку, начинающуюся с адреса $адр. Эквивалентно по своему действию группе операторов COUNT TYPE,
$! ( $адр1 $адр2 --) Перемещает строку, начинающуюся с адреса $адр1, в область памяти с начальным адресом $адр2. Начиная с $адр2, должно быть зарезервировано место, достаточное для помещения строки, иначе результат будет искажен.
$" ( -- $адр) Принимает следующую за собой символьную строку (используя в качестве разделителя кавычку '" '), возвращает в стек адрес, начиная с которого запоминается строка. Используется в форме
$" This is a string" $. Будет выведено "This is a string" (Это строка).
То же самое, что и слово $", которое вы определили в упражнениях. Строка запоминается с адреса PAD.
IN$ ( -- $адр) Печатает знак вопроса "?" и приостанавливается, ожидая ввод строки с кавычкой в качестве ограничителя ('" '). Пример использования этого слова при вводе строки с клавиатуры
IN$ ? This is a string"
$. Будет выведено "This is a string", Применение идентично применений слова $IN, определенного ранее в данной главе.
LEFT$ ($адр1 n - $адр2) Помещает n левых символов строки, находящейся по адресу $адр1, в счетную строку, начинающуюся с адреса PAD и возвращает в стек адрес PAD. Если в строке Окажется меньше п символов, то будет помещена вся строка. Если по адресу FOXY запомнена строка "The quick brown fox", то последовательность операторов FOXY 9 LEFT$ $. напечатает "The quick",
RIGHT$ ( $адр1 n - $адр2) Помещает п правых символов строки, находящейся по адресу $адр1, в счетную строку, начинающуюся с адреса PAD, и возвращает в стек адрес PAD. Если в строке меньше чем n символов, то будет помещена вся строка. Если по адресу FOXY запомнена строка "The quick brown fox", то последовательность операторов FOXY 10 RIGHT$ $. напечатает текст "brown fox".
MID$ ( $адр1 n1 n2 -- $адр) Помещает n2 символов из середины строки, находящейся по адресу $адр1, начиная с символа номер n1, в счетную строку, начинающуюся с адреса PAD. Возвращает в стек адрес PAD, Если между позицией n1 и концом строки окажется меньше n2 символов, то будут помещены все символы, начиная с номера n1 до конца строки. Если по адресу FOXY запомнена строка 'The Quick brown fox", то последовательность операторов FOXY 5 11 MID$ $. напечатает текст "quick brown".
$XCHG ( $адр1 $адр2 --) Переставляет содержимое с адреса $адр1 с содержимым с адреса $адр2, используя для промежуточного хранения строки, начинающейся с адреса $адр2, адрес PAD. Начиная с адреса $адр1 и адреса $адр2, должно быть зарезервировано место, достаточное для размещения большей из двух строк, иначе могут произойти непредвиденные последствия.
$+ ( $адр1 $адр2 -- $адр3) Содержимое адреса $адр2 добавляется (соединяется) к концу содержимого адреса $адр1, результирующая строка помещается, начиная с адреса PAD, при этом в стек помещается адрес PAD. Если с адреса FOXYSTART начинается строка "The quick", а с адреса FOXYEND находится строка "brown fox", то последовательность операторов FOXYSTART FOXYEND $+ $. напечатает текст "The quickbrown fox". Обратите внимание, что между словами quick и brown нет пробела .
$COMPARE ( $адр1 $адр2 -- флаг) Две строки сравниваются посимвольно с учетом алфавитного порядка символов, в стек возвращается флаг. Если строки равны, то возвращается флаг 0; если строка с адресом $адр1 находится по алфавиту дальше, чем строка с адресом $адр2, то возвращается +1; если строка с адресом $адр2 находится по алфавиту дальше, чем строка с адресом $адр1, то возвращается -1. Если по адресу ALPHA находится Строка "abc". а по адресу BETA "abd", то последовательность операторов ALPHA BETA $COMPARE . выведет на экран -1, в то время как BETA ALPHA $COMPARE . выдаст 1. Символы, не входящие в алфавит, сравнивается по значению кодов ASCII.
INSTR ( $адр1 $адр2 -- n) Содержимое строки по адресу $адр1 ищется в тексте строки, начинающейся с адреса $адр2. Если оно не находится, в стек возвращается 0. Если находится в стек возвращается номер позиции первого символа строки, хранящейся по адресу $адр2. Если по адресу FOXY содержится строка "The quick Orown fox", a no адресу FOXYPART строка "brown", то последовательность операторов FOXY FOXYPART INSTR . выведет на экран 11.
ASC ( $адр - n) Возвращает в стек значение кода ASCII первого символа строки, хранящейся по адресу $адр1. Если по адресу FOXY помещена строка "The quick brown fox", то после FOXY ASC . будет выведено 84, т.е. код ASCII буквы Т.
CHR$ ( n -- $адр) Преобразует число п в односимвольную счетную строку, которая содержит символ, код ASCII которого равен n. 84 CHR$ $.
напечатает букву Т. Слово CHR$ можно определить следующим образом: : CHR$ I PAD С! PAD 1+ С! PAD ;
INKEY$ ( -- $адр) Опрашивает клавиатуру, и если была нажата клавиша, то по адресу PAD запоминается односимвольная счетная строка. Если ни одна клавиша не нажата, то в PAD помещается 0. Возвращает в стек адрес PAD, Обычно слово INKEY$ используется в цикле, пример его применения вы найдете в тексте ниже.
STRING$ ( n1 $адр1 -- $адр2) Создает счетную строку в PAD, состоящую из n1 символов, таких же, как первый символ строки с адресом $адр1, В стек помещается адрес PAD, 10 $" S" STRINGS $. выведет строку "SSSSSSSSSS". Приведенное ниже слово очистит содержимое счетной строки, заместив его пробелами: : ERASE-$ ( $addr --) DUP C@ 32 CHR$ STRING$ SWAP $! ;
STR$ ( n -- $адр) Преобразует число n в строку, запоминаемую в PAD. и возвращает в стек адрес PAD. 12345 STR$ $. выводит "12345",
LEN ( $адр - n) возвращает в стек длину строки, запоминаемой по адресу $адр. Определение этого слова очень простое : : LEN С@ ; VAL ( $адр - -n или dn) Если начальные символы строки по адресу $адр распознаются как число в текущей системе счисления (BASE), то в стек помещается это число. Если в числе обнаруживается десятичная точка, то в стек помещается число двойной длины, Если первый символ не является цифрой, то в стек возвращается 0. Таким образом, при шестнадцатеричной системе счисления $" 123AXYZ" VAL . выводит 123А $" 123.ABC" VAL D. выводит 123ABC и $" xyz" VAL . выводит 0
$ARRAY ( n1 n2 --) Слово-определитель для создания строковых массивов. если ввести 29 19 $ARRAY 20STRINGS то последовательность операторов 2 20STRINGS возвратит в стек адрес, где помещается строка длиной до 30 символов (n1) и, кроме того. будет запомнен байт-счетчик. Таким образом. $" The quick brown fox" 2 20STRINGS $! запомнит строку и 2 20STR1NGS $. в свою очередь, выведет на экран "The quick brown fox". Максимальное значение n1 равно 254.
Элементы нумеруются, начиная с нуля. поэтому значение n2 = 19 в вышеприведенном примере резервировало место для 20 строк.
2$ARRAY ( n1 n2 n3 --) Слово-определитель для создания строковых матриц из п2+1 рядов и п3+1 столбцов, причем каждая строка содержит до п1 символов плюс байт-счетчик. Если было введено 19 9 4 2$ARRAY 50STRINGS то $" The quick brown fox" 5 2 50STRINGS $! запомнит текст строки. Тогда 5 2 50STRINGS 8 О 50STRINGS $! создаст ее копию в элементе матрицы в 6-м ряду. 0-м столбце, так что 8 О 50STRINGS $. выведет строку "The quick brown fox".
$-TB ( $адр -- $адр) Удаляет пробелы в конце счетной строки, при этом уменьшает содержимое счетчика числа символов строки на число обнаруженных пробелов. Таким образом, последовательность операторов $" The quick " DUP $. $-ТВ DUP $. $. напечатает The quick The quickThe quick Завершающие пробелы были удалены перед вторым словом $. .
Приведенных объяснений должно быть достаточно, чтобы понять действие большинства описанных слов для работы со строками. Однако некоторые примеры помогут подчеркнуть их полезность. Возможно, наиболее трудным среди этих слов является INKEY$, поэтому лучше всего посмотреть его действие на примере. Действие слова INKEY$ аналогично действию ?KEY, которое является в его определении самым главным. Мы определим слово SHOWKEYS, которое просто выводит на экран все, что было введено с клавиатуры (в том числе символы возврата каретки влево не один шаг и перевода каретки), до тех пор, пока на будет нажата клавиша точки "."; мы определим это слово, используя как ?KEY, так и INKEY$.
: SHOWKEYS ( --) BEGIN (Начало условного цикла) ?KEY DUP (Помещает в стек код ASCII символа или 0, если клавиша не нажата) ?DUP IF EMIT THEN (Печатает символ, если не 0) 46 = UNTIL ; (Повторяет исполнение, начиная с ?KEY. если не была нажата клавиша ".", код ASCII равен 46) А вот определение через INKEY$: : SHOWKEYS ( --) BEGIN (Начало условного цикла) INKEY$ DUP $. (Считывает код клавиши в счетную строку печатает ее.
Длина строки равна 0, если не была нажата клавиша, поэтому ничего не печатается) 1+ С@ (Извлекает в стек символ из строки) 46 = UNTIL ; (Повторяет исполнение с INKEY$, если не была нажата ".", код ASCII равен 46)
Второе определение более медленное, чем первое, но поскольку любое из них работает быстрее, чем кто-либо сможет сделать ввод с клавиатуры, то скорость не является определяющим фактором, Второе определение короче и понятнее. Но главное преимущество второго определения состоит в том, что введенный символ запоминается как счетная строка, которая готова для работы с другими словами, предназначенными для обработки строковых данных.
Приведем несколько более полезный пример. Мы определим слово BUILDTEST, которое обеспечит возможность непосредственно записывать счетную строку в строковую переменную TEST$ (аналогичное слово вы определили в упражнении, пользуясь словами EXPECT и -TRAILING). В качестве разделителя мы используем символ #. Сначала определим строковую переменную 255 $VARIABLE TEST$ и установим ее длину равной нулю: 0 TEST$ С! Теперь мы можем дать определение слова BUILDTEST: : BUILDTEST ( -- ) BEGIN (Начало условного цикла) INKEY$ DUP DUP $. (Принимает строку- символ с клавиатуры, копирует ее адрес и распечатывает ее) $" #" $COMPARE (Сравнивает строку-символ с "#" и возвращает 0, если есть "#") WHILE (Если "#" не обнаружен, продолжает цикл, иначе переходит на исполнение слов после REPEAT и заканчивается) TEST$ SWAP $+ (Добавляет строку-Символ к TEST$, результат - в PAD) TEST$ $! (Запоминает слитую строку в ТЕSТ$) REPEAT ; (Переходит к повторению с INKEY$)
Хотя в данном случае можно описать слово BUILDTEST "обычными" словами Форта, такое определение было бы труднее и для написания, и для понимания. И кроме того, заметьте, что в отличие от тех слов, которые вы определили со словами EXPECT и -TRAILING, если вы используете слово BUILDTEST без предварительного "обнуления" переменной TEST$, то оно будет присоединять новую строку к тому, что уже было в TEST$.
Чтобы так же действовало определение с "обычными" словами, нужно было бы использовать слово ?KEY, при этом определение самого слова ?KEY было бы очень замысловатым. Конечно, вы можете и прямо ввести строку с клавиатуры в TEST$, если наберете $" I am filling the String" TESTI $! (Я заполняю строку) В упражнениях мы предложим вам описать более общее и более полезное слово BUILD$.
Телефонный справочник
Чтобы практически оценить полезность приведенных слов для работы со строками, мы составим небольшую программу, которая будет создавать телефонный справочник. Для этого нужно определить три главных слова: STOREPHONE (записать_телефон), которое будет запрашивать имя абонента и номер телефона и запоминать эти данные в массиве, GETPHONE (выдать телефон), которое будет запрашивать имя абонента и выдавать номер его телефона, если абонент найден, и ERASEPHONE (стереть_телефон) для удаления абонента и номера телефона, чтобы освободить место в справочнике.
Мы используем два массива: 10-байтовый массив AVAILABLE (доступность), в котором каждый байт будет содержать 0 или 1 в зависимости от того, свободна (0) или занята (1) позиция в справочнике; второй массив PHONES (телефоны), представляющий собой строковую матрицу размерности 10х2 (10 строчек х 2 столбца), в каждой строчке содержится имя абонента (нулевой столбец) и телефон (первый столбец). Вначале создадим и инициализируем массивы: CREATE AVAILABLE 10 ALLOT AVAILABLE 10 0 FILL создается массив AVAILABLE и инициализируется заполнением его нулями (все элементы справочника, или позиции, свободны). При вводе 29 9 1 2$ARRAY PHONES создается массив PHONES, в котором на каждую запись отводится 30 символов для имени абонента и номера телефона. Если требуется справочник на большее число абонентов, то нужно резервировать большее число байтов в массиве AVAILABLE и большее число строчек в массиве PHONES.
Теперь можно приступить к определению трех слов, нужных нам для работы со справочником:
: STOREPHONE ( -- ) (Запомнить_телефон) ." Имя абонента?" IN$ (Вводит имя абонента) PAD 31 + $! PAD 31 + (Запоминает его выше PAD; выдает адрес) SPACE ." Номер телефона?" IN$ (Вводит номер телефона PAD) 10 0 DO (Начинает цикл просмотра справочника) AVAILABLE I + С@ (Определяет, свободна [0] или занята [1] запись) 0= IF I 1 PHONES $! (Запоминает номер в элементе I,1) I 0 PHONES $! (Запоминает имя в элементе I,0.) 1 I AVAILA8LE + C! (Запоминает 1, чтобы отметить, что I-я запись занята) LEAVE (Запись произведена, поэтому вы THEN ходит из цикла и из слова) I 9 = IF (Если дошли до конца записей) CR ." Справочник заполнен" (Не обнаружено свободного места в справочнике) THEN LOOP ;
После того как с клавиатуры введено имя абонента, его необходимо убрать из PAD в PAD плюс 31, чтобы освободить место для ввода номера в PAD. Затем с помощью счетного цикла DO-LOOP просматриваются все позиции справочника для поиска незанятой позиции. Если позиция (i) найдена, то имя абонента и его телефон запоминаются в элементах 0 и 1 i-й строчки массива PHONES и исполнение заканчивается. Если свободная позиция не найдена, т.е. ни в одной позиции массива AVAILABLE не встретился 0, то выдается сообщение "Справочник заполнен". Можно было бы определить слово STOREPHONE и без применения слов, обращающихся со строковыми данными, но это не доставило бы вам никакого удовольствия.
Теперь нам нужно описать слово GETPHONE для вывода обнаруженных в справочнике абонентов и телефонов: : GETPHONE ( --) (Вывести_телефон) ." Имя абонента?" IN$ CR (Вводит строку для поиска) 10 0 DO (Цикл просмотра справочника) DUP I 0 PHONES SWAP (Берет имя в 1-й строке и переставляет его с искомым именем) INSTR IF (Возвращает 0, если не обнаружено в этой позиции, или число, номер позиции, если имя найдено) I 0 PHONES $. 2 SPACES (Печатает полностью найденное имя) I 1 PHONES $. CR (Печатает номер телефона абонента) THEN LOOP DROP ; (Снимает имя и заканчивает программу)
Обратите внимание, что мы используем оператор INSTR не для определения положения символа в строке, а только для того, чтобы отметить, найдена ли искомая строка или нет. Кроме того, заметьте, что поиск возможен по любой части имени абонента. Например, если в справочнике имеются абоненты "Godfrey James" и "Eugene Godfrey", то при поиске по фрагменту "frey" будут выведены на экран оба абонента. И наконец, нам нужно еще слово для уничтожения ненужных записей и освобождения места для новых: : ERASEPHONE ( --) (Стереть_телефон) ." Имя абонента?" IN$ (Вводит строку для поиска) 10 0 DO DUP I 0 PHONES SWAP (Поиск, как в GETPHONE,) INSTR IF (Если найдено -...) 0 I AVAILABLE + С! (Разрешает ввод в эту позицию) 0 I 0 PHONES С! (Стирает имя, делая длину записи равной 0) 0 I 1 PHONES С! (Стирает номер, делая длину записи равной 0) THEN LOOP DROP ; (Снимает имя и заканчивает программу)
Действия слов GETPHONE и ERASEPHONE очень похожи, но в последнем случае найденное имя и телефон не выводятся, а стираются. Это производится с помощью записи 0 в длину строки. Аналогично для освобождения найденной позиции в справочнике в массиве AVAILABLE также записывается 0. Чтобы использовать эту программу, нужно набрать на клавиатуре имя требуемой функции. Но если вы хотите, чтобы программой мог пользоваться любой непосвященный человек, необходимо предусмотреть в ней встроенный контроль ошибок, например, чтобы предотвратить ввод слишком длинного имени или номера телефона. Еще лучше, если программой можно будет пользоваться, вводя всего одно слово - имя программы, а затем одно из трех возможных слов можно будет исполнить по выбору из меню. Поскольку такие улучшения программы дают возможность показать пример использования слов для операций со строками, посмотрим, как они могут быть сделаны.
Вначале мы определим еще одно слово QUITPHONE ( закончить_телефоны), которое нужно для того, чтобы выйти из программы: : QUITPHONE ." ok" CR QUIT : Для исполнения одной из функций программы: трех определенных ранее и QUITPHONE - мы применим вызов по вектору. Итак, нам нужен вектор CREATE CHOICE FIND STOREPHONE , FIND GETPHONE , FIND ERASEPHONE , FIND QUITPHONE ,
(На Форт-83 вместо FIND нужно использовать '.) Ну а как мы будем узнавать, какое слово следует исполнить? Для этого создадим меню, которое будет подсказывать, что нужно ввести "S", чтобы записать номер телефона, "G" - чтобы его выдать, и т.д. Для приема ответа оператора с клавиатуры мы используем слово INKEY$, и тогда, если принят ответ "G", это будет означать, что мы хотим вывести номер телефона. Как же теперь можно исполнить соответствующий элемент вектора? Это можно сделать путем определения позиции буквы ответа (например, "G") в строке, по которой мы будем определять положение функции GETPHONE в векторе. Поэтому нам потребуется строка CHOICES (выбор): $CONSTANT CHOICE$ SGEQ" которая показывает относительное положение слов в векторе.
Еще нам потребуется слово для представления меню: : MENU CR ." НАЖМИТЕ S, чтобы ЗАПИСАТЬ телефон" CR ." НАЖМИТЕ G, чтобы ВЫВЕСТИ телефон" CR ." НАЖМИТЕ Е, чтобы СТЕРЕТЬ телефон" CR ." НАЖМИТЕ Q, чтобы выйти из программы" CR ;
Обратите внимание, как легко составляется меню на Форте. Вот теперь мы можем дать описание главной программы: : FINDPHONE ( --) ( Найти_телефон) MENU BEGIN (Печатает меню и входит в цикл) CHOICES INKEY$ (Вводит адрес строки выбора вариантов; опрашивает клавишу) INSTR (Находит позицию символа выбора в строке CHOICE$ или кладет в стек 0, если не обнаруживает) ?DUP IF (Если найден один из 4 символов...) 1- 2* CHOISE + @ (Находит слово в векторе и исполняет) EXECUTE MENU THEN (Печатает меню и продолжается) 0 UNTIL ; ( Бесконечный цикл; прекращается по QUITPHONE)
Снова заметим, что можно определить PINDPHONE и не пользуясь словами для строковых операций, - через ?KEY, хотя и значительно более длинным и запутанным исходным текстом, но с помощью специально сконструированных слов $CONSTANT, INKEY$ и INSTR это сделано значительно проще. Вы можете вспомнить, что в MMSFORTH есть слово ACASE (гл.7), которое так же успешно можно использовать в данном случае, но в других версиях оно не имеет эквивалентов, между тем слова $CONSTANT, INKEY$ и INSTR относительно несложно определить. Если вы не располагаете версией MMSFORTH, мы советуем вам написать набор необходимых слов для работы со строковыми данными.
Еще один способ написать слово, эквивалентное FINDPHONE, состоит в использовании конструкции IF...THEN, но исходный текст программы получился бы ужасно большим. Поэтому векторное исполнение значительно предпочтительнее. Написание такой же программы на Бейсике и других языках программирования, конечно, возможно, но представляет собой значительно более трудную задачу.
У этой программы есть одно существенное ограничение: вы потеряете справочник, как только выключите машину. В гл.10 мы разовьем эту программу с той целью, чтобы справочник сохранялся на диске.
Упражнения
1. Определите слова NEWLEFT$ и NEWRIGHT$, используя MID$. 2. Определите слово $CHAR. которое должно брать из стека код ASCII и создавать счетную строку из одного символа в PAD, оставляя адрес PAD в стеке. 3. Определите слово 1STCHAR, которое должно возвращать код ASCII первого символа счетной строки, адрес которой находится в стеке. 4. Определите слово $SWAP, которое должно переставлять содержимое двух счетных строк, адреса которых находятся в стеке. Определите это слово на обычном Форте и на MMSFORTH, пользуясь его словами для работы со строками. 5. Найдите слова MMSFORTH, эквивалентные тем, которые были вами определены в упражнениях 1-4. 6. Определите слово с именем BUILD$, которое работает, как BUILDTEST, но, в отличие от него, требует перед собой указания имени (или $адр) строковой переменной, т.е. TESTS BUILDS должно делать то же самое, что делает одно слово BUILDTEST. (Совет: Запомните адрес в стеке возвратов при первом вводе слова.) 7. Опишите слово, которое работает так же, как BUILD$, используя IN$ и $! (Это очень просто.) 8. Модифицируйте программу для создания телефонного справочника, расширив его до 30 записей. Измените программу так, чтобы число записей в справочнике можно было просто изменять, изменяя значение одной константы. Почему это лучше? 9. Определите слово SHOWPHONES. которое должно выводить весь телефонный справочник. 10. Определите слово ERASEALL для стирания всех записей в телефонном справочнике. Оно должно выдавать подсказку: "Вы уверены, что хотите стереть все? (Y/N)", прежде чем перейти к исполнению программы. Для распознавания ответа (Y - да или N - нет) примените функцию INSTR.
Преобразование символьных строк в числа
В гл.5 мы обещали вам, что опишем в данном разделе, как можно вводить числа с клавиатуры по ходу исполнения программы (функция, которая должна быть стандартной, но тем не менее в стандартном Форте отсутствует). Весь фокус в том, чтобы вводить числа в виде символьных строк, а затем преобразовывать их в стеке в числа.
Вот как это делается. Наиболее важное слово для этой операции - CONVERT. Слово CONVERT предполагает наличие двойного (32-битного) слова во втором и третьем элементах стека и адреса на вершине стека. Адрес должен быть на один байт ниже текста символьной строки, представляющей цифры числа (т.е. адрес должен указывать на байт-счетчик строки, хотя слово CONVERT игнорирует байт-счетчик). Слово CONVERT затем преобразует все цифровые символы с начала строки в число (указание знака недопустимо) и добавляет результат к числу двойной длины - "зародышу", находящемуся в стеке. После этого оно оставляет в стеке адрес первого байта, который не является цифрой, и преобразованное число (в виде числа двойной длины) ниже этого адреса. Обычно в качестве "зародыша" для слова CONVERT используется 0 и, кроме того, возвращаемый словом адрес первого нецифрового байта убирается. Слово CONVERT выполняет преобразование в соответствии с текущим значением основания системы счисления, т.е. оно будет делать преобразование также и алфавитных символов, если основание больше 10.
Мы рассмотрим несколько примеров постепенно увеличивающейся сложности, но сначала вы должны ввести слова $IN и $CONSTANT, как мы их определили выше в данной главе. Теперь определим $CONSTANT $NUM 1234X" и введем последовательность операторов 0 0 $NUM CONVERT ... Если мы представим для определенности, что $NUM хранится по адресу 1000, то вы должны увидеть на экране: 1005 0 1234
Адрес байта-счетчика (1000) игнорируется, и преобразование начинается с адреса 1001; после того, как оно завершится, адрес символа "X" (1005) кладется на вершину стека, а число двойной длины 1234, старшая часть которого равно 0, находится ниже этого адреса. Теперь введите определение : SILLY 0 0 32 WORD CONVERT DROP DROP . ; и после этого SILLY 1596 Тогда вы увидите на экране число 1596, выведенное из стека. Если вы попробуете SILLY -1256 на выходе будет получен 0, потому что слово CONVERT не воспринимает знак "-" и не делает преобразование.
Слово SILLY (глупо) и в самом деле глупое, поскольку оно вводит числа прямо в стек. Вот несколько более полезное слово: : #IN 0 0 $IN CONVERT DROP DROP ;
Слово #IN (число_ввод), которое определено в MMSFORTH, приостанавливает исполнение и, как и $IN, позволяет вам ввести символьную строку, а затем оставляет в стеке "величину" строки в виде числа одинарной длины. Вы должны самостоятельно определить слово D#IN, которое позволит вводить числа двойной длины.
Но есть одно затруднение в приведенном определении слова #IN; если вы делаете ошибку при вводе и введете первый символ, например "X", то слово #IN возвратит 0, чего вы, конечно, не хотели- В связи с этим нужно в слове #IN предусмотреть проверку на наличие ошибки. Вот как это можно сделать. Забудем слово #IN: FORGET #IN и введем новое определение: : #IN ( -- n) (число_ввод) BEGIN (Начинает условный цикл) $IN DUP 0 0 ROT (Вводит строку, делает ее копий и подготавливает ее для CONVERT.) CONVERT (Выполняет преобразование) SWAP DROP (Удаляет старшую часть числа) ROT 1+ = (Если возвращаемый адрес такой же, как адрес первого символа, то этот символ - не цифра) WHILE ." REDO " DROP (Убирает ввод и запрашивает новый) REPEAT ; ( Если цифра, то продолжает преобразование) Теперь введите HEX #IN и ответьте на предложение сделать ввод (?) строкой "XYZ". Вы увидите сообщение "REDO" (введите снова) и новый знак вопроса. На этот раз напечатайте в ответ "12AB", а затем выведите результат, вводя. (точку). Попробуйте использовать это определение #IN с другими числами. Не забывайте возвращаться к десятичной системе счисления после каждого исполнения слова.
Остается еще одна нерешенная проблема: слово #IN не понимает знак "-", помещенный слева от строки символов, если преобразуется отрицательное число. Забудем старое определение #IN: FORGET #IN и рассмотрим определение, которое будет распознавать отрицательные числа: : #IN ( -- n) (ввод_числа) BEGIN (Начинает условный цикл) $IN DUP 1+ С@ 45 = (Не является ли первый символ знаком "-"?) DUP >R (Запоминает флаг в стеке возвратов) IF 1+ THEN (Добавляет 1 к $адр, пропуская "-", если он есть) DUP 0 0 ROT CONVERT (Выполняет преобразование) ROT ROT (Помещает число двойной длины на вершину) R> IF DNEGATE THEN (Если найден знак "-", делает отрицательным) DROP (Удаляет старшую часть числа) SWAP ROT 1+ = (Если возвращаемый адрес такой же, как адрес первого символа, то строка не цифровая) WHILE ." REDO " DROP (Убирает ввод и запрашивает новый) REPEAT ; (Если цифра, то продолжает преобразование)
В этой версии слова # IN сначала просматривается, есть ли в самом начале строки знак "-" (минус); если есть, то адрес начала строки увеличивается на 1, т.е. пропускается символ "-" и в стек возвратов помещается флаг истина. После преобразования число двойной длины превращается в отрицательное, если флаг в стеке возвратов - истина. За исключением той части, где делается проверка на знак "-", определение совпадает с предыдущим. Вы понимаете, что это определение важное и, возможно, включите его в вашу версию Форта. И кроме того, теперь вы лучше понимаете, как работает слово CONVERT.
В большинстве версий Форта есть еще одно слово, которое по своему действию похоже на CONVERT. Это слово NUMBER.
Слово NUMBER не стандартное, но оно входит в контролируемый список, т.е. определено, как оно должно действовать, если имеется. Но беда в том, что некоторые реализации Форта не следуют стандартам, хотя объявляется, что следуют. Согласно стандарту слово NUMBER предполагает в стеке наличие адреса строки, оно преобразует эту строку в число двойной длины так же, как и CONVERT, но, во-первых, не требует в стеке "числа-зародыша", во-вторых, не возвращает в стек адрес первого не цифрового символа и, в-третьих, в преобразуемой строке может находиться знак "-", в таком случае в стек помещается отрицательное число. Вот определение слова NUMBER, соответствующее его описанию в стандарте: : NUMBER ( $адр - d или n) 0 0 ROT DUP 1+ С@ 45 = DUP >R + CONVERT DROP R> IF DNEGATE THEN ; Вы должны сами понять, как это определение работает, сравнивая его с последним описанием слова #IN.
Некоторые версии не соответствуют стандарту, в частности они проверяют символ; следующий после цифровой подстроки, не является ли он десятичной точкой, и если является, то оставляют в стеке результат в форме двойного числа. В противном случае они выдают результат преобразования в виде числа одинарной длины. Определением такого слова NUMBER может быть : NUMBER ( $адр - d или n) 0 0 ROT DUP 1+ С@ 45 = DUP >R + CONVERT С@ 46 = 0= IF DROP THEN ( Если не ".", то убрать) DROP R> IF DNEGATE THEN ;
Нетрудно видеть, что последние два определения очень похожи, но в последнем случае результат, возвращаемый словом CONVERT, проверяется: не содержится ли в нем десятичная точка. Если не содержится, то старшая часть числа убирается из стека, результат выдается как одинарное число. В некоторых версиях Форта десятичная точка допускается в любом месте цифровой строки. При этом они могут принимать или не принимать ее во внимание для того, чтобы выдавать одинарное или двойное число. Кроме этого, можно устанавливать переменную пользователя (т.е. записывать "1"), чтобы отметить, что обнаружена десятичная точка, тогда значение этой переменной дает возможность пользователю, если он желает, убирать из стека старшую часть 32-битового числа (как это делает MMSFORTH).
Если версия содержит слово NUMBER, то оно является важной частью интерпретатора Форт-системы. Вспомните, что анализ входного потока производится словом WORD, которое оставляет в результате разбора символьные строки. При этом именно слово NUMBER используется для того, чтобы цифровые строки превращать в числа, которые кладутся в стек. Эта проблема обсуждается в гл.15. Чтобы еще лучше оценить слова CONVERT и NUMBER, проделайте несколько упражнений.
Упражнения
1. Опишите три варианта слова D#IN, которые аналогичны трем вариантам слова #IN, данным выше, но оставляют в стеке двойное число. 2. Определите версию слова #IN, которая оставляет в стеке либо двойное, либо одинарное число, в зависимости от того, заканчивается или не заканчивается строка десятичной точкой. Определение должно допускать ввод отрицательных чисел " давать сообщение "Введите снова", если строка не содержит цифр. 3. Определите слово #IN, используя слово NUMBER. Это определение не должно делать проверки строки на нецифровые символы. 4. Это и последующие упражнения должны убедить вас в том, что и в Форте можно пользоваться инфиксной нотацией. Если вы определите слово : SILLY 32 WORD NUMBER DROP . ; то в результате SILLY 526 будет выведено число 526.
Здесь слово WORD ввело цифровую строку. Слово NUMBER преобразовало ее в число и, естественно, напечатало. Учитывая этот пример, напишите слово PLUS, которое будет брать из входного потока число, которое следует после него, преобразовывать его в число одинарной длины и складывать с числом, которое уже находится в стеке. Таким образом, 5 PLUS 6 . должно вывести на экран 11. Может быть. вы определите слово INFIX для упрощения определения слова PLUS, а также слов, которые встретятся в следующих упражнениях. 5. Теперь определите еще четыре слова: MINUS (минус), TIMES (умножить), DIVIDEDBY (разделить_на) и EQUALS (равно), которые позволят использовать Форт в качестве калькулятора (правда, только для целых чисел), т.е. в результате операций 3 PLUS 9 TIMES 2 DIVIDEDBY 4 EQUALS (3 плюс 9 умножить_на 2 делить на 4 равно), вы должны получить на экране 6.
Применяя слова WORD, NUMBER, и др., вы можете полностью переделать саму природу языка. На практике применение подобных приемов позволяет написать на языке Форт интерпретатор другого языка, например, Бейсика.
Выводы
Одна из распространенных претензий к языку Форт состоит в том, что он якобы имеет очень слабые возможности для работы с символьными строками. Не воздерживаются от критики даже те программисты, которые работают на языке Форт, когда они пишут что-либо о языке. Мы надеемся, что вы уже убедились в несостоятельности подобной критики, форт может быть столь же эффективен в обработке строковых данных, как и другие языки. С подобной критикой мы уже встречались в связи с работой с числами с плавающей запятой. Важная цель стандартов языка Форт состоит в том, чтобы не навязывать жесткие ограничения, которые могут препятствовать его развитию и гибкости. С другой стороны, отсюда вытекает, что каждый пользователь должен приспособить язык, чтобы он соответствовал более общим требованиям, и это задает программисту дополнительную работу. Что касается авторов, то мы считаем недостатком стандарта то, что он не определяет лучшие возможности работать с числами с плавающей запятой и символьными строками.Можно с этим соглашаться или не соглашаться, но расширением словаря различных версий Форта может значительно увеличить производительность программистов. Почти все работающие на Форте быстро создают свои пакеты программ и слова, которыми можно воспользоваться при необходимости. Одна из целей данной книги - помочь вам создать такую коллекцию слов и программ; вы сами можете включить некоторые слова из этой главы в свою коллекцию.
Вы уже, вероятно, осознали, что ввод данных с клавиатуры, как числовых, так и текстовых, связан с неудобствами. Например, составленный вами выше телефонный справочник стирается, когда вы выключаете компьютер. В связи с этим нам нужен какой-либо способ запоминать строки и числа на диске. Выход из положения состоит в записи в блоки на диске, которые являются предметом рассмотрения следующей главы.
Хранение программ и данных
Объем памяти микрокомпьютера обычно не достаточен, чтобы хранить в ней все данные и программы, которые нам нужны. Поэтому необходимо использовать некоторое устройство массовой памяти. В микрокомпьютерах в большинстве случаев применяется либо кассетный магнитофон, либо, что более распространено, магнитный диск. Большинство языков программирования пользуется для хранения программ и данных именованными файлами, управление и обращение с которыми обеспечивается дисковой операционной системой. Как вы уже знаете. Форт сильно отличается от других языков тем, что сохраняет программы и данные в блоках.
Чтобы понять дальнейший материал этой главы, вы должны узнать, что такое файлы и как они используются в типичной дисковой операционной системе, и немного о структуре диска. Файл попросту представляет собой совокупность двоичных данных, организованных в последовательность байтов, как в памяти ЭВМ. Файл может содержать алфавитно-цифровой текст в виде кодов ASCII (например, текст этой главы был сохранен в файле после того, как его ввели с помощью процессора текста), программу, которая также часто сохраняется в виде кодов ASCII, и данные, которые могут быть записаны либо кодами ASCII, либо в двоичной форме. Файл можно загружать, если это программа, например, на Бейсике либо к нему может быть организован доступ из программы (если это данные).
Если вы введете команду "DIR" с клавиатуры, то большинство операционных систем покажет вам справочник (директорию) файлов на диске. То, как организуется хранение файла на диске, находится под контролем операционной системы.
Данные записываются на диске с помощью магнитных головок, входящих в привод диска) вдоль концентрических окружностей, называемых дорожками. Каждая дорожка разделяется на несколько дуг, называемых секторами, в каждом секторе сохраняется определенное число байтов (например, в IBM PC с DOS 2.1 на сектор приходится 512 байтов и 9 секторов на дорожку). Компьютер считывает с диска сектор за сектором. Сектор - это минимальное количество информации, которое за один раз может быть считано компьютером.
В свою очередь, файл хранится в одном или нескольких секторах. Секторы в файле могут следовать не обязательно подряд, но операционная система прослеживает, какие секторы и в какой последовательности соответствует каждому файлу. Если часть файла были изменена, то соответствующие ей секторы перезаписываются и изменяются.
В системе Форт, в противоположность описанному, данные и программы хранятся на дисках в блоках, содержащих по 1024 байта; так определяется способ хранения в стандартах. Каждый блок может состоять из одного или более секторов (Блоки также называют экранами, имея в виду, что находящаяся в блоках информация может быть очень удобно представлена на экране дисплея в виде 16 строк по 64 символа в строке. Некоторые считают, что экран означает любой блок, который преобразован так, что его можно вывести на экран видеодисплея, даже если он содержит, например, управляющие символы.) Как мы уже говорили раньше, Форт может работать либо "под операционной системой" (т.е., например. Форт вызывается в операционной системе СР/М или MS-DOS, если ввести команду FOKTH), либо может быть сам себе операционной системой; в этом случае он готов к работе сразу же после того, как машина включена и "загружена". В последнем случае блоки обычно начинаются с минимального номера на первом приводе дисковода и их последовательная нумерация ведется до самого большого номера сектора последнего привода
Хотя многие реализации Форта не работают с файлами операционной системы, имеется возможность организовать блоки Форта в виде файлов данных и программ, подобно тому как операционная система создает файлы из секторов. (В MMSFORTH имеется программа-утилита, которая позволяет передавать данные из операционной системы в Форт и обратно.) Взаимодействие Форта с операционной системой - это особая проблема, решение которой зависит от конкретного оборудования ЭВМ и операционной системы. Мы не будем обсуждать эту проблему, но покажем вам, как надо работать с программами и данными, которые хранятся в блоках Форта.
Мы также рассмотрим некоторые простейшие способы использования нескольких блоков подобно файлу. Сначала мы рассмотрим запись-чтение программы, а затем хранение и запись данных. Вы убедитесь, что отсутствие файлов не является серьезным препятствием для форта и блоки имеют даже некоторые преимущества.
Вывод листинга программы и загрузка
Вы уже видели в гл. 1, как можно вывести листинг блоков исходной программы (LIST) и загрузить блок (LOAD) и, может быть, уже редактировали их, пользуясь либо редактором вашей системы или редактором, описанным здесь в гл. 12. Сейчас мы рассмотрим более подробно сначала вывод листинга, а затем загрузку блоков. Для повторения пройденного введите 25 LIST тогда на экране вы увидите текст блока номер 25 в виде последовательности из 16 строк по 64 символа в каждой, пронумерованных от 0 до 15. Во многих версиях Форт имеется возможность вывести листинг нескольких блоков. Если в MMSFORTH ввести 25 6 PLISTS то будет выведен листинг шести блоков, начиная с 25-го по 30-й (В некоторых версиях синонимом этого слова является слово SHOW.) Конечно, все они быстро пробегут по экрану вверх. Слова типа PLISTS предусмотрены для вывода нескольких блоков на принтер. В некоторых версиях (в частности, в MMSFORTH) есть и другие слова, с помощью которых можно получить изящно оформленный листинг программы.
Переменная SCR (screen - экран) используется Форт-системой для вывода листинга блоков. Слово SCR имеется в Форт-79, в Форт-83 это слово необязательное, но в большинстве версий оно также имеется, и в нем запоминается номер последнего блока, который был выведен. Так, после 25 LIST SCR @ . выведет на экран 25. В различных версиях имеются слова для повторения вывода листинга блока, вывода предыдущего и следующего блока. Если назвать эти слова L, LL (вывести_последний) или LN (вывести_следующий), то они могут быть определены следующим образом: : L ( - ) SCR @ LIST ; : LL ( - ) SCR @ 1- LIST ; и : LN ( - ) SCR @ 1+ LIST ; Если повторять ввод LN, то будут выводиться последовательно несколько блоков.
Во многих версиях Форта есть слово INDEX, которое показывает первые строки последовательности экранов (индексные строки, в которые обычно записывают пояснения о назначении экранов). В некоторых системах перед словом INDEX должны быть указаны число просматриваемых экранов и номер начального экрана (так сделано в MMSFORTH), в других - номера начального и конечного экранов. Таким образом, в зависимости от версии 20 6 INDEX или 20 25 INDEX покажут первые строки блоков 21, 22, 23, 24 и 25.
Загрузка блоков словом LOAD так же проста, как и вывод блоков. Если в блоке содержится текст, который может быть введен с клавиатуры, то этот же текст может быть введен и из блока. Текст будет интерпретироваться точно так же, как если бы он был введен вручную. Так, если блок содержит определения слов или текст, который должен быть сразу исполнен, то 25 LOAD введет текст. Очевидно, слово LOAD можно также использовать, если оно записано внутри блока. Пусть, например, в блоке 25 содержатся определения, которые являются частью программы, продолжающейся в блоке 30. Если в блоке 25 включен текст 30 LOAD то, когда будет встречен текст 30 LOAD, то будет загружен блок 30, после чего произойдет возврат к блоку 25, чтобы продолжить ввод того, что еще могло остаться в этом блоке.
Обычно программа располагается на последовательно расположенных блоках. В большинстве версий Форта есть нестандартные слова для загрузки последовательности блоков. В MMSFORTH так же, как слово PLISTS выводит, слово LOADS загружает последовательность экранов. Так, если программа находится в блоках с 25-го по 30-й, то их можно загрузить путем ввода с клавиатуры: 25 6 LOADS Слово LOADS можно определить следующим образом: : LOADS ( п1 п2 - ) OVER + SWAP DO I LOAD LOOP ;
Вам должно быть понятно, как оно работает. (В некоторых версиях используются совершенно другие слова для загрузки, о чем будет сказано дальше.) Слово THRU можно определить так: : THRU ( п1 п2 - ) 1+ SWAP DO I LOAD LOOP ;
Очевидно, что можно в конце каждого блока последовательности из нескольких блоков помещать номер следующего загружаемого блока со словом LOAD.
Например, в конце блока 25 нужно поместить 26 LOAD, в блоке 26 - 27 LOAD и т.д. Но во многих версиях предусмотрен более простой способ. Нестандартное слово -> (следующий_блок) означает "загрузить следующий блок". Как только будет встречено это слово, будет загружаться следующий блок, даже если после него в предыдущем блоке что-либо осталось. Мы узнаем вскоре, как определить это слово.
Слово EXIT производит особое действие, когда оно встречается в блоке (вне определения через двоеточие). Оно прекращает загрузку блока. Поэтому, если вы хотите загрузить только часть программы, вы можете вставить слово EXIT перед той частью программы, которая должна быть проигнорирована.
Кроме слов LIST и LOAD вы должны познакомиться со словом COPY (оно также нестандартное, но имеется во многих версиях). Слово COPY используется в такой форме обращения: n1 n2 COPY чтобы скопировать блок n1 в блок n2. В зависимости от версии Форта после слова COPY может потребоваться слово FLUSH, которое указывает, что произведенные в блоке изменения следует сохранить. Слово FLUSH в MMSFORTH включать в программу копирования обязательно. Слово COPY удобно для перемещения программы в любое место. Мы предлагаем вам дать определение этого слова.
Слово BLK - это переменная, в которой записывается номер блока, загруженного последним (если ввод производится с клавиатуры, то в BLK записан 0, это означает, что в блоке 0 не может быть записан исходный код программы). Вы можете определить слово : .BLK ( - ) BLK @ U. : IMMEDIATE которое будет показывать на экране номер блока, если он был загружен. Слово IMMEDIATE обеспечивает немедленное исполнение слова BLK, независимо от того, включено оно или не включено в определение-двоеточие. (Мы рассмотрим слово IMMEDIATE более подробно в гл. 15.)
При загрузке блоков используется еще одна переменная >IN. В гл. 9 вы узнали, что >IN указывает на соответствующий байт во входном буфере, в который поступает входная информация (более подробно об этом слове смотрите в гл. 15).
Если содержимое BLK не равно 0, то это означает, что ввод производится с диска, тогда >IN указывает на номер байта в блоке, из которого приходит ввод. В некоторых версиях Форта имеется слово \ (обратная косая черта), которое используется для пропуска оставшейся части строки, т. е. если встречается \, то остаток строки при вводе игнорируется, поэтому этот знак можно использовать для помещения комментариев. Приведем определение этого слова: : \ ( - ) >IN @ 64 / 1+ 64 * >IN ! ; IMMEDIATE
Слово \ заставляет переменную >IN указывать на начало следующей строки, независимо от того, встречается ли оно в определении через двоеточие или самостоятельно. Теперь мы можем определить слово --> : : --> ( -- ) 0 >IN ! 1 BLK +! ; IMMEDIATE
Вам должно быть понятно, как оно работает.
Упражнения
1. Предположим, что вы определили слово : =-> ( -- ) BLK @ 1+ LOAD ; IMMEDIATE Чем его действие будет отличаться от действия слова --> , определенного выше? (Указание: рассмотрите, что произойдет после того, как будет загружен следующий блок.) 2. Дайте новое определение слова --> под именем N->, которое не просто загружает следующий блок, а, кроме того сообщает на экране "Блок n загружен", где n - номер блока, который был загружен. Не пользуйтесь для определения словом -->! 3. Определите слово LISTS, которое будет выводить блоки, сообщая в начале каждого блока его номер "Блок ххх"", где ххх - номер блока, если задан номер начального блока и число блоков, которое нужно вывести, т. е. блоки должны выводиться в виде Блок 25 1 ...... 2 ...... 3 ...... Для вывода каждого блока потребуется 17 строк. На обычной бумажной странице можно напечатать 66 строк- Определите слово LISTS так, чтобы между листингами экранов было такое количество пустых строк, чтобы три экрана занимали ровно 66 строк. 4. Иногда требуется, чтобы сообщение об ошибке выдавалось во время загрузки блока и указывало, в каком месте в блоке обнаружена ошибка. Определите слово с именем ... (уголок), которое указывало бы на местоположение ошибки, т.
е. оно должно выдавать сообщение типа Block 25 Line 3 Character 53 (Блок Строка Символ) Вы можете вставить знак " в любом месте, где вы предполагаете ошибку. Для этого вам потребуется использовать слово >IN, чтобы определить номер строки и номер символа в строке. Используйте слово IMMEDIATE. 5. При отладке программы возникает необходимость неоднократного изменения содержимого экранов. Если первое слово, которое определено в программе, : TASK, то осуществить это проще. Пусть ваша программа начинается в блоке 20, вы изменили ее и хотите удалить старую версию программы. Что вам нужно напечатать на клавиатуре, для того чтобы это сделать?
Скрытые блоки
В некоторых версиях Форта реализована идея скрытых блоков. Очевидно, возможные реализации могут быть различными, но одна из них такова: в четных блоках находится код, который нужно загрузить, в нечетные блоки записаны комментарии о содержимом следующих блоков. Если вы попытаетесь загрузить скрытый блок, то не сможете это сделать - произойдет ошибка. Кое-кому может показаться, что применение скрытых блоков - это спорный вопрос, поскольку дисковое пространство должно быть в два раза больше и, кроме того, нельзя одновременно видеть на экране блок, содержащий код программы, и скрытый блок. Но некоторые.программисты считают такую методику заслуживающей внимания и в некоторых случаях полезной, например если нужны очень подробные комментарии к программе. Вместо того чтобы рассказывать вам, как это делается, давайте реализуем скрытые блоки в следующих упражнениях.
Упражнения
1. Определите слово SLOAD, взяв за основу слово LOAD, которое, если указывается четный номер блока, дает сообщение "Не могу загружать четные блоки" и возвращает управление клавиатуре. Дайте определение, используя слова ABORT" и." (точка-кавычка). 2. Определите слово SLIST, используя слово LIST, таким образом, чтобы при запросе на выдачу блока с четным номером выводился бы следующий блок с нечетным номером. (Разумеется, при запросе на выдачу блока с нечетным номером нужно.
чтобы он выводился.) 3. Определите слово VIEW, которое должно выводить скрытый блок, связанный с загружаемым блоком, т.е. практически выводить листинг скрытого блока. Таким образом, если вводится 20 VIEW или 19 VIEW, будет выводиться скрытый блок 19. 4. Определите слово SLISTS, которое, если задан номер блока и количество блоков, которое должно быть выведено, будет выводить четные блоки, на которых записан исходный код программы. При этом первый выводимый блок должен быть такой же, как в случае слова LIST, определенного в упражнении 2. Перед каждым блоком должен быть указан номер, и если назначено вывести их на устройство печати, то нужно, чтобы в 66 строках помещалось ровно три экрана. Так, например, 19 5 LISTS должно выводить блоки 20, 22, 24, 26 и 28. 5. Определите слово VIEWS, аналогичное слову SLISTS, из упражнения 3, но выводящее скрытые блоки, т. е. если ввести 20 5 VIEWS то будут выведены блоки 19, 21, 23, 25 и 27. 6.Дайте новое определение слова --> с именем S-->, которое вызывает загрузку следующего блока, пропуская имеющиеся скрытые блоки. Если исходный код размещается у вас на четных и нечетных блоках, то реализовать идею скрытых блоков нельзя - но крайней мере, не прибегая к сложным программным ухищрениям, которые допускали бы использование "обычных" блоков с исходным кодом программы.
Загрузка экранов
Наиболее часто Форт подвергается критике за то, что невозможно загрузить Форт-программу из файлов с помощью операционной системы. Это означает, что вы не можете загрузить программу, вводя LOAD "PROG.BAS" как это делается в Бейсике. (Хотя мы уже выше упоминали о существовании некоторых версий Форта, которые обеспечивают возможность загрузки Форт-программы из файла операционной системы.) Обычно программист должен сам следить, в каких блоках записана его программа, а также за последовательностью блоков, из которых она должна быть загружена. Как правило, для этого используется так называемый загрузочный экран, или загрузочный блок.
Загрузочный экран предназначен для того, чтобы связать имя программы с блоками, в которых она размещается, с целью осуществления загрузки программы по имени. Реализация загрузки экранов может иметь различную форму.
Проще всего загрузить программу, если все относящиеся к ней блоки соединяются словом -->. Предположим, что у вас имеется несколько наборов расширяющих слов, которые можно загружать по мере необходимости. Допустим, это слова для работы со строками, слова арифметики с плавающей запятой, декомпилятор и графические слова. В этом случае вы можете скомпоновать блок, в котором содержатся следующие определения: : STRINGS 30 LOAD ; (Символьные строки) : FLTPT- 40 LOAD ; (Арифметика с плавающей запятой) : DECOMP 50 LOAD ; (Декомпиляция) : GRAPHICS 60 LOAD ; (Графика)
Если вы загрузите экран, на котором помещены эти определения, то вы сможете вызывать по выбору любой из имеющихся наборов, вводя его имя. Например, слово STRINGS загрузит слова для работы со строками. Разумеется, если блоки в STRINGS не объединены с помощью -->, то можно определить слово: : STRINGS 20 5 LOADS : или : STRINGS 20 LOAD 23 LOAD 28 LOAD : если программа находится не в последовательных блоках. Идея состоит в том, чтобы определить слово LOADS, которое сильно отличается от рассмотренного нами выше слова LOADS. Новое слово LOADS позволяет дать определение слова, которое после его ввода будет загружать блок. Вот определение этого слова: : LOADS ( n - ) CREATE , DOES> @ LOAD ;
Если это определение использовать, например, так: 30 LOADS STRINGS 40 LOADS FLTPT 50 LOADS DECOMP 60 LOADS GRAPHICS то слово LOADS определит слова STRINGS, FLTPT, DECOMP и GRAPHICS. Поэтому если ввести одно из этих слов, то будет загружен соответствующий ему блок.,Пока вы еще не знаете слово DOES>, поэтому вам может быть не вполне понятно, как работает слово LOADS, но если вы просмотрите гл. 6, где рассматривается слово CREATE, то поймете, как в данном случае работает DOES>. Вкратце, слова, заключенные между CREATE и DOES>, определяют, что происходит при исполнении слова LOADS, в то время как слова, находящиеся между DOES> и ;, описывают, что должны делать вновь определенные слова.
В данном случае они извлекают число - номер блока из слова, подобного переменной, и затем загружают соответствующий блок. Конструкция CREATE...DOES> порождает слова, которые создают новый класс слов, о чем мы более детально будем рассказывать в следующей главе. Вернемся, однако, к загрузке блоков. Мы можем создать блок-справочник, который будет напоминать о названиях программ и наборов расширяющих слов. Предположим, что вы хотите иметь справочник блока 10, причем загрузочный экран имеет номер 20. Последнее определение в блоке 20 пусть будет таким: : DIR 10 LOAD : при этом пусть в блоке 10 находится ." Расширения, определенные в блоке 20" CR CR ." STRINGS" CR (Символьные строки) ." FLTPT" CR (Арифметика с плавающей запятой) ." DECOMP" CR (Декомпиляция) ." GRAPHICS" CR (Графика)
(В Форт-83 вместо оператора." используется.( ). Если ввести DIR, то после загрузки загрузочного экрана вам будут представлены возможности выбора одного из наборов расширяющих слов. В некоторых версиях Форта, в частности в MMSFORTH, вам предоставляется возможность указать, какой из блоков Форта нужно загрузить. Если на одном блоке не хватает места для справочника, нужно указать, как связаны вместе все блоки.
Можно работать с блоками многими другими способами. Некоторые из них мы попробуем применить в упражнениях.
Упражнения
1. Пусть у вас имеется игровая программа, которая начинается в блоке 100. Каким образом можно использовать константу, чтобы можно было загрузить программу, вводя PACFORTH LOAD ? 2. Определите новое слово для загрузки GET, которое загружало бы программу с помощью GET PACFORTH. 3. Если блоки программы расположены не последовательно, то можно для хранения их номеров использовать массив, первым элементом которого будет число блоков. Так. если PACFORTH находится в блоках 30, 33, и 36, то массив можно определить так: CREATE PACFORTH 3 , 30 , 33 , 36 , Опишите слово LOADIT так, что если вы вводите PACFORTH LOADIT то будет загружена программа PACFORTH. (Указание: используйте цикл DO...LOOP.) 4.
Большим неудобством в Форте может быть слежение за тем, где находятся программы, особенно если они не находятся в последовательных блоках. Определите слово SHOWBLOCKS (показать_блоки) так. что если вы вводите PACFORTH SHOWBLOCKS то увидите 30 33 36 ok, 5. Для слежения за номерами блоков нескольких программ можно также использовать массив типа того, что мы применили в упражнении 3. Предположим, что у вас имеются массивы номеров блоков программ 1PROG. 2PROG и 3PROG. Создайте массив CREATE #PROGS 5 , 1PROG , 2 PROG , 3PROG , Теперь определите слово ?BLOCKS (в_каких_блоках?) так, что если вы напечатаете #PROGS ?BLOCKS то увидите что-нибудь вроде 1 23 25 26 27 2 31 33 39 3 55 56 57 58 60 Надеемся, что в действительности вы никогда не устроите такой беспорядок в ваших блоках. 6. Определите слово LOADEM так, что если вы напечатаете SPROGS 3 LOADEM то будет загружена программа 3PROG из предыдущего примера.
Эти упражнения могут дать вам первоначальную идею для организации блоков в файлоподобные структуры. Мы рассмотрим детальнее данный вопрос в данной главе дальше.
Работа с содержимым блоков
Очевидно, что нужно иметь способ, позволяющий манипулировать содержимым любого блока. Это должен уметь делать редактор. Возможность управления содержимым блоков требуется также для извлечения данных из блоков. Если вы выбираете блок (n), который содержит какой-то текст, например определения слов, и введете n BLOCK 1024 TYPE то увидите содержимое этого блока, правда выведенное не очень красиво. Слово BLOCK переносит содержимое блока в буфер блока область памяти размером 1024 байта, и оставляет адрес буфера в стеке. После этого 1024 TYPE печатает на экране содержимое буфера. Теперь вы можете делать всевозможные полезные действия с содержимым того блока, которое было перенесено в память словом BLOCK. Если напечатать n BLOCK CR 64 TYPE то вы увидите на экране первые 64 символа из блока n, т.е. его первую строку. Последовательность действий n BLOCK 64 + CR 64 TYPE вызовет печать второй строки.
Находящаяся в блоке информация может быть использована, если ее предварительно поместить в память словом BLOCK, с этого момента с ней можно обращаться как с данными, находящимися в памяти. Например, вот определение слова LIST: : LIST ( n - ) CR BLOCK 16 0 DO I 2 .R SPACE DUP I 64 * + 64 -TRAILING TYPE CR LOOP DROP ; Вам нетрудно понять, как оно работает. Мы должны здесь упомянуть, что в некоторых версиях Форта имеются операторы, позволяющие обмениваться содержимым блоков не в буферах, а в специально выделенной области памяти. Так, в MMSFORTH 50 PAD RBLK считает в память информацию, содержащуюся в блоке 50, помещая ее в память, начиная с адреса PAD, в то время как 50 PAD WBLK запишет 1024 байта из памяти с адреса PAD в блок 50. С помощью этих двух слов в MMSFORTH описываются слово BLOCK и некоторые другие слова для обращения с буферами блоков.
Предлагаем вам поэкспериментировать с внесением изменений содержимого блоков в нескольких упражнениях, но сначала мы должны описать, как работает блочный буфер. Выберите два блока для вывода (назовем их n1 и n2). Теперь напечатайте n1 LIST, а после этого n2 LIST. Понаблюдайте за поведением дисковода, следя за обращением к нему, и снова напечатайте n1 LIST. Вывод листинга произойдет без участия дисковода. Почему? В Форт-системе имеется по крайней мере два буфера блока (а в некоторых и больше). Один из блоков вы загрузили в буфер с помощью nl LIST, другой командой n2 LIST - в следующий буфер. Когда во второй раз вы напечатали nl LIST, Форт быстро определил, что этот блок уже находится в памяти, поэтому он не сделал попытки загрузить его снова с диска. Если блок уже помещен в память словами BLOCK, LIST, LOAD и т. п., то он будет загружаться только в том случае, если его еще нет в памяти. Подобное использование буферов блока называется иногда хранением в виртуальной памяти, поскольку в некотором смысле диск является частью памяти компьютера. Способ использования буферов диска называют также кэшированием т.е. хранением в памяти часто используемой информации с диска, не без загрузки ее с диска каждый раз, когда требуется доступ к этим данным.
Кэш-диск сокращает число обращений к дисковому устройству, значительно ускоряя исполнение программы, когда одни и те же данные требуются многократно. Во многих версиях Форта число буферов диска увеличено минимум на 2. Если вам придется работать с большим количеством часто используемых данных и вам не хватит двух буферов блоков, вы можете увеличить их число. Попробуем сделать несколько экспериментов, манипулируя с информацией из блоков, чтобы проследить, как используются буферы. Выберите три блока, содержимым которых вы не дорожите, и поэтому их можно переписывать (мы назовем их n1, n2 и n3 и будем считать, что у нас есть только два буфера блоков). Теперь попробуйте сделать 10 n1 BLOCK ! а потом n1 BLOCK @ . выведет на экран число 10. Первый элемент в блоке n1 вы изменили на 10. Теперь напечатайте 20 n2 BLOCK ! после этого 30 n3 BLOCK ! а затем n1 BLOCK @
Число 10, которое вы занесли в первый элемент блока n1, куда то пропало! Что произошло? Когда вы поместили число 30 в блок n3, то поскольку блок n3 был загружен с диска в первый блочный буфер, то его содержимое наложилось на содержимое ранее находившегося здесь блока n1. Поэтому перед извлечением первого элемента блока n1 произошла перезагрузка n1, но с его исходным содержимым, а не с числом 10 в первом элементе. Когда в блочный буфер вводится новый блок, то он попадает в последний использованный буфер. Как же в таком случае сохранить измененные данные из буфера на диске? Изменения в буферах диска могут быть сделаны постоянными, если вы используете слово UPDATE. Попробуйте проделать эксперимент заново, но после каждого изменения, внесенного оператором записи !, напечатайте UPDATE. Теперь, когда вы во второй раз введете n1 BLOCK @ . то увидите, что в первом элементе блока n1 было запомнено число 10, UPDATE помечает, что буфер нужно сохранить на диске, чтобы в противном случае на его содержимое не могло наложиться содержимое другого блока. Другими словами, оно делает произведенные изменения постоянными, так что если используемый в последний раз буфер будет переписываться, то перед этим его содержимое должно быть сохранено на диске.
Любые изменения содержимого буфера блока должны быть сделаны постоянными, прежде чем он снова будет использован, с помощью слова UPDATE. Слово UPDATE работает очень быстро, поэтому нет никаких препятствий применять его почаще, не опасаясь перестараться. Но что будет с блоком, буфер которого не был использован вторично до выключения компьютера, а изменения были объявлены постоянными? Так как блочный буфер сохраняется на диске только в том случае, когда он используется повторно, то внесенные изменения будут утрачены. Можно заставить Форт-систему записать на диск все буферы, объявленные измененными, пользуясь словами FLUSH или SAVE-BUFFERS, которые либо являются синонимами, либо очень близки по назначению в зависимости от версии Форта. Поэтому после того, как закончилась программа или процедура, которая произвела изменения в блоке, нужно использовать одно из этих слов. Между словами FLUSH и SAVE-BUFFERS имеются некоторые тонкие различия, зависящие от версии языка. В Форт-83 оба слова производят запись содержимого всех обновленных блоков на диске и снимают признак внесения изменений, но если слово FLUSH отменяет приписывание буферов конкретным номерам блоков, то слово SAVE- BUFFERS может делать или не делать это в зависимости от реализации. Поэтому если используется слово SAVE-BUFFERS, то можно изменить содержимое буфера (не используя слова BLOCK для загрузки нового блока), снова объявить изменения постоянными словом UPDATE и сохранить буферы на диске словом FLUSH. Вы можете экспериментально установить, как работает ваша версия Форта со словом SAVE-BUFFERS. В Форт-79 обязательным является только слово SAVE-BUFFERS, причем стандарт не оговаривает, должно ли оно отменять назначение буферов конкретным блокам. В большинстве версий Форт-79 имеется также и слово FLUSH, которое практически является синонимом SAVE-BUFFERS. Незначительные различия этих слов в Форт-83 в большинстве случаев не имеют значения, и ими можно пренебрегать.
Можно ли после того, как вы произвели изменения в блоках и объявили их обновленными, передумать и отменить признак обновления? Вы можете это сделать, используя слово EMPTY-BUFFERS.
Это слово понимает, что запоминать буферы на диске не нужно, и отменяет назначение буферов блокам, а в некоторых версиях оно, кроме того, производит заполнение буферов нулями (байтами, имеющими значение 0) или пробелами с кодом ASCII 32.
Слово EMPTY-BUFFERS в Форт-83 не обязательное, поскольку стандартной программе не разрешается изменять содержимое буфера блока до тех пор, пока не будут спасены на диске предыдущие изменения. Словом EMPTY-BUFFERS следует пользоваться осмотрительно, поскольку можно потерять ценную информацию, не сохранив ее предварительно на диске. Лучше всего поучиться работать с блоками и буферами блоков, проделав несколько упражнений.
Упражнения
1. Определите слово.LINE, которое будет показывать на экране строку заданного блока, если в стеке на вершине указывается номер строки, а второй элемент содержит номер блока. 2. Определите слово INDEX под именем NEWINDEX, используя слово .LINE. 3. Определите слово LIST под именем NEWUST, используя слово .LINE. 4. Определите слово BLLINE, которое будет заполнять строку 64 пробелами (код ASCII 32), если задан номер блока и номер строки. Нужно ли делать UPDATE? 5. Слово TL (напечатать строку), которое является словом MMSFORTH, выводит на экран ряд строк с номерами, так же как и LIST, выбирая их из блока, номер которого содержится в SCR. Это значит, что 5 SCR ! 9 11 TL будет выводить строки с 9-й по 11-ю из блока 5. Определите слово TL с именем NEWTL. Определите также NEWLIST пол именем NEWLIST1, используя TL. 6. Определите слово CLEAR-BLOCK, которое, если перед ним в стеке задан номер блока, заполнит его пробелами (код ASCII 32). 7. Слово РР в некоторых версиях форта позволяет изменить содержимое строки, попросту печатая ее новое содержимое. Например, 32 5 РР Это новое содержимое этой строки
изменит содержимое пятой строки в блоке 32 на текст "Это новое содержимое этой строки". Определите слово РР. (Указание: Используйте 0 WORD.) 8. Определите слово COPY, назвав его NEWCOPY. 9. Очень полезно слово, позволяющее копировать ряд блоков.
Определите слово , которое действует по аналогии с CMOVE>. 11. Можете ли вы определить новое слово, которое производит копирование "вперед" () в зависимости от того, перекрываются или не перекрываются области исходных блоков и область назначения скопированных блоков?
Хранение данных в блоках
Мы уже несколько раз говорили о том, что поскольку Форт в большинстве своих версий не использует файлы операционной системы, то хранить данные, как в файлах, хотя и трудно, но все же возможно. Практически хранение данных в блоках обладает большей гибкостью, чем хранение в файлах, хотя следует признать, что нужно приложить некоторые усилия, чтобы вести учет блоков и связей между блоками. Более того, можно сконструировать файлы, основанные на концепции блоков, и директорию (справочник), которая используется так же, как директория операционной системы. Сначала рассмотрим хранение данных. Простейший способ запомнить данные в блоках состоит в том, чтобы поместить массивы в буферы диска, откуда их можно переместить для хранения на диск. Предположим, что у вас имеется массив, который вы создали таким образом: CREATE TESTARRAY 20 , 26 , 326 , 999 , 228 ' т.е. 10-байтовый массив с пятью 16-разрядными числами. Вы можете сохранить содержимое массива на диске с помощью TESTARRAY 50 BLOCK 10 CMOVE UPDATE а для извлечения данных из массива с диска можно использовать 50 BLOCK TESTARRAY 10 CMOVE
Конечно, из всего блока мы использовали всего 10 байтов. Вы можете сохранить на диске в каждом блоке до 102 таких 5- элементных массивов с номерами 0 - 101. Для этого определим сначала две переменные: VARIABLE STORBLK VARIABLE ARRLEN и инициализируем их: 50 STORBLK ! 10 ARRLEN !
Теперь определим слова для записи массивов на диск: : ARRAYPUT ( адр n - ) (Поместить_массив) STORBLK @ BLOCK (Помещает блок в буфер) SWAP ARRLEN @ * + (Рассчитывает размер места для хранения) ARRLEN @ CMOVE UPDATE ; (Перемещает массив в блок) Для примера, при вводе TESTARRAY 5 ARRAYPUT содержимое TESTARRAY будет запомнено в массиве номер 5 блока STORBLK.
На практике необходимо предусмотреть некоторую проверку на возможность появления ошибок, как, например, попытку записи данных после конца блока. Если рассматривать блок 50 как файл, то можно назвать входящие в него 10-байтовые массивы записями. Не забывайте, что для любой записи на диск требуется исполнить слово FLUSH. В действительности чаще всего не требуется запоминать результаты в массивах перед записью их в блок. Приведем пример, в котором будем рассматривать три блока как три файла записей данных наблюдений врачом пациентов. Пациентам присвоены номера 0 - 512, в записи о каждом пациенте должны быть указаны его вес, систолическое и диастолическое давление крови. Эти данные нужно ввести в три последовательно расположенных блока, начинающихся с адреса PATBLOCK, печатая вес, систолическое давление, диастолическое давление, номер пациента и слово PD (ввести_данные_пациента), т.е. последовательность 125 132 86 92 PD должна записать вес 125 фунтов в первый блок, систолическое давление 132 во второй блок и диастолическое давление в третий блок для пациента номер 92. Эти данные должны попадать в 184-й и 185-й байты каждого блока (2х92 и 2х92+1). Мы выбрали такое загадочное короткое имя слова PD только для того, чтобы облегчить ввод для оператора. Для начала нам нужна константа PATBLOCK: 50 CONSTANT PATBLOCK Вот как можно определить слово PD : : PD ( n1 n2 n3 -- ) 2 * >R (Сохраняет смещение в блоке) PATBLOCK 2+ BLOCK R@ + !UPDATE (Записывает систол. давление) PATBLOCK 1+ BLOCK R@ + !UPDATE (Записывает диастол. давление) PATBLOCK BLOCK R> + !UPDATE ; (Записывает вес)
Как видно из описания, слово PD будет одновременно стирать существующие записи и записывать новые. Не забудьте о том, что в конце записи необходимо сделать FLUSH.
Теперь мы можем определить слова для извлечения данных. В качестве примера приводим слово SD (от Show_Data - показать^данные): : SD ( n --) 2 * >R CR R@ 2/ ."Номер пациента" . CR R@ PATBLOCK BLOCK + @ ." Вес" . CR R@ PATBLOCK 1+ BLOCK + @ ."Систолическое давление".
CR R@ PATBLOCK 2+ BLOCK + @ ."Диастолическое давление". CR ;
Если ввести с клавиатуры 18 SD то мы увидим что-нибудь вроде Номер пациента 18 Вес 192 Систолическое давление 148 Диастолическое давление 76 ok
Теперь посмотрим, как можно получить средние показатели по всем пациентам (мы только начнем решение этой задачи, предоставляя вам закончить ее в упражнениях). Прежде всего нужно убедиться, что в блоках не содержится ничего, кроме данных о пациентах, т.е. все остальные байты должны содержать нули. Это сделать просто, вводя 50 BLOCK 1024 0 FILL UPDATE й то же самое повторить с блоками 51 и 52. Теперь можно ввести данные с помощью PD. Тогда сумму содержимого каждого блока можно найти, используя слово : SUMBLOCK ( n -- ) BLOCK 0 0 ROT 512 0 DO DUP I 2 * + @ SWAP >R 0 D+ R>
LOOP DROP ;
Теперь, если вы введете PATBLOCK SUMBLOCK D. то увидите на экране сумму всех весов. Обратите внимание, что мы должны использовать числа двойной длины и сложные манипуляции в стеке, так как суммарный вес может оказаться больше 65535 фунтов (считая, что средний вес каждого пациента больше 127 фунтов). Предлагаем вам продолжить решение в следующих упражнениях.
Упражнения
1. Переделайте слово ARRAYPUT таким образом, чтобы при попытке записать какую-либо часть массива после конца блока выдавалось сообщение об ошибке и происходил уход из программы. 2. Предположим, что у вас есть два 10-байтовых массива, 1ARRAY и 2ARRAY, которые вы хотите записать в последовательных блоках точно так же, как мы записывали TESTARRAY. Определите слово PUTARRAYS, которое, если ввести: 1ARRAY 2ARRAY 5 PUTARRAYS запомнит эти два массива последовательно друг за другом, начиная с байта номер 100 (5 х 20 - 100). Зачем может потребоваться запоминание массивов попарно? 3. Определите заново слова PD и SD так, чтобы ни ввод, ни вывод не могли бы произойти после конца блока. 4. Создайте переменную CNT и модифицируйте программу SUMBLOCK так, чтобы она инкрементировала значение CNT. В CNT должно накапливаться число ненулевых записей (т.
е. фактически число записей пациентов) после исполнения слова SUMBLOCK. He забудьте в начале SUMBLOCK обнулить переменную CNT. 5. Определите слово AVE (среднее), используя CNT и SUMBLOCK так, чтобы в стек помещались средние значения каждого типа данных. 6. Можно выделить для счета пациентов первую ячейку блока вместо отдельной переменной. Каждый раз, когда данные добавляются или удаляются, должно происходить изменение счетчика. Преимущество этого состоит в том, что счет производится при вводе данных, а не при подсчете статистики, поэтому допустимо вводить и нулевые данные (в предыдущем случае слово SUMBLOCK не должно было считать их). Пусть у вас есть блок с данными о весе пациентов, причем в первой ячейке блока содержится количество пациентов, а в остальных - значения весов. Определите слово AD (Add_Data -добавить_данные) таким образом, чтобы при каждом исполнении AD в конце массива добавлялся вес, а счетчик увеличивался бы на 1. 7. Определите слово DD (удалить_данные), которое, если указана позиция, удаляет данные из нее. уменьшает па единицу счетчик пациентов, а данные, находящиеся выше, перемещаются словом CMOVE вниз. При этом, конечно, необходимо изменить нумерацию пациентов. 8. Определите вместо SUMBLOCK (SB) и AVE (AV) новые слова, которые так же, как в упражнениях 4 и 5, должны подсчитывать суммарный и средний вес, используя для счета пациентов первую ячейку блока. 9. Модифицируйте слова, определенные в упражнениях 6 - 8, назвав их ADS, DDS, SBS и AVS. таким образом, чтобы кроме записи в первой ячейке числа пациентов они записывали бы во второй и третьей ячейках суммарный вес в виде числа двойной длины. 10. Часто оказывается полезным сравнение двух блоков на идентичность их содержимого. Если суммы всех байтов первого и второго блоков равны, то, вероятно, они содержат одинаковую информацию. Такую сумму называют контрольной. Определите слово CHKSUM, которое должно подсчитывать контрольную сумму. Чтобы производить сравнение, не обязательно использовать числа двойной длины, хотя возможность переполнения не учитывать нельзя.
Почему для данной задачи можно пользоваться 16-битовыми числами? Определите также слово ?BLK=, чтобы при вводе 50 55 ?BLK= в стек помещался флаг истина, если контрольные суммы равны, и ложь в противном случае.
Хранение символьных строк в блоках
До сих пор мы рассматривали хранение в блоках только чисел, представленных в двоичной форме. Если вы попробуете вывести на экран листинг блока, на котором были записаны числа, то увидите какую-либо чушь. Однако так же успешно, как и числа, в блоках можно хранить строки
Если после загрузки программы ввести слово FETCHPHONES, то в память будет введен последний (обновленный) телефонный справочник. Больше того, если слово FETCHPHONES было последним в загруженных блоках, то все массивы окажутся автоматически инициализированными. Теперь если в справочнике производятся изменения, то, чтобы запомнить его содержимое на диске, вам нужно только ввести слово SAVEPHONES. Но не будем торопиться. В гл. 9 мы создали два слова, с помощью которых можно было изменять записи в справочнике: STOREPHONE и ERASEPHONE. Можно сделать так, что справочник будет записываться автоматически, если внести простое изменение в определения этих слов, а именно включить в конце определения слово SAVEPHONES (перед ;). Тогда если справочник изменялся, то он будет автоматически обновляться на диске. Теперь можно выключать компьютер в любой момент без потери данных. Таким образом, можно сделать справочник составной частью вашего Форта. И все же данный пример фактически не так уж много разъяснил вам, как работать со строками в блоках. Единственное, что мы сделали, - это записали в массивы в блоки. Но делать со строками можно гораздо больше.
Для разбора (разделения) слов, содержащихся в блоках на диске, можно использовать слово WORD подобно тому, как мы использовали слово QUERY в гл. 9. Рассмотрим, как это делается, на примере. Выберите блок для эксперимента (пусть это будет блок 50) и введите редактором в первую строку блока фразу "The quick brown fox".
Затем определите следующее слово: : PARSE ( блок# -- ) (Разбор) BLK @ >R >IN @ >R (Запоминает прежние указатели) BLK ! 0 >IN ! ( Готово для раэбора нового блока) BEGIN 32 WORD (Начало раэбора) COUNT ?DUP WHILE TYPE SPACE (Печатает, пока находит) REPEAT DROP R> >IN ! R> BLK ! ; (Восстанавливает указатели) Теперь если ввести 50 PARSE то вы увидите на экране The (Выделенные quick при brown разборе fox слова)
Вместо того чтобы разделять слова во входном буфере с помощью слова PARSIT из гл. 9, вы можете разделять их непосредственно в блочном буфере словом PARSE, присваивая соответствующие значения переменным BLK и >IN. Описанный прием извлечения строк из блоков является довольно эффективным для работы как со строковыми, так и с числовыми данными, запоминаемыми в форме кодов ASCII. В этом вы убедитесь, сделав несколько упражнений.
Упражнения
1. Определите слово BLOCKWORD, которое должно выделять только одно слово из блока, запоминать его в счетной строке с адресом PAD и выдавать в стек адрес PAD, В стеке должен находиться номер блока, смещение начального байта в блоке и код разделителя. Таким образом, если вы введете 50 8 32 BLOCKWORD COUNT TYPE то в PAD будет занесена счетная строка, начинающаяся с восьмого байта и завершающаяся пробелом. После этого выделенное слово будет выведено на экран. 2. Определите переменную под именем POSITION для задания смещения байта, с которого должно начинаться разделение слов. После этого дайте новое определение слова BLOCKWORD под именем BWORD. которое должно производить разбор слов, начиная с позиции, записанной в переменную POSITION. Таким образом, если ввести 8 POSITION ! 50 32 BLOCKWORD COUNT TYPE то действие должно быть таким же, как в упражнении 1, Кроме того, новое определение BLOCKWORD должно изменять значение переменной POSITION так, чтобы оно указывало на следующую после разделителя позицию выделяемого слова. Таким образом, если несколько раз подряд исполнить 50 32 BLOCKWORD COUNT TYPE то на экран будут выведены все слова из блока. 3.
Опишите слово BLOCKNUMBER, которое должно брать число из блока в форме строки символов ASCII и помещать его в стек. Для решения этой задачи вам. возможно, придется освежить в памяти слова CONVERT и NUMBER из гл. 9. 4. Кроме извлечения строк из блоков может оказаться полезным также запоминание их в блоках. Определите слово TOBLOCK, которое действует как BLOCKWORD, но запоминает строку по указанному адресу в указанном блоке в позиции, определенной в переменной POSITION. Строка должна быть представлена в счетной форме, но запоминаться она должна как строка без байта-счетчика с разделителем в конце строки. Слово POSITION нужно изменить так. чтобы оно указывало на позицию, следующую сразу после разделителя. Таким образом, если в POSITION записан 0 и строка "fox" записана, начиная с PAD. то при вводе PAD 50 32 TOBLOCK строка "fox" будет записана в блок номер 50, начиная с нулевого байта; переменная PAD будет изменена на 4, чтобы указывать на байт, следующий сразу после пробела, которым заканчивается строка "fox". Когда подобное изменение POSITION может оказаться полезным?
Использование нескольких блоков в качестве файла
Если вы пользовались файлами, работая с другими языками программирования, то вам, безусловно, понятно, что в Форте мы рассматриваем отдельный блок как короткий файл. Когда мы обсуждали передачу данных между блоками и массивами чисел и символьных строк, используя оператор CMOVE, мы показали вам так называемый метод произвольного доступа при вводе-выводе (т.е. при вводе и выводе) данных, который применяется для доступа к файлам. Мы имели в виду, что данные, находящиеся в записях (в нашем случае в массивах) запоминаются и извлекаются в блоке с указанного пользователем места. В противоположность большинству языков программирования, которые работают с записями фиксированной длины, т.е. производят обмен данными с диском фиксированными порциями информации в байтах, мы могли сами выбирать любое число байтов для обмена с буферами и, следовательно, с диском.
Применяя терминологию других языков программирования, это можно назвать произвольным доступом с произвольной длиной записи. Считается, что осуществление произвольного доступа с переменной длиной записи очень прогрессивно, но представляет собой трудную задачу. В Форте же она решается очень просто.
Метод доступа к данным, который мы применяли в последней серии упражнений, называется применительно к файлам последовательным доступом. Можете думать, что такое название объясняется тем, что текст последовательно считывается из файла (или блока) с помощью указателя (в нашем случае POSITION), который указывает на положение следующей строки, которая должна быть извлечена.
Большинство языков программирования при осуществлении последовательного доступа допускают использование очень небольшого оговоренного заранее набора разделителей. В Форте разделителем может быть любой символ. Так в упражнениях мы использовали для этой цели пробел. Кроме того, в других языках программирования последовательный доступ должен всегда производиться с начала файла, а в блоке Форта вы можете начать с любого места. Таким образом Форт позволяет очень гибко использовать отдельный блок в качестве короткого файла. Однако очевидным недостатком Форта является то, что его "файлы" не могут быть длиннее 1024 байтов. Для многих применений этого объема недостаточно. Поэтому теперь мы обсудим, каким образом можно осуществить доступ к двоичным числам и строковым данным, находящимся в настоящих файлах, состоящих из нескольких блоков. Мы можем определить новые слова Форта, которые будут подобны тем, что мы ввели в упражнениях, за исключением одного: когда будут исчерпаны данные одного блока, они будут обращаться за продолжением к другому блоку. Но это не очень просто, поскольку непросто работать со строками или числами, размещенными даже на двух блоках. Кроме того, возникает необходимость в директории блоков, чтобы знать, в каком порядке они располагаются в файле. Тем не менее мы можем создать так называемый файл в памяти, в который одновременно будут помещаться все блоки и где будут выполняться все действия с ними, а запись на диск будет производиться по завершении всех операций с данными.
В дальнейшем изложении мы будем рассматривать именно та кие файлы в памяти. Сейчас же мы обсудим, как можно использовать их для осуществления процедуры последовательного доступа при обращении как с числами, так и со строковыми данными. (Большую часть работы вам предстоит выполнить в упражнениях.)
Предположим, что имеется метеорологическая база данных, в которой хранятся записи о температуре, относительной влажности воздуха, скорости ветра и атмосферном давлении, которые производятся через каждые полчаса. База данных была собрана в блоки при помощи системы сбора данных, реализованной на Форте. Одновременно необходимо работать с данными, накопленными за четыре недели. Каждая запись содержит результаты четырех измерений, т.е. каждая запись состоит из 8 байтов. За день производится 48 записей (по одной записи за полчаса), поэтому число записей за неделю будет 48х7=336 или 336х4=1344 записей в месяц. Следовательно, для хранения записей за месяц потребуется 1344х8=10.572 байта, которые можно разместить на II блоках диска. Допустим, что нужно выводить данные за день, неделю и за месяц. Можно работать с этой информацией с помощью программы, которая обращается к очень большому массиву, размещенному в памяти, который мы назвали файлом в памяти. Сначала мы создадим массив, в котором будут храниться общее число блоков и их номера, например: CREATE DATA 11 , 50 , 51 , 52 , 53 , 60 , 61 , 62 , 63 , 64 , 65 , 66 , (Заметьте, что блоки могут идти не обязательно подряд.) Теперь резервируем место для размещения файла CREATE METFILE DATA @ 1024 * ALLOT Определим слово, которое должно перемещать данные из блоков на диске в файл METFILE: : GETFILE ( -- ) (Взять_файл) DATA @ 1+ 1 DO (Проходит по блокам) DATA I 2 * + @ BLOCK (Получает адрес каждого блока) METFILE I 1- 1024 * + 1024 CMOVE (Перемещает данные в файл) LOOP ;
При исполнении слова GETFILE файл в памяти (метафайл) будет заполнен информацией из соответствующих блоков.
Можно дать более общее определение слова GETFILE, которое не только будет заполнять метафайл, но и создавать его: : GETFILE ( адр - ) CREATE HERE OVER @ 1024 * ALLOT SWAP DUP @ 1+ 1 DO 2DUP I 2* + @ BLOCK SWAP I 1- 1024 * + 1024 CMOVE LOOP DROP DROP ;
В таком виде слово GETPILE можно использовать для создания файла из любого набора блоков, номера которых хранятся в массиве DATA. Если массив номеров блоков устроен так же, как DATA, то слово GETFILE используется в такой форме: DATA GETFILE METFILE
Теперь нужно сконструировать слова для обеспечения доступа к данным в метафайле. Вспомним, что в каждой записи имеется по четыре элемента (в 8 байтах). Поэтому запись номер 1 начинается с 0-го байта файла, запись номер 2 - с 8-го, запись номер 3 - с 16-го и т.д. Если нам нужен третий элемент данных из записи номер 200, то мы можем извлечь его из метафайла с помощью такой последовательности действий: METFILE 200 1- 8 * + 3 1- 2* @
Можно будет упростить эту операцию, если определить еще несколько слов. Причем можно сделать эти определения достаточно общими, чтобы пользоваться ими впоследствии для любого метафайла с любой длиной записей. Сначала определим переменную для хранения длины записи: VARIABLE RECLEN 8 RECLEN !
Затем определим переменную, указывающую на позицию начала записи, которая будет использоваться в такой форме: 200 METFILE RECLOC чтобы поместить в стек адрес начала 200-й записи в файле. Вот это определение: : REGLOC ( n адр -- адр ) SWAP 1- RECLEN @ * + ;
Теперь опишем слово DATALOC для вычисления адреса любого элемента заданной записи, которое, например, в случае 3 200 METFILE DATALOC будет возвращать в стек адрес начала 3-го элемента данных в записи номер 200. Вот определение этого слова: : DATALOC ( n1 n2 адр1 -- адр2) RECLOC SWAP 1- 2* + ;
С помощью этих общих слов мы можем определить слова для извлечения конкретных данных из нашего файла, т.е. для извлечения, например, данных о температуре, влажности воздуха и т.д. Мы выпишем их в последовательности, в которой они записаны в метафайле: : TEMP 1 SWAP METFILE DATALOC ; (Температура) : HUMID 2 SWAP METFILE DATALOC ; (Влажность) : WIND 3 SWAP METFILE DATALOC ; (Скор. ветра) : BAROM 4 SWAP METFILE DATALOC ; (Давление)
Тогда если вы введете 200 WIND @ то получите скорость ветра в 200-й записи. (Как вы узнаете в гл. 11, эти четыре слова можно определить еще лучше, если применить специальное слово-определитель в конструкции CREATE...DOES>.) Наконец, было бы неплохо определить слово, позволяющее по значениям номера дня относительно начала текущей серии данных, по значениям часа и минуты возвращать в стек номер записи, относящейся к этой дате.
Это может быть сделано с помощью слова : TIME->REC 30 / SWAP 2 * + SWAP -18 * + ;
Напомним, что данные собираются через каждые полчаса. Тогда, если ввести 7 10 30 TIME->REC TEMP @ то вы получите значение температуры в седьмой день от начала записей в 10 ч 30 мин. Заметьте, что слова GETFILE. RECLEN, RECLOC и DATALOC вместе со словом PUTFILE, которые вы определите в порядке упражнения, представляют собой простые, но весьма полезные и достаточно общие расширения языка для работы с файлами. Единственное ограничение на длину файла накладывает доступный объем памяти. У вас может возникнуть желание собрать эти слова вместе и оформить для применения в более общем виде (и еще некоторые слова, которые мы определим в следующем разделе). Кроме того, если вам часто приходится работать с файлами данных, вы захотите усовершенствовать обращение с файлами, чтобы добавить более мощные возможности. Мы предлагаем вам еще поработать с метеорологической базой данных в следующих упражнениях,
Упражнения
1. Определите слово DATA-AVE, которое должно выдавать среднее значение данных, если задать номер первой и последней записи и время этой записи. Так, например, если ввести 800 TIME->REC 8 23 30 TIME >REC 3 DATA-AVE то вы получите среднее значение температуры за восьмой день. считая со дня создания файла. Здесь потребуются числа двойной длины. 2. Определите слово TEMP-AVE, которое действовало бы как 1 DATA-AVE, т.е. чтобы не нужно было указывать положение элемента данных в записи. Используйте в определении ТЕМР@. 3. Предположим, что при измерении температуры была допущена ошибка в 1 градус из-за неверной калибровки термометра, в результате чего нужно добавить к каждому значению температуры в METFILE 1 градус. Определите слово CORRECT-TEMP, которое должно произвести указанную коррекцию. 4. Определите слово PUTFILE для копирования файла с изменениями в исходные блоки, из которых он был загружен в память, т.е. если ввести METFILE DATA PUTFILE то файл с измененными данными будет возвращаться на диск. 5.
Определите слово SAVEREC, которое будет заменять запись, помещая в нее число из стека. Предусмотрите уход из программы, если количество чисел в стеке меньше, чем значение переменной RECLEN.
Файлы строковых данных с последовательным доступом
В примере, который был очень подробно разобран в предыдущем разделе, мы имели дело с файлом числовых (двоичных) данных с последовательным доступом и с фиксированной длиной записи. Если вы вспомните, что еще раньше мы пользовались словом WORD для выделения строк из блока, то вам нетрудно будет представить, что мы можем работать также и с файлами, содержащими последовательности строк различной длины, отделенных друг от друга разделителями.
Мы рассмотрим здесь в общем виде строковые файлы в памяти, при этом вы определите в упражнениях некоторые полезные слова, а затем построим файл адресов и обсудим, как его можно использовать. Строковый файл (или файл строковых данных) можно описать в виде массива точно так же, как файл метеорологических данных. Предположим, что у вас имеется 10 блоков, содержащих имена и адреса людей. Можно создать массив, который описывает файл, следующим образом: CREATE ADDRESS 10 , 50 , 51 , 52 , 53 , 54 , 60 , 61 , 62 , 63 , 64 ,
Пользуясь этим массивом, можно инициализировать файл в памяти, применяя операцию GETFILE точно так же, как мы создавали в предыдущем разделе файл METFILE. т.е. чтобы сделать это, нужно написать ADDRESS GETFILE ADDFILE
Однако вместо того, чтобы описывать длину каждой записи в переменной, мы используем переменную для хранения байта-разделителя. Выберем сначала в качестве символа разделителя пробел, потому что это нам потребуется в упражнениях: VARIABLE DELIMITER 32 DELIMITER !
Теперь предположим, что нам нужно произвести разделение текста первых 64 символов в файле на строки. Мы можем определить для этого слово PARSELINE по аналогии с PARSIT из материала гл. 9: : PARSELINE ( адр -- ) (Разделить_строку) BLK @ >R >IN @ >R (Запоминает указатели при вводе из блока) TIB 80 0 FILL (Заполняет буфер ввода нулями) TIB 64 CMOVE (Вводит в буфер 64 символа) 0 BLK ! 0 >IN ! CR (Указывает на начало буфера ввода) BEGIN DELIMITER @ WORD (Начинает разделение) COUNT ?DUP WHILE TYPE SPACE (Выводит, если есть что) REPEAT (Повторяет) TIB 80 BLANK (Заполняет буфер ввода пробелами) R> >IN ! R> BLK @ ; (Восстанавливает указатели блока)
Если ввести ADDFILE PARSELINE то первые 64 символа файла будут подвергнуты разбору, т.е. разделены на слова-строки, между которыми в качестве разделителя стоит пробел, и выведены на экран. Главные различия между словами PARSIT и PARSELINE состоят в том, что в данном случае перед словом указывается не номер блока, а адрес и, кроме того, вместо конкретного кода 32 мы применили более общее DELIMITER и для выполнения разделения использовалось пространство в буфере ввода.
Теперь вам предстоит определить некоторые слова, которые являются полными аналогами слов из упражнений предыдущей серии (мы даже сохраним их имена), но они должны работать не с блоками, а с файлами.
Упражнения
1. Определите слово GETWOBD, которое будет выделять только одно слово-строку из файла и запоминать его в форме счетной строки в PAD, возвращая а стек адрес PAD. В стеке должны находиться адрес файла и смещение первого байта в файле. Тогда после ввода ADDFILE 8 GETWORD COUNT TYPE в PAD будет перемещена из файла и запомнена счетная строка символов ASCII* начинающаяся с байта номер 8 и заканчивающаяся пробелом. После этого выделенное слово-строка должно быть выведено на экран. Для распознавания конца слова-строки нужно использовать переменную DELIMITER. 2. Определите переменную POSITION, в которой должна быть записана позиция байта в файле, начиная с которой нужно произвести разбор текста и выделение слов-строк. Теперь определите слово FILEWOKD, которое должно выполнять разбор текста в файле, начиная с места, на которое указывает переменная POSITION. Например, последовательность операторов 8 POSITION ! ADDFILE FILEWORD COUNT TYPE должна приводить к тому же результату, как в примере из упражнения 1. Кроме того, слово FILEWORD должно изменять значение в POSITION так, чтобы оно указывало на байт, следующий после байта-разделителя, стоящего в конце выделенного слова-строки. Таким образом, если повторно будет исполняться ADDFILE FILEWORD COUNT TYPE то мы увидим последовательные слова строки. 3. Определите слово FILENUMBEK, используя FILEWORD, которое должно брать из файла число, представленное последовательностью кодов ASCII, и помещать его как число в стек.
Для этого вам, может быть, потребуется вспомнить, как работают слова CONVERT и NUMBER из гл. 9. 4. Кроме извлечения строк из файла весьма полезно иметь возможность записывать их в файлы. Определите слово TOFILE, которое работает аналогично слову FILEWORD, но в отличие от него запоминает строку, начиная с указанного адреса в указанный файл с позиции, указанной в переменной POSITION. Строка должна быть в исходном виде в счетной форме, а запоминаться как несчетная строка, в конце которой находится байт-разделитель. Содержимое POSITION должно быть изменено так, чтобы оно указывало на позицию, следующую после байта-разделителя. Так, если в POSITION записан 0 и текст "fox" помещен, начиная с адреса PAD, то при исполнении PAD ADDFILE TOFILE текст "fox" будет запомнен в файле, начиная с байта номер 0; содержимое переменной POSITION будет изменено на 4, т.е. будет указывать на байт, следующий после пробела, замыкающего текст "fox".
Файл адресов
В гл. 9 мы построили телефонный справочник, а в данной главе мы его модифицировали, для того чтобы запоминать в блоках диска. Несмотря на свою гибкость и быстродействие, этот справочник не лишен недостатка, состоящего в том, что для каждой записи требуется 30 байтов для имени абонента и 30 байтов для адреса. Это приводит к напрасному расходованию памяти и не позволяет записывать более длинные имена. Более гибким оказывается другой подход: будем отделять вводимые данные друг от друга с помощью байта-разделителя, в этом случае они будут занимать ровно столько места, сколько действительно требуется. Именно так мы построим наш файл адресов.
Будем считать, что каждая запись в файле должна состоять из имени абонента, его адреса и номера телефона. Каждая запись будет разделяться на четыре поля: поле имени, поле названия улицы, поле названия города, почтового индекса и страны, поле номера телефона. Для отделения полей используем знак-разделитель "'"(код ASCII 94). Мы ставим перед собой задачу определить слова для вывода имени и адреса, поиска по имени, добавления новых имен и адресов в конец файла.
После этого мы предлагаем вам дополнить список слов еще некоторыми функциями в упражнениях. выглядеть, если запросить листинг первого блока: 1 John Slmpson^2223 Second St.^Louisville, PA 234^56-225-294-678^ 2 Jean Baptist de Lamarc^23a Rue des Arbres^Parls, France^02-955 3 6^John TrenfCat's House Cottage^burton-Underhill PD56-5BC Dors 4 set England^01-35-5624^James Mathieson^238 Parkway Drive^Philade
(Помните, что файл находится в памяти, а для того, чтобы его поместить туда, нужно использовать слово GETFILE.) Для обеспечения доступа к файлу адресов, выделения полей с помощью разделителей мы воспользуемся идеями предыдущего раздела. Итак, в переменную DELIMITER нужно записать код 94, который будет помечать конец каждого поля. Теперь пусть POSITION указывает на начало имени. Проще всего вывести имя и адрес; для этого можно воспользоваться словом FILEWORD из упражнения 2 предыдущего раздела. Слово : PRINT-RECORD ( -- ) ( Напечатать_запись) CR 4 0 DO DUP FILEWORD COUNT TYPE CR LOOP DROP ; решает эту задачу, если использовать его в следующем контексте: ADDFILE PRINT-RECORD
Но как найти начало поля имени? Для этого мы должны иметь возможность искать имя, помещая указатель POSITION в начало поля имени. Поэтому нам потребуется строковая переменная, в которую мы поместим строку для поиска: 80 $VARIABLE SEARCH$
Нам нужно также иметь слово, которое возвращало бы указатель (POSITION) снова в начало поля, предполагая, что содержимое поля хранится в PAD; это слово мы будем использовать, чтобы после того, как поиск завершится успешно, переместить указатель в начало.поля имени: : POINTBACK PAD С@ 1+ NEGATE POSITION +! :
Слово POINTBACK вычитает длину поля плюс разделитель из значения, хранимого в POSITION. Нужно также распознавать, когда мы дойдем до конца файла. Мы будем делать это, используя признак конца файла в последнем поле имени, в качестве которого выберем строку "&&&", определив ее как константу: $CONSTANT EOF &&&"
Далее мы воспользуемся некоторыми словами для работы со строками из версии MMSFORTH, которые вы можете заменить собственными аналогами, если посмотрите их определения в гл. 9.) Приведенное ниже слово будет возвращать в стек флаг истина, если в строке, адрес которой находится в стеке, будет встречен признак конца файла: : ?EOF ( $адр -- флаг ) EOF INSTR 0= 0= ; (Чтобы понять, как используется слово INSTR, посмотрите пример с телефонным справочником в гл. 9.) Теперь мы можем определить слово SEARCH-NAME ( -- ) (Искать_имя) BEGIN ADDFILE FILEWQRD DUP DUP ?EOF IF POINTBACK CR ." Имя не найдено" ABORT THEN SEARCH$ INSTR DUP IF POINTBACK CR SWAP COUNT TYPE ." найдено" CR ABORT THEN UNTIL ;
Вот как будет использоваться слово SEARCH-NAME. Предположим, что в файле находится имя "John Jones", а после него "Gary Jones". Вы хотите найти адрес и телефон Gary, но вам нужно вспомнить, как его фамилия.
Введите $" Jones" SEARCH$ $! и 0 POSITION ! а после этого SEARCH-NAME
Вы увидите, что на экран будет выведено "John Jones найдено", но вы искали не это. Введите теперь PAD C@ POSITION +! чтобы пропустить John (если желаете, определите для этой цели специальное слово NEXTFIELD), а затем снова введите SEARCH-NAME, тогда вы увидите сообщение "Gary Jones найдено". Текстовая строка "Gary Jones" была запомнена в PAD в виде счетной строки, переменная POSITION указывала на начало поля имени. Теперь, если вы введете PRINT-RECORD, на экране вы увидите имя абонента и его адрес. Обратите внимание на то, что, поскольку поиск производится во всех полях, будет найдена также строка "101 Jones St."
Теперь нам нужно сконструировать слово для добавления строки к концу файла, т.е. перед началом признака конца файла): : FINDEOF ( адр - ) BEGIN DUP FILEWORD ?EOF UNTIL DROP POINTBACK ;
Форма использования этого слова такая: ADDFILE FINDEOF
Кроме того, нам нужно слово, которое должно добавлять новое поле в файле, если задан адрес счетной строки, которая должна помещаться в это поле: : PUTFIELD ( $адр - ) $" ^" $+ TOFILE ; (Слово TOFILE было определено в упражнении 4 предыдущего раздела.) Вот, например, слово для добавления нового поля в запись адреса: : ADD-ADDR ( -- ) ADDFILE FINDEOF ." Имя " IN$ PUTFIELD CR ." Адрес " IN$ PUTFIELD CR ." Город/Штат " IN$ PUTFIELD CR ." Телефон " IN$ PUTFIELD CR $" &&&" PUTFIELD ; Обратите внимание на то, что новое поле наложилось на старый признак конца файла.
Несмотря на то, что приведенный пример имеет узкоспециализированное применение, он демонстрирует большое число приемов работы с текстовыми файлами.
Вы можете развивать его дальше в последующих упражнениях.
Упражнения
1.Определите слово NEXTFIELD, которое должно помещать указатель POSITION на начало следующего поля. Это значит, что если POSITION указывает на поле имени, то NEXTFIELD должно помещать его на поле названия улицы. 2. Определите слово NEXTREC, которое, если указатель показывает на поле имени, будет помещать его на начало следующего поля имени. 3. Определите слово FINDPHONE, которое, если в PAD задано имя абонента, будет находить его в файле и печатать номер телефона. 4. Определите слово DELREC, предназначенное для удаления записи из файла и смещения оставшихся записей так, чтобы перекрыть удаленную запись. Переменная POSITION должна указывать на начало поля имени удаляемой записи.
Выводы
В этой главе мы обсудили множество вопросов, начав с простейших операций вывода листинга и загрузки программы, а закончив разбором текстовых файлов с переменной длиной записей. Мы очень подробно рассмотрели реализацию загрузки блоков, использование блоков в качестве файлов и файлов, организованных в памяти. Наш обзор затронул также примеры использования директорий файлов для организации их ввода в память блок за блоком. Причиной столь подробного изложения является то, что в большинстве версий Форта не предусмотрены организация блоков в файлы и хранение данных на диске. Даже если вам все это и не потребуется, мы надеемся, что вы имели возможность убедиться в возможностях Форта расширяться для решения задач, которые поначалу могут показаться не присущими языку. Если вам нужны средства для работы с базами данных, мы надеемся, что эта глава поможет вам решать задачи такого рода.
Создание слов-определителей
Замечательная способность языка Форт к расширению является следствием наличия в нем так называемых определяющих слов. Единственное назначение этих слов состоит в компиляции (определении или создании) других слов. Наиболее важным из определяющих слов (слов-описателей) является : (двоеточие). Из других слов, с которыми вы к настоящему времени познакомились, это 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 на ассемблере. Применение слов-определителей может коренным образом изменить ваш стиль программирования. Через некоторое время вы будете удивляться, как вы могли раньше обходиться без них.
Редакторы Форта
Одной из важнейших частей любого языка программирования для ЭВМ является редактор, который служит для ввода и видоизменения программы, в этом отношении Форт не исключение. Удобной особенностью Форта является то, что редактор написан на Форте и может, как и всякая другая программа, легко изменяться с целью адаптации к вашим нуждам и вкусам. Так как редактор - это программа, которую вы будете использовать при программировании на форте наиболее часто, имеет смысл изменить его, если вы не удовлетворены редактором, который вам достался вместе с системой.
Попыток стандартизировать Форт-редактор не предпринималось, за исключением того, что слово EDITOR используется в качестве имени словаря редактирующих операторов (словари рассмотрены в гл.14). При таком разнообразии на рынке аппаратуры (в дополнение к разнообразию мнений о том, что такое хороший редактор), достаточно бессмысленна попытка навязать редактор, приемлемый для всех.
Существует два типа редакторов, поставляемых обычно с Фортом (часто с одной и той же системой). Первый из них строчный редактор, который в соответствии с названием дает возможность модифицировать содержимое блоков форта строчка за строчкой (предполагается, что блок Форта содержит 16 строк по 64 символа). Хотя строчный редактор занимает мало места в памяти и обладает высоким быстродействием, ему свойственны ограничения, связанные с тем, что вы не видите изменений, которые вносите в текст блока в целом. Вторым типом Форт-редактора является экранный редактор, где модификации происходят в месте текста, отмеченном курсором, который можно переместить куда угодно в пределах блока, отображенного на экране. Экранный редактор проще использовать, чем строчный, хотя и за счет большего объема занимаемой памяти.
В этой главе мы рассмотрим типичный, но довольно простой строчный редактор, а затем предложим исходный текст экранного редактора. Если у вас имеется только строчный редактор, вы сможете его использовать для написания предложенного здесь экранного редактора.
Владея работающим экранным редактором, вы можете его использовать для упражнений, предлагаемых в книге, так же как и для совершенствования самого редактора. Даже если вы вполне удовлетворены вашим редактором (многие версии Форта снабжены редакторами, более совершенными, чем приведенный здесь нами). вам следует рассмотреть некоторые наши идеи, прежде чем переходить к гл.13, где описана структура редактора.
Основы редактирования для Форта
Как было показано в гл.10, ввод-вывод для диска осуществляется через резервные зоны памяти, называемые блочными буферами. Как вы знаете, ключевым словом для ввода с диска служит BLOCK, которое загружает содержимое блока с диска в свободный блочный буфер и заносит в стек адрес первого байта этого буфера. Напомним также, что ключевыми словами для спасения блоков информации на диске являются UPDATE, которое помечает буфер блока, чтобы он был спасен, и FLUSH (или SAVE-BUFFERS), которое записывает на диск содержимое буферов, помеченных UPDATE.
Редактирование производится путем переноса содержимого блока с диска в буфер, изменения содержимого этого буфера и пометки буфера таким образом, что его измененное содержимое будет снова записано на диск. В действительности при работе с экранным редактором текст из буфера переносится в другую область памяти, где производится редакторская правка до возвращения текста обратно в буфер. В этом случае измененный текст из буфера редактора переносится в блочный буфер только при условии, что сделанные редактором изменения следует занести на диск. Если текст всегда редактируется непосредственно в блочном буфере, вы будете вынуждены использовать EMPTY-BUFFERS каждый раз , когда вы решите отменить некоторые редакционные исправления. Измененный текст в буфере редактора должен быть перенесен в блочный буфер прежде, чем он сможет быть записан на диск, таким образом уменьшится вероятность занесения на диск нежелательных изменений.
Строчное редактирование
Строчный редактор Форта состоит из набора слов, которые используются для изменения содержимого блоков Форта строчка за строчкой.
Строчный редактор не является "унифицированной" программой, в которой каждое слово, предназначенное для редактирования строки, применяется независимо для изменения отдельной строки текста. Команда n LIST в начале процедуры редактирования присваивает номер блока переменной SCR и отображает содержимое блока, который будет редактироваться (SCR не нужно для системы Форт-83, но большинство диалектов ее используют).
Слова строчного редактора используют число в SCR для идентификации блока, который должен редактироваться, и, кроме того, многие из них требуют при исполнении наличия в стеке номера строки. В табл. 12.1 представлены некоторые типичные команды строчного редактора, которые использовались в устаревшей версии MMSFORTH V 1.9 (MMSFORTH в настоящее время поставляется с мощным экранным редактором). Вы сами можете описать эти слова, основываясь на знаниях, приобретенных в гл.10.
Таблица 12.1. Команды строчного редактора MSFORTHV 1.9.
n A текст Добавить строку, сдвинуть вниз остальные строки: строка помещается в PAD-буфер n D Стереть строку, сдвинуть вверх остальные строки; строка помещается в PAD-буфер n I Ввести строку, сдвинув остальные строки вниз; строка извлекается из PAD-буфера n P текст Поместить текст, следующий за "Р", в строку n n R Заменить строку n строкой из PAD-буфера S Выйти из редактора и спасти все изменения n Т Отобразить на экране строку n; поместить строку в PAD-буфер Q Выйти из редактора без спасения внесенных изменений
При выполнении команды LIST номер блока запоминается в SCR, а его содержимое заносится в буфер. Большинство операторов строчного редактора используют последовательность SCR @ BLOCK для получения адреса памяти, куда был загружен блок с диска (блочный буфер). Редактирование производится непосредственно в блочном буфере, и дополнительно требуется только 64 байта памяти сразу за словом PAD. Эта память служит для размещения строки, которая стерта или введена, так что она может быть позднее использована для ввода или замещения фрагмента текста в блочном буфере.
Простота этого редактора привлекательна, но его возможности, очевидно, весьма ограничены.
Более мощные строчные редакторы снабжены командами для поиска, добавления или замещения фрагмента текста в пределах строки, блока или серии блоков. Эти или другие улучшения могут сделать строчное редактирование более гибким и эффективным. Простой строчной редактор занимает очень мало места в памяти и работает на любом дисплее. Но даже после улучшения строчный редактор не сможет сравниться в простоте использования с экранным редактором.
Экранное редактирование
Экранный редактор представляет собой программу (в противоположность совокупности слов, составляющих строчный редактор), которая загружает содержимое блока в буфер, отображает текст и позволяет изменить текст в соответствии с положением курсора на экране. Все вводы с клавиатуры перехватываются редактором, который распознает вводимый код и выделяет коды, генерируемые командными клавишами, в то время как все прочие "печатные" символы используются для изменения текста. Экранный редактор просто использовать, так как измененный текст всегда на экране, а применение курсора для отметки места, где надлежит что-то изменить, является вполне естественным. Экранный редактор может быть дополнен многими новыми возможностями, такими как дополнительные строчные буферы, так называемые "теневые блоки" для обширных комментариев (см.гл.10), автоматическая простановка даты (дата ставится на верхней строке), средства для чтения и ввода двоичных величин при редактировании блоков данных и многое другое.
Экранный редактор
Наш экранный редактор требует, чтобы ваша версия Форта и (или) ваш видеотерминал имели некоторый контроль над тем, что отображается на экране. Как минимум, вы должны быть способны очистить экран и поместить курсор в верхний левый угол, а также установить курсор в указанную точку экрана. Способы реализации этих функций сильно варьируются от ЭВМ к ЭВМ и от терминала к терминалу. Большинство удаленных терминалов воспринимают последовательности управляющих символов, посланных оператором EMIT, и за счет этого управляют изображением на экране.
Многие ЭВМ со встроенными дисплеями также воспринимают управляющие символы, и кроме того, работают со словами Форта, предназначенными для перемещения курсора и управления экраном. Например, MMSFORTH использует слово PAGE для очистки экрана и РТС для перемещения курсора в позицию, заданную числами в стеке. Мы написали этот редактор для работы с терминалом Lear-SiegeI ADM-31, и если вы используете другой терминал, вам надо модифицировать программу в блоке 2.
Кроме минимальных возможностей по очистке экрана и перемещению курсора многие терминалы (встроенные дисплеи) имеют много других функций. Например, часто бывает предусмотрена возможность стирания символов в интервале от места, отмеченного курсором, до края строки перемещения курсора вверх или вниз на одну строку и т.д. Если ваш дисплей позволяет делать такие вещи, то вы можете сделать работу редактора более удобной, хотя эти усовершенствования, строго говоря, и не являются обязательными. Мы дадим вам советы, как выполнить эти модификации. Наконец, некоторые встроенные дисплеи имеют ЗУ, в которых изображение хранится в виде ASCII символов и которые непрерывно опрашиваются с помощью схемотехнических средств, а их содержимое отображается на экране. Если ваша ЭВМ работает таким образом, можно сделать намного более эффективный редактор. Мы дадим вам некоторые предложения по поводу такого редактора в гл.13, где обсудим структуру этого редактора.
Приведенный ниже текст программы редактора содержит различные советы по модификациям, которые могут сделать его использование более удобным и эффективным. Программа составлена, однако, с тем расчетом, чтобы максимально упростить ее применение и сделать это возможным на максимальном числе ЭВМ. Мы полагаем, что вы используете ее сначала только с совершенно необходимыми, которые совершенно необходимыми, т.е. изменив лишь
0 ( 20 июля 85 Экранный редактор NS 01 из 10) 1 : TASK ; DECIMAL 2 3 32 CONSTANT BL 102/1 CONSTANT 1K 4 64 CONSTANT 64 63 CONSTANT 63 5 10000 CONSTANT PDELAY 5000 CONSTANT SDELAY 6 VARIABLE SCR VARIABLE ROW 7 VARIABLE COL VARIABLE I/R 8 VARIABLE LOWBLK VARIABLE HIGHBLK 9 ( запоминание первого и последнего блоков, которые можно редактировать на вашей ЭВМ) 10 1 LOWBLK ! 169 HIGHBIK ! 11 ; --- ; ( слово, не выполняющее никакой работы) 12 : MODE ( -- ) I/R @ 0= I/R ! ; ( выбор ввод-замещение ) 13 : DELAYS ( n -- ) 0 DO LOOP ; ( задержка на n циклов ) 14 : PAUSE ( -- ) PDELAY DELAYS ; ( длинная задержка) 15 : SPAUSE ( -- ) SDELAY DELAYS ; ( короткая задержка )
0 ( 20 июля 85 Экранный редактор NS 02 из 10) 1 : PADDR ( n--) CREATE , DOES> @ PAD + ; 2 1K 2 * PADDR BLINE ( последняя строка кольцевого буфера) 3 1K 2 * 64 + PADDR LBUFF ( адрес строки для копирования-замещения) 4 : CONTROL ( с--) CREATE DEPTH DUP С, 0 DO DEPTH ROLL С, 5 LOOP DOES> DUP DUP C@ + SWAP DO I 1+ C@ EMIT LOOP ; 6 27 42 CONTROL ( очистка экрана, курсор вверх влево) 7 : 27 EMIT 61 EMIT ROW @ 32 + EMIT COL @ 32 + EMIT ; 8 ( положение курсора в строке Х и столбце Y) 9 ( : PAGE ; : ROW @ COL @ PTC ;)
10 ( Чтобы использовать далее , , , и ) 11 ( необходимо описать их здесь, используя лишь в случае 12 отсутствия прямого позиционирования курсора) 13 ( 28 CONTROL ( перемещение курсора вверх влево) 14 ( : ROW @ ?DUP IF 0 DO LOOP THEN ) 15 ( COL @ ?DUP IF 0 DO LOOP THEN ;)
описания и в блоке 2. Вы можете позднее использовать сам редактор для внесения последующих модификаций, которые допускает ваша ЭВМ, ваша версия Форта и ваш дисплей.
Хотя это весьма простой редактор, он включает в себя некоторые функции, которые полезны и в то же время необычны. Даже если вы удовлетворены редактором, поставленным с вашей версией Форта, для вас будет, вероятно, полезно изучить эту программу, особенно в связи с тем, что мы используем ее в качестве примера того, как следует укладывать и комментировать исходный текст программы.
Если ваша версия Форта содержит пользовательскую переменную SCR, вам следует удалить описание SCR со строки 6. Вы должны также заменить числа 1 и 169 в строке 10 на номера первого и последнего блоков, которые может модифицировать редактор в вашей системе. CONTROL представляет собой слово-описатель, которое формирует слова, предназначенные для генерации последовательностей управляющих символов, число которых задается числом в стеке. Хотя это слово используется здесь только один раз (для в строке 9), оно включено в текст, чтобы позволить вам описать другие управляющие последовательности. Слово определено так, что оно посылает на терминал символ 27, за которым следует код 42.
Описания и в строках б, 7 и 8 выполнены для терминала ADM- 31 и, вероятно, должны быть изменены для вашего оборудования. Описание в строке 7 показывает типовой способ прямого позиционирования курсора. ADM-31 требует ESC- последовательности из четырех символов вида
ESC "=" (строка) + 32 (столбец) + 32,
где строка (row) и столбец (column) равны номерам строки и столбца, куда должен быть установлен курсор (0 - 23 и 0 - 79).
Когда ESC и "=" (ASCII коды 27 и 61) переданы, к кодам строки и столбца добавляется 32, после чего они посылаются на терминал. Редактор использует переменные ROW и COL для отслеживания положения курсора.
Если ваша версия Форта содержит слова для очистки экрана и перемещения курсора, вы захотите использовать описания на строке 9 (не забывайте поместить в скобки прежние описания). В MMSFORTH слово PAGE очищает экран и устанавливает курсор в верхний левый угол, в то время как PTC устанавливает курсор в положение, заданное кодами в стеке. Вам следует подставить эквивалентные слова из вашей версии Форта. Если у вас нет средств для непосредственного управления положением курсора с помощью слова типа PTC или управляющих кодовых последовательностей, вы можете использовать строки 13 и 14,
0 ( 20 июля 85 Экранный редактор NS 03 из 10) 1 : @CURSOR ( --r с ) ROW @ COL @ ; ( вызов строки и столбца ) 2 : ! CURSOR ( r c--) COL ! ROW ! ; ( запоминание столбца и строки) 3 ( Опишите здесь, если возможно, и и используйте в блоках 3 и 4) 4 : 0 COL ! ; ( перемещение курсора в начало строки ) 5 : 0 0 !CURSOR ; ( перемещение курсора вверх влево ) 6 : BABOVE ( --n ) ROW @ 64 * ; ( число байтов в рядах выше курсора ) 7 : BBELOW ( --n ) 16 ROW @ - 64 * 1К + ; ( байт ниже курсора ) 8 : OFFSET ( --n ) BABOVE COL @ + ; ( число байт в буфере перед курсором ) 9 10 : CPOS ( --адр ) PAD OFFSET + : ( адрес курсора ) 11 : LSTART ( --адр ) PAD BABOVE + ; ( адрес начала строки ) 12 : LEND ( --адр ) LSTART 63 + ; ( адрес конца строки ) 13 14 : BLEFT ( --n ) LEND CPOS - ; ( число байтов в строке слева от курсора) 15
но вам нужно сначала описать слова для перемещения курсора в верхний левый угол (без стирания чего-либо), для сдвига курсора вниз на одну строку и для перемещения курсора на одну позицию. Эти слова могут быть описаны с помощью управляющих кодовых последовательностей, которые описаны с привлечением CONTROL. Эти модификации программы для и - единственное, что необходимо, чтобы заставить редактор работать на любом дисплее, который отображает по крайней мере 16 строк по 64 символа в каждой. Изменения, предлагаемые для других блоков, являются чисто рекомендательными, но сделают работу с редактором
0 ( 20 июля 85 Экранный редактор NS 04 из 10 ) 1 ( Если возможно, Опишите ,,, здесь ) 2 ( для того, чтобы использовать вместо в описаниях ) 3 ( в строках 4, 5, 6 и 7. ) 4 : LEFT COL @ 0 > IF -1 COL +! ( или ) THEN ; 5 : RIGHT COL @ 63 < IF 1 COL +! ( или ) THEN ; 6 : UP ROW @ 0 > IF -1 ROW +! ( или ) THEN ; 7 : DOWN ROW @ 15 > IF 1 ROW +! ( или ) THEN ; 8 : ( -- ) BLEFT SPACES : ( стереть конец строки ) 9 : NEWLINE ( -- ) DOWN ; ( курсор в начало следующей строки ) 10 : SHOWLINES ( -- ) @CURSOR 16 ROW @ ( отображение строк ) 11 DO I ROW ! LSTART 64 -TRAILING TYPE LOOP 12 ! CURSOR ; 13 : SHOWBLK ( -- ) @CURSOR SHOWLINES ! CURSOR ; 14 : TYPELINE ( -- ) CPOS BLEFT 1+ OVER OVER TYPE -TRAILING 15 SWAP DROP BLEFT 1+ - SPACES ;
удобнее и производительнее.
Ваш видеодисплей или терминал может воспроизводить управляющие кодовые последовательности для перемещения курсора в начало строки или в верхний угол экрана. Если это так, то вам следует использовать CONTROL для описания так, чтобы перемещать курсор в начало строки, и для установки его вверх влево. Эти слова могут затем использоваться вместо в описаниях и в строках 4 и 5. Это сделает редактирование более удобным и быстрым.
Редактор будет более быстрым и удобным, если ваше оборудование позволяет и вы можете заменить в строках 4-7 словами, описанными в строках с 1-й по 3-ю или ранее. должно перемещать курсор на предшествующую позицию, - на одну позицию вперед.
Аналогично слово должно перемещать курсор вверх на одну строку. Эти слова могут, вероятно, быть описаны с помощью CONTROL. Ваше оборудование может также позволять замену описания в строке 8 на управляющую последовательность, описанную через CONTROL.
0 ( 20 июля 85 Экранный редактор NS 5 из 10 ) 1 2 ; BCLEAR ( -- ) PAD 1K + 1152 BL FILL ; ( очистка буфера ) 3 : SCLEAR ( -- ) PAD 1K BL FILL SHOWBLK ; ( Очистка экрана ) 4 5 6 : LOADBLK ( -- ) SCR @ BLOCK PAD 1K CMOVE ; ( загрузка блока ) 7 : RESTORE ( -- ) LOADBLK SHOWBLK ; 8 9 : (UPDATE) ( -- ) PAD SCR @ BLOCK 1K CMOVE UPDATE ; ( пометка для спасения ) 10 11 : +BLK ( -- ) SCR @ HIGHBLK @ < IF 1 SCR +! RESTORE THEN ; 12 : -BLK ( -- ) SCR @ LOWBLK @ > IF -1 SCR +! RESTORE THEN ; 13 14 : CLRTOP ( -- r c ) @CURSOR ; 15 : WRITETOP ( r c--) TYPELINE !CURSOR ;
0 ( 20 июля 85 Экранный редактор NS 06 из 10 ) 1 : ?BLK# ( -- ) CLRTOP ." * * * BLOCK#Ж " SCR @ . ( блок# ) 2 PAUSE WRITETOP ; 3 4 : UPDATES ( -- ) (UPDATE) ( пометить блок и показать это ) 5 CLRTOP , " * * * UPDATED Block#: " SCR @ . SPAUSE WRITETOP ; 6 7 : ?CLEAR ( --) CLRTOP ." * * *" X-OUT: (B)uffer, (S)creen ?" 8 KEY DUP 66 = IF DROP BCLEAR 9 ELSE 83 = IF SCLEAR !CURSOR EXIT THEN 10 THEN WRITETOP ; 11 : ?EXIT ( f--) CLRTOP ." * * * EXIT:(S)ave. (Q)uit ?" 12 KEY DUP 83 = 13 IF DROP DROP DROP UPDATE FLUSH 1+ EXIT 14 ELSE 81 = IF DROP DROP EMPTY-BUFFERS 1+ EXIT THEN 15 THEN WRITETOP ;
0 ( 20 июля 85 Экранный редактор NS 07 из 10) 1 : OPENUP ( -- ) ( раздвинуть текст в месте буфера, куда указывает курсор) 2 COL @ 64 < IF CPOS DUP 3 1+ BLEFT ) BL CPOS C! THEN ; 4 5 : OPEN ( -- ) OPENUP TYPELINE ; ( раздвинуть текст в месте, указанном курсором) 6 : TRUNC ( - ) CPOS BLEFT BL FILL ; ( укоротить строку) 7 8 : OVERTYPE ( СИМВ-- ) COL @ 64 < ( заместить символ, на который указывает курсор) 9 IF DUP EMIT CPOS С! COL +! ELSE DROP THEN ; 10 11 : INSERT ( СИМВ-- ) OPENUP OVERTYPE TYPELINE ; ( ввести символ ) 12 : DELETE ( -- ) COL @ 64 < ( стереть символ ) 13 IF CPOS 1+ CPOS BLEFT CMOVE BL LEND C! 14 TYPELINE 15 THEN ;
0 ( 20 июля 85 Экранный редактор NS 08 из 10) 1 : CLINE ( -- ) LSTART LBUFF 64 CMOVE ; ( копирование строки в буфер) 2 3 : PLINE ( -- ) LBUFF LSTART 64 CMOVE ; ( выложить строку буфера) 4 @CURSOR TYPELINE !CURSOR ; 5 6 : KLINE ( -- ) ( стереть строку, занеся ее в кольцевой буфер) 7 LSTART BLINE 64 CMOVE ( перенос текущей строки ) 8 LSTART 64 + LSTART BBELOW CMOVE ( сдвинуть буфер вверх ) 9 SHOWLINES ; ( отображение измененных строк ) 10 11 : ILINE ( -- ) ( ввести строку из кольцевого буфера ) 12 LSTART DUP 64 + BBELOW ) ( сдвинуть буфер ) 13 BLINE LSTART 64 CMOVE ( перенести строку с низа буфера ) 14 SHOWLINES ; ( отображение измененных строк ) 15
0 ( 20 июля 85 Экранный редактор NS 09 из 10) 1 CREATE KEYVECTORS ] ( исполнительный вектор команд редактора) 2 LEFT ( А курсор влево ) BLK# ( в номер блока ) 3 CLINE ( С копирует строку ) DELETE ( D стирает символ ) 4 ?ЕХIТ ( Е уход из редактора ) --- ( F ) 5 MODE ( G перевод в новый режим ) --- ( H ) 6 ILINE ( I ввод строки ) --- ( J ) 7 KLINE ( К стирание строки ) -BLK ( L последний блок ) 8 NEWLINE ( М или ENTER, CR+LF ) +BLK ( N следующий блок ) 9 OPEN ( O раздвинуть текст ) PLINE ( Р вставить строку ) 10 ( Q курсор на место ) RESTORE ( R восстановить экран ) 11 RIGHT ( S курсор вправо ) TRUNC ( Т укоротить строку ) 12 UPDATES ( U пометить буфер )----( V ) 13 UP ( W курсор вверх ) ?CLEAR ( X очистить буфер экрана ) 14 --- ( Y ) DOWN ( Z курсор вниз ) [ 15 : KEYDO ( n -- ) 1- 2* KEYVECTORS + @ EXECUTE ;
0 ( 20 июля 85 Экранный редактор NS 10 из 10) 1 : EDITCASE ( флаг симв -- флаг ) 2 DUP 27 < OVER 0 > AND ( легальный управляющий символ? ) 3 IF KEYDO ( если так. то исполняем команду ) 4 ELSE DUP 31 > OVER 127 < AND ( если нет, печатный символ ?) 5 IF I/R @ ( если да - смотрим, каков режим ) 6 IF INSERT ELSE OVERTYPE THEN ( и вводим или замещаем ) 7 ELSE DROP --- ( но, если символ непечатный, игнорируем ) 8 THEN 9 THEN ; 10 11 : EDINIT ( blk -- ) SCR ! EMPTY-BUFFERS LOADBLK SHOWBLK ; 12 : EDITLOOP ( -- ) edinit 0 BEGIN KEY EDITCASE DUP UNTIL 13 DROP ; 14 : EDIT ( blk- ) EDITLOOP ; 15 : E SCR @ EDIT :
Многие терминалы и ЭВМ позволяют описать слово, которое генерирует звуковой сигнал. Это часто делается путем выдачи кода CTRL G или ASCII 7, называемого BEL. Вы можете описать как 7 CONTROL
или что-нибудь эквивалентное. Если это так, вы можете заменить "---" в блоках 9 и 10 словом , и ваша ЭВМ выдаст вам звуковой сигнал, если вы напечатаете неописанный управляющий или другой символ, который нельзя использовать.
Использование экранного редактора
Теперь поговорим об использовании редактора. Загрузите блоки редактора и, чтобы отредактировать блок с номером "n", выдайте команду n EDIT. Оформленный соответствующим образом блок появится на экране, курсор будет помещен в верхнем левом углу. Раз SCR содержит номер нужного блока (а так должно быть после выполнения вами процедур EDIT или LIST для этого блока), вы просто нажимаете "Е" для редактирования данного блока. Используя команды, перечисленные в табл. 12.2, вы сможете изменить содержимое блока различными способами, пока не получите удовлетворительного результата путем спасения этих изменений или игнорируя их. Экспериментируйте с редактором, чтобы привыкнуть к нему.
Таблица 12.2. Команды экранного редактора, клавиши и их функции
Команды позиционирования курсора Q Курсор на место - в верхний левый угол. W Курсор вверх на одну строку. S Курсор вправо. Z Курсор вниз. А Курсор влево. М Курсор в начало следующей строки (клавиша делает то же самое).
Команды редактирования символов
О Раздвижка текста, смещение остальной строки на один символ. D Стирание, смещение остальной строки на один символ влево). Т Укорачивание, стирание строки справа от курсора. G Установка режима переключения между режимами ввода и замещения. В режиме замещения буква, на которую указывает курсор, заменяется напечатанной буквой. В режиме ввода напечатанная буква вводится в текст, остальная часть строки смещается вправо.
Команды редактирования строк
С Копирование текущей строки в буфер строки. Р Извлечение строки из буфера и укладка ее на место текущей строки.
К Стирание строки и помещение ее в кольцевой буфер (строка не теряется). I Вывод строки из кольцевого буфера.
Команды редактирования блока
U Перемещение буфера редактора в блочный буфер и пометка его для записи на диск (UPDATE), R Восстановление буфера редактора (стирание изменений, восстановление последней записанной версии). L Переход к редактирований последнего (предшествующего) блока. N Переход к редактированию следующего блока. В Отображение номера блока, редактирование которого производится. Х Заполнение пробелами (X-out) кольцевого буфера или экранного или никакого. Е Уход из редактора с (без) записи на диск текста (вам будет предоставлен выбор).
Необычной особенностью редактора является наличие так называемого "кольцевого буфера", который позволяет не терять строки текста, подлежащего уничтожению (стиранию). Когда очередная строка удаляется, она копируется в область "ниже" экрана, и, если стерто достаточно много строк, они могут снова появиться на экране снизу. Если вы поупражняетесь с процедурами стирания и восстановления строк, станет понятно, что на экране отображается только верхняя половина главного буфера, используемого редактором. Часть ниже экрана никогда не отображается непосредственно, но ее содержимое может быть просмотрено путем стирания 16 строк (при курсоре на верхней строке), чтобы содержимое буфера попало на экран. Теперь исходное содержимое экрана находится в буфере. Стирание или ввод 16 новых строк "вращает" буфер в прямом или обратном направлении, что позволяет восстановить исходное состояние буфера. Буфер редактора имеет кольцевую структуру, так что строки не могут быть потеряны. Эта мера облегчает стирание до 16 строк, чтобы избавиться от них или чтобы переместить их в другой блок, введя их туда.
Существуют два способа, чтобы подавить заполнение "скрытого" буфера стертыми строками, которые вы не желаете хранить. Команда X-out заполнит все 16 строк скрытого буфера пробелами, если в ответ на запрос вы нажмете клавишу "В". (Если же вы откликнетесь, нажав клавишу "S", пробелами будет заполнен экран.
При нажатии любых других клавиш ничего не произойдет.)
Если вы хотите избавиться от одной строки, укоротите ее перед стиранием, что исключит загромождение буфера ненужными строками. Использование кольцевого буфера станет вполне простым после того, как вы поупражняетесь с ним.
Существует много способов улучшить редактор, но за счет усложнения текста программы и увеличения требуемой памяти. Некоторые модификации будут предложены в гл.13, но кое-что следует упомянуть здесь. Как уже было сказано, чисто косметической является замена "---" в блоках 9 и 10 на , после чего редактор будет сигнализировать вам, если нажата неописанная управляющая клавиша или поступил символ, который не может быть отображен на дисплее. Вы можете использовать управляющие символы из нижнего регистра путем добавления нескольких битов в маску (см. гл.З).
Если вы можете заменить символ курсора на вашем терминале, вам следует изменить описание MODE в блоке 1, чтобы использовать курсор для индикации действующего режима ввод-замещение. А если вы хотите сменить функции командных клавиш редактора, поменяйте местами слова в массиве KEYVECTORS.
Существуют и другие улучшения, которые можно добавить, если ваш дисплей может отображать 24 строки по 80 символов. Например, блок, подлежащий редактированию, может размещаться в центре экрана. Границы блока могут быть выделены, а неиспользуемая область вокруг блока может быть использована для отображения номера блока, режима ввода и даже меню команд редактирования. Редактор может быть модифицирован (с определенным трудом) для использования на терминалах, которые отображают менее 1024 символов, но многие преимущества экранного редактора при этом будут утрачены, так как весь блок не будет отображен на экране.
Ясно, что используемый вами редактор будет сильно влиять на ваше отношение к Форту. В равной мере верно и то, что свойства редактора, кажущиеся одним важными, могут рассматриваться другими как излишние (и наоборот), так как редактирование в высшей степени индивидуальный процесс.В гл.13 исследуются причины, почему редактор написан так, а не иначе, даются комментарии и оригинальные тексты программы. После прочтения главы вы должны знать достаточно о написании редактора, для того чтобы модифицировать то, что мы предложим, или чтобы добавить функции к редактору, поставленному вместе с вашим Фортом. Если вы почувствовали, что некоторые свойства вашего редактора вас не устраивают, вы не должны проклинать Форт - в ваших руках средства, чтобы написать редактор, который удовлетворит вас в полной мере.
Программирование на Форт. Стиль
Форт может изменить ваше представление о программировании. Помимо очевидного отличия Форта от других языков (постфиксная, а не прямая нотация, расширяемость, а не фиксированный набор команд), вы можете почувствовать, что форт открывает новые подходы к решению проблем. В этой главе мы рассмотрим стиль программирования форта как с точки зрения решения проблем с помощью программирования, так и с позиции привычек, делающих программирование легче и продуктивнее.
В этой главе подробно рассматривается редактор, представленный в гл. 12. Вместо того чтобы просто анализировать, как работает редактор в целом, мы проведем вас через программу и опишем цепочку решений, которые вынудили нас написать редактор именно так. Вы узнаете больше, если мы скажем вам, как мотивировалось наше решение при написании программы. чем если бы мы только объяснили, каким образом работает редактор. Программирование - в равной мере искусство и наука, и невозможно просто дать правила, которым можно следовать слепо. Каждый программист решит проблему по-разному, и гибкость форта поощряет эту индивидуальность. Итак, раз вы изучаете редактор, попытайтесь подумать о других возможных путях решения проблемы.
Сначала проблема должна быть рассмотрена в целом. Затем следует разработать слова низкого уровня, которые обеспечат связь между оборудованием ЭВМ и словами высокого уровня, выполняющими многие программные функции. (Конечно, многие из таких слов низкого уровня являются уже частью словаря Форта.) Полезная работа Форт-программы большей частью выполняется словами, которые занимают положение между примитивами и словами высокого уровня, которые связывают все воедино. Создание этих слов,среднего уровня всегда предполагает некоторое число проб и ошибок как в отладочных программах, так и в корректировке неправильного представления об исходной проблеме и о том, как подогнать отдельные части друг к другу, чтобы решить эту проблему. Работа этого метода проб и ошибок станет ясна, если вы посмотрите, как мы написали редактор.
Задание на программу
Если вам рассказали в деталях, что программа должна делать и как она должна это делать, обычно довольно просто ее написать. В действительности вы не пишите программу, а только транслируете набор инструкций из одной формы (словесное описание) в другую (программа Форт). Хотя процесс трансляции может потребовать сосредоточенности, простое написание программы согласно спецификациям - занятие менее увлекательное и менее творческое, чем выполнение всей работы с самого начала. Если вы отвечаете за задание и за написание программы, вы имеете возможность изменить функцию программы в процессе ее написания. Это то место, где начинается творчество, и Форт этому поможет. Так как вы описали слова, то лучше представляете пути решения ваших проблем и возможности вашей программы.
Программирование является упражнением в решении проблем. Нужно не только понять задачу, но и заставить ЭВМ выполнять процедуру, необходимую для решения задачи. Это включает в себя несколько довольно расплывчатых этапов: описание всей проблемы и цели; разделение проблемы на меньшие задачи, решаемые независимо, и, может быть, последующее деление таких задач; написание программы для этих мелких задач; отладка программы, возможно изменение алгоритма для решения главной проблемы или составляющей ее части и снова отладка; сборка составных частей; окончательная отладка всего пакета. Многие языки маскируют этот естественный процесс: описывается набор алгоритмов, определяющих все, что должна делать программа, а затем пишется программа с минимальными по возможности переделками и отладкой.
Форт допускает другой подход; внутренняя природа решения проблемы при этом более прозрачна. Мелкие проблемы могут решаться одна за другой, проверяться, корректироваться и, может быть, если надо, при этом модифицируются другие части программы как следствие обстоятельств, выявленных в процессе отладки. Это возможно в Форте, так как в нем весьма быстро и легко можно изменить и проверить описание слова или нескольких слов.
Мы использовали эту итеративную модификацию слов несколько раз в предшествующих главах частично как способ показа того, как строить слова на базе основополагающих идей, частично для того, чтобы привести примеры процесса написания Форт-программ. Составление программы на Форте включает в себя написание некоторого числа коротких описаний, использование их в других описаниях и, наконец, сборку всей системы в целом обычно в виде одного слова, которое исполняет программу. Но прежде чем что-либо из этого можно было сделать, вы должны иметь очень точное представление о том, что должна делать программа, т. е. что является главной задачей. Проблема должна быть сформулирована таким образом, чтобы ее можно было поделить на задачи, которые решаются путем описания слов Форта.
Это возвращает нас снова к заданию на программу к описанию того, что должна делать программа. Описание высшего уровня представляет ваши интуитивные цели: что бы вы хотели, чтобы делала программа? Точное описание ваших намерений является наиболее важным шагом в написании программы, так как диапазон ваших целей определяет то, насколько будет полезна ваша программа или даже следует ли ее вообще писать. Этот этап постановки задачи и ошибка здесь будет усиливаться по мере вашей работы и может сделать вашу проблему неразрешимой.
Мы решили написать экранный редактор для этой книги не потому, что было бы полезно для читателей его иметь. Много важнее то, что сам процесс написания редактора является хорошим примером процесса написания любой программы средней сложности. Мы написали редактор для работы по возможности с простейшим терминалом, так как мы хотели сделать его как можно более универсальным, имея в виду, что это сильно упростит его использование читателями с другими типами ЭВМ. Но это ограничение сделало также алгоритм более строгим, а процесс программирования и саму программу более интересными. Таким образом мы не могли полагаться на известные характеристики ЭВМ или терминала, что сделало бы работу легче.
Какова же задача экранного редактора?
Конечно, концепция экранного редактора уже включает многое из того, что должна делать программа. Основной функцией редактора являются вход в режим и модификация текста программы, но существует много способов выполнения этого. Фактическим стандартом для входа в экранный редактор Форта является n EDIT. Эта команда должна отображать содержимое блока n, обеспечивать модификацию содержимого различными путями и последующую запись на диск. Текст изменяется путем перемещения курсора в любом направлении и ввода или стирания символов, на которые указывает курсор. Мы бы также хотели, чтобы редактор позволял нам копировать, замещать, вставлять и стирать строки текста, на которые указывает курсор, а также выполнять некоторые другие операции. Мы можем теперь установить определенное число точно заданных, но достаточно общих спецификаций того, что должен делать редактор.
1. Позволять редактировать блок путем выдачи команды EDIT, отображающей содержимое блока в виде 16 строк по 64 символа и установку курсора в верхний левый угол экрана. 2. Позволять перемещать курсор вправо, влево, вверх и вниз путем нажатия определенных клавиш. Позволять стирание символов нажатием другой клавиши. 3. Позволять замещать символы, на которые указывает курсор, если включен соответствующий режим. 4. Позволять переходить в режим ввода, когда символ не печатается поверх существующего, отмеченного курсором, а вводится так, что остальные символы в строке смещаются, освобождая место для нового. Позволять также переход в режим "замещение" (предположительно переключение из режима в режим производится с помощью командного символа). 5. Позволять стирать строки текста и заносить их в буфер (используя другой управляющий символ) так, чтобы имелась возможность ввести где-то еще. 6. Позволять установить флаг записи на диск (опять же посредством управляющего символа), используя слово UPDATE, чтобы редактируемый блок мог быть записан. 7. Позволять уходить из редактора (посредством еще одного управляющего символа) с последующим уходом в Форт-интерпретатор.
Хотя многие программисты, может быть, и не записывают такие формальные спецификации, они, несмотря на это, держат в уме достаточно точно определенные цели. Мы теперь имеем хорошо описанные цели, но еще не знаем, как разбить программу на слова Форта. Общая задача должна быть сначала разделена на меньшие части. Существует много способов, как это сделать, а для редактора это может быть сделано в уме. Например, мы знаем, что должны разделять текстовые и управляющие символы, выполнять определенные процедуры согласно тому, что требуют управляющие символы, и осуществлять переключение между режимами ввода и замещения. Часто полезно представить проблему на бумаге. Это можно сделать разными способами. Традиционно используется блок-схема. На рис. 13.1 представлена блок-схема редактора. Но блок-схема занимает много места на бумаге и ее неудобно создавать и изменять. Словесное описание функций редактора, как это показано в табл. 13.1, может быть лучше. (Для подготовки таких описаний особенно удобен текстовый редактор, который предоставляет широкие возможности для внесения изменений.)
Так как вы программируете на Форте, то можете предпочесть запись описания в форме псевдофорт, как в табл. 13.2. Слова псевдофорт могут отличаться от тех, что будут использоваться в програме (хотя они могут и предвосхитить имена реальных слов), они имеют то преимущество, что делят всю проблему наилучшим образом.
Таблица 13.1. Словесное описание редактора
Загрузить редактируемый блок в блочный буфер. Отобразить блок, поместить курсор влево вверх, Начать цикл. Получить символ с клавиатуры, Это управляющий символ? Если да, то выполнить команду редактора. Если нет, проверить, является ли он отображаемым, Если да, то проверить - мы в режиме ввода? Если да, то ввести символ в текст. Если нет, то произвести замещение символа. Если нет, то символ игнорируется. Продолжить цикл, пока придет команда выхода из системы
Как мы увидим, существует весьма яркая параллель между табл. 13.2 и словами, которые будут использоваться для выполнения основной работы редактора.
Существует много методов деления программных заданий, начиная от формализма блок-схемы и вплоть до идей, которые вы держите в уме. Вы можете считать полезной комбинацию слов и диаграмм. Важно, чтобы деление на вторичные задачи было выполнено до начала написания программы. Форт требует меньше формализма при разделении проблемы на части, чем многие другие языки, но решение любой задачи (безразлично - программной или нет) выиграет, если она сначала обдумана. Хотя ни одно из этих описаний редактора не является полным, все они задают базовую стратегию решения проблемы. Каждое нажатие клавиши должно обрабатываться индивидуально, для того, чтобы отделить команды от букв, вводимых в текст. Хотя команды редактора не стандартизованы, мы имеем отдельный механизм для их ввода (используя управляющие клавиши). Мы также решили включить два режима (ввод и замещение), позволяющие вводить символы двумя различными способами. Редактор будет работать в бесконечном цикле до тех пор, пока не будет выдана команда EXIT ("выход"), по которой измененный текст будет записан (или нет) на диск. Мы можем теперь рассказать вам, как мы подходили к написанию программы.
Мы начали с уточнения некоторых деталей нашего общего плана. Редактирующие команды, выбранные кодом управляющей клавиши, было бы легко описать, если бы использовался исполнительный вектор. Это делает команды легко задаваемыми, а для пользователя легко изменяемыми по желанию. Простейший путь выполнения выбора ввод-замещение открывает введение флага, который будет устанавливаться одной из команд редактора. Редактор выполняет большинство своих операций в пределах цикла BEGIN... UNTIL отчасти потому, что нужен именно бесконечный цикл (позволяющий выполнять любое число
Таблица 13.2. Описание редактора на псевдофорт
: EDIT ( n -- ) BLOCK-TO-BUFFER BLOCK-TO-SCREEN CURSOR-TO-START BEGIN FETCH-CHARACTER CONTROL-CHAR? IF DO-EDIT ELSE PRINTABLE-CHAR? IF INSERT-MODE? IF INSERT ELSE OVERTYPE THEN ELSE DROP THEN THEN END-FLAG UNTIL ;
редактирующих операций), отчасти из- за того, что EXIT только меняет флаг, лежащий в стеке, для того чтобы выйти из цикла при UNTIL. В нашей концепции необходимо еще принять решение о многих других редактирующих командах и об использовании памяти ЭВМ.
Мы имеем выбор позволить редактору непосредственно модифицировать содержимое блочного буфера или перенести его содержимое в другую часть памяти (PAD и далее) и редактировать его там. Был выбран последний метод, так как он позволяет "отменять" изменения и таким образом обеспечивать некоторую защиту против изменений на диске, внесенных случайно. Карта распределения памяти показана в табл. 13.3. Слово BLOCK используется для загрузки блока, номер
Таблица 13.3. Предварительные соображения о распределении памяти для редактора
Блочный Переносится PAD Отображается Терминал буфер в на
Первая строка --> Первая строка --> Первая строка Вторая строка --> Вторая строка --> Вторая строка ............. --> ............. --> ........... Последняя строка --> Последняя строка --> Последняя строка
которого хранится, в стеке, в блочный буфер, откуда 1024 символа переносятся командой CMOVE в PAD, где будет проводиться редактирование. Когда нужно сохранить изменения, 1024 символа из PAD переносятся в блочный буфер, который помечается для сохранения оператором UPDATE. Блок из 1024 символов отображается на экране терминала. Положение курсора на терминале и положение соответствующего символа в PAD оказываются связанными так, что изменение положения курсора означает изменение указания на символ в тексте, лежащем в массиве выше адреса PAD. Положение курсора логически характеризуется кодами строки и столбца, номера которых отсчитываются от левого верхнего угла дисплея. Эти коды могут быть затем использованы для получения любой другой информации, связанной с положением курсора.
Одной из особенностей редактора, в которой мы нуждались, была возможность вводить и стирать строки текста, на которые указывает курсор.
Память сразу за последней отображенной строкой (начиная с PAD+1024) может использоваться для хранения бесконечного числа стертых строк, которые могут быть вставлены в отображаемый текст, если это желательно.
Таблица 13.4. Использование памяти редактора
Блочный Перено- PAD Отобража- Терминал буфер сится в ется на
Первая строка --> Первая строка --> Первая строка Вторая строка --> Вторая строка --> Вторая строка .......... --> ............. --> ............. Последняя строка --> Последняя строка --> Последняя строка 1-я скрытая строка 2-я скрытая строка 16-я скрытая строка Дополнительная строка Строка для операций копирования-замещения
Представляется разумным включить 16 строк в скрытый буфер так, чтобы содержимое всего блока могло быть стерто и затем восстановлено где-то еще. Если процедуры ввода и стирания строки были спроектированы для работы в кольцевом режиме, ни одна строка не будет потеряна. Так как отображаемый и скрытый буфера смежны, программа для реализации этого кольцевого буфера будет весьма проста. Окончательное распределение памяти в редакторе, выбранное нами, включая дополнительную строку для процедур копирования-замещения, показано в табл. 13.4.
Представляется полезным добавить кольцевой буфер к редактору, и это можно рассматривать в качестве примера для модификации других редакторов, не имеющих таких возможностей. (Как это работает, будет показано в деталях позднее.)
Этого обзора было бы достаточно, чтобы начать писать редактор, но мы добавили еще два ограничения. Мы знали, что надо свести число терминальных операций к минимуму, чтобы редактор работал с наиболее широким спектром ЭВМ. После некоторого размышления мы решили спроектировать редактор, который работает, используя только две терминальные команды: 1 - очистка экрана и 2 - установка курсора на заданную строку и столбец. Было ли это практичным, стало ясным после того, как редактор был частично написан, но это была цель, за которую мы боролись. Позднее мы ограничили нашу задачу написанием редактора для минимального размера экрана 16 строк по 64 столбца, что упростило текст программы.
Это завершило спецификацию " высокого уровня" для редактора; теперь мы в основном поняли, как должен работать редактор. Продолжать более общее описание функций редактора без реального решения некоторых проблем "нижнего" уровня было бы в заметной мере пустой тратой времени. Например, вдруг число слов управляющих терминалом, нельзя сократить до 2? Теперь мы были готовы начинать писать Форт-программу. Для большинства языков программирования требуется много, больше планирования, так как модульная структура, присущая словам Форта, им недоступна. Если вы изучаете Фортран, Кобол или большинство других языков, в ваших интересах спланировать все как можно подробнее, прежде чем написать строчку программы. Например, вам надо выбрать, какой управляющей клавише присвоить какую функцию, и вы должны мысленно спроектировать все управляющие функции терминала. Процесс разработки при этом будет дольше и конечный продукт, вероятно, не будет столь уж хорош. Форт позволяет писать и отлаживать программы методом проб и ошибок или на интерактивной основе. Это выявляет многие ошибки прежде, чем они получат шанс распространиться непредсказуемым путем через программу. Мы не будем описывать все пробы и ошибки, которые были совершены при написании редактора, но попытаемся представить вам идеи некоторых шагов, которые предпринимались.
Закладка фундамента
До сих пор мы строили замок грез, лишенный фундамента. Наши размышления привели к ясному пониманию того, какой редактор мы хотим создать, и к конкретным идеям построения некоторых его структур высокого уровня. Но прежде чем мы сможем проверить какие-либо наши идеи, мы должны определить слова, на которых будет базироваться все остальное. Должны быть описаны переменные и константы (хотя некоторые из них могут быть добавлены позднее). Должны быть зафиксированы некоторые адреса памяти. Нужно описать слово или слова, управляющие функциями терминала. Должна быть решена проблема, как изменить и запомнить положение курсора на экране терминала и соответствующие позиции в буфере редактора.
Эти и другие аспекты работы редактора должны быть рассмотрены на ранних этапах.
Хотя мы опишем окончательный текст программы блок за блоком (чтобы сделать ее более понятной), сам редактор пишется не так. Мы сначала написали редактор, который может делать очень немногое, чтобы кое-что проверить. Базовые слова дописывались позднее в процессе написания программы и были помещены в первые два блока, чтобы сохранить иерархию функций слов от наименее к наиболее сложным и сделать программу простой для понимания. Мы группировали слова по их функциям настолько, насколько возможно, в некоторых случаях проводя переукладку после того, как программа была функционально завершена.
Кто-то может сказать, что текст программы написан слишком плотно, но мы должны были экономить место в книге. Нужно оставить достаточно свободного места в программе, чтобы можно было выполнить переукладку слов согласно их функциям, а также чтобы осталось место для изменений и добавлений. Если вы делаете много изменений в тексте программы, вам нужно место для ее расширения. Первые два блока редактора содержат описания констант, переменных, флагов и адресов некоторых буферов и базовые слова для управления курсором и терминалом. Переменные ROW и COL (строка и столбец, где находится курсор) и I/R (флаг ввод-замещение) были действительно описаны первыми. Остальные были добавлены по мере необходимости. Позднее в процессе программирования с целью экономии памяти были описаны некоторые числа (BL, 1K, 64 и т.д.). PDELAY и SDELAY представляют собой два цикла, организующие задержки для отображения информационных сообщений.
0 ( 20 июля 85 Экранный редактор NS 01 из 10) 1: TASK ; DECIMAL 2 3 32 CONSTANT BL 1024 CONSTANT 1K 4 64 CONSTANT 64 63 CONSTANT 63 5 10000 CONSTANT PDELAY 5000 CONSTANT SDELAY 6 VARIABLE SCR VARIABLE ROW 7 VARIABLE COL VARIABLE I/R 8 VARIABLE LOWBLK VARIABLE HIGHBLK 9 ( запоминание первого и последнего блоков, которые можно редактировать на вашей ЭВМ) 10 1 LOWBLK ! 169 HIGHBLK !
Они являются константами, чтобы можно было изменить задержки, не изменяя текста программы.
Константы и переменные должны быть собраны в начале программы (или в начале секций большой программы) для того, чтобы их было легко найти и изменить. Переменная SCR была описана для тех версий Форта, где этого слова нет. Переменные LOWBLK и HIGHBLK были добавлены для того, чтобы редактор не имел доступа к тем блокам, к которым не следует. Они могут быть изменены в тексте программы или с клавиатуры после того, как редактор загружен. 11 : --- ; ( слово, не выполняющее никакой работы для исполнительного вектора) Это слово (названное так, чтобы бросаться в глаза в тексте программы) позволяет использовать неописанные управляющие клавиши в исполнительном векторе KEYVECTORS (блок 9). Это слово также было описано, когда был описан KEYVECTORS, но помещено здесь, так как это слово низкого уровня. 12 : MODE (-) I/R @ 0= I/R ! ; ( выбор ввод-замещение) Каждый раз, когда используется MODE, оно переводит значение I/R из состояния истинно в состояние ложно или наоборот. MODE действует как переключатель, который выбирает режим ввода символов. 13 : DELAY ( n - ) 0 DO LOOP ; ( Задержка на n циклов) 14 : PAUSE ( - ) PDELAY DELAYS ; ( Длинная задержка) 15 : SPAUSE ( - ) SDELAY DELAYS ; ( Короткая задержка) Эти циклы задержки были описаны позднее, но перемещены в начало текста программы. PAUSE и SPAUSE были описаны как отдельные слова, чтобы в дальнейшем сэкономить место в программе. Основополагающие слова размещены в следующем блоке. 0 ( 20 июля 85 Экранный редактор NS 02 из 10) 1 : PADDR ( n - ) CREATE , DOES> @ PAD + ; 2 : 1K 2 * PADDR BLINE ( Последняя строка кольцевого буфера) 3 : 1K 3 * 64 + PADDR LBUFF ( Адрес строки для копирования-эамещения)
Слово-описатель PADDR ("PAD ADDRESS" = адрес PAD) используется для формирования двух адресов, необходимых при работе с буферами редактора. Для симметрии мы могли бы описать также О ADDR OLINE, чтобы получить адрес PAD (начало буфера редактора), но в тексте программы использовался PAD для того, чтобы подчеркнуть, какой именно адрес памяти используется.
Команды 1K 2 * и 1K 2 * 64 + применены для вычисления значений 2048 и 2112, так как это легче понять в терминах блоков и строк, которые, в свою очередь, дают лучшие представление о структуре буфера редактора. Заметьте, что мы не заботимся о времени вычислений, так как они выполняются только раз, во время компиляции. 4 : CONTROL ( с --) CREATE DEPTH DUP С, 0 DO DEPTH ROLL С, 5 LOOP DOES> DUP DUP C@ + SWAP DO I 1+ C@ EMIT LOOP ; 6 27 42 CONTROL ( Очистка экрана, курсор вверх влево)
CONTROL - слово-описатель, сформированное для описания слов, которые генерируют управляющие коды и ESC-последовательности. На терминале ADM 31 работает 27 42 CONTROL
так как ESC-последовательность ESC * (ASCII 27 и последующий ASCII 42) используется для очистки экрана. Хотя CONTROL имеет встроенное описание, это слово упрощает описание слов управления терминалом и делает текст программы более читаемым. Описание : 27 EMIT 42 EMIT ; также будет работать, но применение слова-описателя CONTROL при наличии нескольких описаний сэкономит память. Слова, имеющие отношение к функциям терминала, снабжены именами, заключенными в треугольные скобки, чтобы они выделялись в тексте программы. 7 : 27 EMIT 61 EMIT ROW @ 32 + EMIT COL @ 32 + EMIT ; 8 ( Положение курсора в строке Х и столбце Y) Это описание демонстрирует типовой образец слова, осуществляющего прямое позиционирование курсора. Оно работает с ADM 31 и, конечно, будет переделано для других терминалов или ЭВМ. 9 ( : PAGE ; : ROW @ COL @ PTC ; ) Это показывает, как можно описать и , применяя слова Форта, а не ESС-последовательности (эти примеры используют слова MMSFORTH). 10 ( Чтобы использовать далее . , , и ) 11 ( необходимо описать их здесь. Используется лишь в 12 случае отсутствия прямого перемещения курсора) 13 ( 28 CONTROL (перемещение курсора вверх влево) 14 ( : ROW @ ?DUP IF 0 DO LOOP THEN ) 15 ( COL @ ?DUP IF 0 DO LOOP THEN ; )
Требуется только одно описание ("cursor-X-Y") и ("clear"); это альтернативное описание медленнее и основано на
("downward-line-feed" = переход на одну строку вниз) и ("advance" = сдвиг курсора на одну позицию), которые описываются оператором CONTROL. Это слово следует использовать, только если терминал позволяет очень примитивное управление курсором. Чтобы сделать программу легко читаемой, все слова, описанные CONTROL, нужно сгруппировать вслед за описанием самого слова CONTROL.
В блоке 3 продолжается описание слов, управляющих курсором. 0 ( 20 июля 85 Экранный редактор NS 03 из 10) 1 : @CURSOR (- r c ) ROW @ COL @ ; ( Вызов строки и столбца) 2 ; !CURSOR ( r c -) COL ! ROW ! ; ( Запоминание строки и столбца)
@CURSOR и 1CORSOR были описаны, чтобы упростить вычисления и запоминание положения курсора. Это было сделано после того, как мы выяснили, что комбинации, которые они представляют, появляются в нескольких местах текста разрабатываемой программы. Эти слова довольно часто встречаются попарно в одном и том же описании и делают программу легко читаемой. Представление положения курсора через номера колонок и столбцов (а не только через номера байта) упрощает перемещение курсора и описание некоторых последующих слов, 3 ( опишите здесь, если возможно, и и используйте в блоках 3 и 4) 4 : 0 COL ! ; ( Перемещение курсора в начало строки) 5 : 0 0 !CURSOR ; ( Перемещение курсора вверх влево) Слова ("start-line" - начало строки) и ("home-cursor" - курсор на место) могут быть описаны с помощью CONTROL (если ваш терминал поддерживает эти функции) и использованы вместо в этих определениях. Заметьте, что положение курсора должно отслеживаться в двух местах: его реальное положение запоминается в ROW и COL, но курсор должен быть также помещен в соответствующую точку на экране. 6 : BABOVE ( - n ) ROW @ 64 * ( Число байт в рядах выше курсора) 7 : BBELOW ( - n ) 16 ROW @ - 64 * 1К + ; ( Число байт ниже курсора) 8 : OFFSET ( - n ) BABOVE COL @ + ; ( Число байт в буфере перед курсором) 9 Неудачно названные BABOVE ("bytes-above" - число байтов до) и BBELOW ("bytes-below" - число байтов после) - слова, которые пригодятся позднее.
Они вычисляют число байтов (в буфере) в строках выше курсора и число байтов (также в буфере) в строке, где находится курсор, а также в строках ниже его (включая весь кольцевой буфер). Слово OFFSET кладет в стек число байтов, отсчитанное от начального положения курсора (верхний левый угол). Так как это слово используется только в CPOS, OFFSET является излишним, оно лишь делает описание CPOS более легким для понимания. Обратите внимание, что любое изменение величины ROW или COL поменяет число, выдаваемое в стек оператором OFFSET (и CPOS). 10 : CPOS ( - адр) PAD OFFSET +: ( адрес курсора) 11 : LSTART ( - адр) PAD BABOVE +: ( адрес начала строки) 12 : LEND ( - адр) LSTART 63 +: ( адрес конца строки) Слова CPOS ("cursor-position" - положение курсора), LSTART ("linestart - начало строки) и LEND ("line-end" - конец строки) выдают три адреса в буфере, которые широко используются в последующем тексте редактора. 14 : BLEFT ( -n ) LEND CPOS - ; ( Число байтов в строке слева от курсора) 15 BLEFT ("bytes-left" - байтов осталось) необходимо для последующих слов, которым нужна информация о числе байтов (символов) между курсором и концом строки.
Заметьте, что положение курсора в буфере задается несколькими способами. Мы знаем его строку и столбец, число байтов до и после него в PAD-буфере, число байтов от начала буфера, его адрес в памяти, адрес начала и конца строки, где он находится, и число байтов, лежащих в строке после курсора. Мы теперь имеем полный набор слов, позволяющий нам описать большое число слов высокого уровня в блоке 4 0 ( 20 июля 85 Экранный редактор NS 04 из 10) 1 ( Если возможно, опишите , , и здесь для того, чтобы использовать их) 2 ( вместо в описаниях на) 3 ( строках 4, 5, 6 и 7. ) 4 : LEFT COL @ 0 > IF -1 COL +! (или ) THEN : 5 : RIGHT COL @ 63 < IF 1 COL +! (или ) THEN ; 6 : UP ROW @ 0 > IF -1 ROW +! (или ) THEN ; 7 : DOWN ROW @ 15 < IF 1 ROW +! (или ) THEN ;
Первая и наиболее важная вещь, которую мы хотели бы сделать, это двигать курсор в четырех направлениях, но не покидая экрана.
Слова и т.д. должны быть описаны с помощью CONTROL, если ваш терминал или ЭВМ позволяют это, и затем использованы вместо в этих словах. Заметьте, что ROW и COL должны так изменяться, чтобы отслеживать соответствие положения курсора и адреса в буфере редактора. 8 : ( --) BLEFT SPACES ; ( Стереть конец строки) Было бы лучше описать оператор ("erase-end-line" - стереть конец строки) с помощью оператора CONTROL при условии, что ваш терминал позволяет это. 9 : NEWLINE ( -- ) DOWN ; ( Курсор в начало следующей строки)
Имени NEWLINE будет поставлена в соответствие клавиша (которая формирует код, эквивалентный CTRL-M), чтобы установить курсор в начало следующей строки. Это слово имитирует функцию "возврат каретки + перевод строки" (). Однако в редакторе ВК только перемещает курсор; это не должно вызывать перемещение текста на экране. 10 : SHOWLINES ( - ) @CURSOR 16 ROW @ ( отображение строк) 11 DO I ROW ! LSTART 64 -TRAILING TYPE LOOP 12 !CURSOR ; 13 : SHOWBLK ( -- ) @CURSOR SHOWLINES ICURSOR ;
Оператор SHOWLINES отображает строку, где находится курсор и все последующие строки до конца блока. Когда изменена только нижняя часть экрана, это экономит время по сравнению с отображением заново всего экрана. Оператор SHOWBLK использует оператор SHOWLINES для отображения всего блока после перемещения курсора влево вверх. Обратите внимание на то, как @CURSOR и ICURSOR используются в паре для занесения в стек кодов исходного положения курсора и для последующего в конце работы слова восстановления его позиции. 14 : TYPELINE ( -- ) CPOS BLEFT 1+ OVER OVER TYPE -TRAILING 15 SWAP DROP BLEFT 1+ -SPACES ;
Оператор TYPELINE пропечатывает все символы, начиная* от курсора и до конца строки. -TRAILING используется для определения числа пробелов, необходимых для того, чтобы избавиться от лишних символов, которые в противном случае появятся за пределами 64- символьной строки.
Мы имеем теперь к концу четвертого блока описания почти всех слов низкого уровня, в частности слов, оперирующих адресами памяти, перемещающих курсор и отображающих текст на экране.
Только некоторые из действительно редактирующих команд были описаны в первых четырех блоках, но в следующих четырех блоках мы опишем остальные.
Упражнения
1. Почему строка 1 блока 1 записана следующим образом : TASK ; DECIMAL 2. Почему лучше описать LOWBLK и HIGHBLK как переменные, а не как константы ? 3. Почему PDELAY и SDELAY описаны как константы, а не использованы непосредственно числа в описаниях PAUSE и SPAUSE? 4. Приведите две причины для выделения DELAYS из описаний PAUSE и SPAYSE. 5. Предположим, что вы используете терминал ADM 31 и хотите добавить слова PAGE и РТС из MMSFORTH в ваш Форт. Сформулируйте их описание. 6. Предположим, что вы работаете с терминалом, на котором работают ESC-последовательности: 27 13 Сместить курсор на одну строку вниз, 27 14 Сместить курсор вперед на один символ, 27 15 Сместить курсор вверх влево (HOME) Опишите , и в строках 10, 11 и 12 блока 2. Что бы бы теперь сделали, чтобы использовать описание , приведенное в строках 14 и 15 ? 7. Почему модификации, предложенные в строках 1, 2 и 3 блока 4, были бы предпочтительнее, если они возможны?
Основные положения
Теперь, когда фундамент заложен, в следующих четырех блоках может быть описано большинство редактирующих команд. Эти операторы основаны большей частью на словах, которые мы уже описали, а не на словах, описанных в Форте. Наибольшее удовольствие доставляет написание программ Форта среднего и наивысшего уровней, так как вы при этом используете в основном язык, созданный вами самими. Но на этой фазе проявляются противоречия и пропуски, допущенные в написанных ранее программах. Это как раз то время, когда вы оцените оставленное вами свободное место в предшествующих блоках для расширения возможностей и внесения изменений. Пока мы писали редактор, блоки 1-4 занимали на самом деле 7 блоков. Порядок, в котором описываются слова промежуточного уровня, до некоторой степени произволен, поскольку большинство из них являются независимыми. Разумно помещать связанные слова в одном и том же блоке хотя бы для того, чтобы легче было их найти и отредактировать.
0 ( 20 июля 85 Экранный редактор NS 05 из 10) 1 2 Ж BCLEAR ( -- ) PAD 1K + 1152 BL FILL : ( Очистка буфера) 3 : SCLEAR ( -- ) PAD 1K BL FILL SHOWBLK ; ( Очистка экрана) 4 Имеются слова для очистки содержимого кольцевого буфера и дисплея. 5 6 : LOADBLK ( - ) SCR @ BLOCK PAD 1K CMOVE ; ( Загрузка блока) 7 : RESTORE ( - ) LOADBLK SHOWBLK ; 8 Слово LOADBLK ("load-block" - загрузить блок) производит загрузку в блочный буфер, а затем и в буфер редактора. RESTORE загружает буфер редактора и отображает его содержимое. RESTORE аннулирует редактирование, проведенное с момента, когда блок был последний раз отредактирован и сохранен. Функция RESTORE оказалась полезной сама по себе при написании программ (она отсутствовала в исходной спецификации) и оказалась практичной, так как блочный буфер не используется непосредственно при редактировании. 9 : (UPDATE) ( - ) PAD SCR @ BLOCK 1K CMOVE UPDATE ; ( Пометка для сохранения)
Это слово копирует отображаемую часть буфера редактора в блочный буфер и помечает для сохранения. Его имя в скобках предполагает, что оно тесно связано с функцией UPDATE, описанной ниже. 10 11 : +BLK ( - ) SCR @ HIGHBLK @ < IF 1 SCR +! RESTORE THEN ; 12 : -BLK ( - ) SCR @ LOWBLK @ > IF -1 SCR +! RESTORE THEN ;
Эти слова используются для начала редактирования следующего или предшествовавшего блока. Заметьте, что блок, который вы покидаете, должен быть помечен для сохранения (UPDATE), если изменения, сделанные в нем, нужно записать на диске, прежде чем вы продолжите работу. HIGHBLK и LOWBLK отсутствовали в исходной спецификации; они были добавлены позднее для того, чтобы предотвратить доступ редактора к запретным или несуществующим блокам. 13 14 : CLRTOP ( - r c ) @CURSOR ; 15 : WRITETOP ( r с -) TYPELINE !CURSOR ;
Слова CLRTOP ("clear-top" - очистить верх) и WRITETOP были описаны для упрощения написания подсказок, которые используются в четырех словах следующего блока. При совместном использовании они запоминают положение курсора в стеке, заполняют верхнюю строку на терминале пробелами, восстанавливают верхнюю строку и устанавливают курсор туда, где он был.
Четыре слова в блоке 6 выдают информацию оператору и (или) обеспечивают выбор из меню. 0 ( 20 июля 85 Экранный редактор NS 06 из 10 ) 1 : ?BLKS ( -- ) CLRTOP ." * * * BLOCK#: " SCR @ . ( блок# ) 2 PAUSE WRITETOP ;
?BLK# ("block-number" - номер блока) отображает текущий номер блока на верхней строке, ждет некоторое время, а затем восстанавливает верхнюю строку на дисплее. Это слово просто напоминает оператору номер блока, который редактируется. Длительность паузы задается константой PDELAY. 4 : UPDATES ( -- ) (UPDATE) ( пометить блок и показать это) 5 CLRTOP ." * * * UPDATED Block#: " SCR @ . SPAUSE WRITETOP ;
Оператор UPDATES позволяет вам пометить блок для сохранения и затем подтвердить эту операцию, используя более короткую паузу, чем в ?BLK#. Отображение для этого необязательно, но без него у вас не будет средств убедиться, что пометка (UPDATE) действительно произошла. 6 7 : ?CLEAR ( - ) CLRTOP ." * * * X-OUT; (B)uffer, (S)creen ?" 8 KEY DUP 66 = IF DROP BCLEAR 9 ELSE 83 = IF SCLEAR !CURSOR EXIT THEN 10 THEN WRITETOP;
Это и следующее слова представляют субменю, которое позволяет присвоить заданные функции определенным управляющим клавишам в основной программе. В 7CLEAR ("очистить?") предполагается три варианта: очистить буфер, экран или ничего (ничего, если вы нажмете любую клавишу, кроме "В" или "S"). Верхняя строка восстанавливается, если информация на экране не была стерта (что показывает, насколько полезной может быть операция EXIT). В любом случае курсор будет установлен туда, где он был ранее. 11 : ?EXIT ( f - ) CLRTOP ." * * * EXIT; (S)ave, (Q)uit?" 12 KEY DUP 83 = 13 IF DROP DROP DROP UPDATE FLUSH 1+ EXIT 14 ELSE 81 = IF DROP DROP EMPTY-BUFFERS 1+ EXIT THEN 15 THEN WRITETOP ;
Это слово, используемое для ухода из редактора, работает во многом так же, как и ?CLEAR, представляя три варианта, прежде чем делать что-либо. Слово 1+ становится понятным, когда вы заглянете в EDITLOOP в последнем блоке, оно устанавливает флаг, позволяющий выйти из редактора.
EXIT используется здесь так же, как и ?CLEAR, для предотвращения в WRITETOP восстановления верхней строки при выходе из редактора.
Следующий блок содержит слова, необходимые для ввода текста и изменения его различными путями. Существуют два основных способа или режима, используемые для ввода символов: замещение символа, на который указывает курсор, или раздвижка текста (перемещение остальной части строки на одну позицию вправо) перед вводом нового символа. 0 ( 20 июля 85 Экранный редактор NS 07 из 10) 1 : OPENUP ( - ) ( Раздвинуть текст в месте буфера, куда указывает курсор) 2 COL @ 64 < IF CLOS DUP 3 1+ BLEFT ) BL CPOS C! THEN ; 4 : OPEN ( -- ) OPENUP TYPELINE ; ( Раздвинуть текст в месте, указанном курсором)
OPENUP необходимо для ввода пробела в позицию, указанную курсором, перед вводом символа. Это слово воздействует только на буфер, ничего не отображая на экране. Если вы не хотите терять конец строки, который выдвигается в правую сторону дисплея, проверяйте последний символ в строке (используя LEND @), чтобы убедиться, что это пробел, прежде чем позволить слову OPENUP делать чтобы-то ни было. Слово OPEN позволяет вам вводить пробел в позицию, отмеченную курсором, смещая текст на один символ вправо. Это побочный продукт слова OPENUP, которое все равно нужно было написать для INSERT. 6 : TRUNC ( -) CPOS BLEFT BL FILL ; ( Укоротить строку)
TRUNC ("отбросить") заполняет пробелами часть строки, лежащую между курсором и концом строки. 7 8 : OVERTYPE ( Симв - ) COL @ 64 < ( Заместить символ, на который указывает курсор) 9 IF DUP EMIT CPOS С! 1 COL +! ELSE DROP THEN ; 10
Это основное слово для замещения символа, на который указывает курсор, символом, лежащим в стеке. Если курсор не находится за краем строки, символ, лежащий в стеке, отображается на терминале и записывается в соответствующем месте буфера редактора. Положение курсора в буфере (COL) увеличивается на 1, чтобы обеспечить соответствие с положением терминального курсора, который при отображении символа перемещается на одну позицию вправо. 11 : INSERT ( симв - ) OPENUP OVERTYPE TYPELINE ; ( Ввести символ )
Слово INSERT сначала раздвигает текст, затем замещает пробел, возникший на месте курсора, символом, лежащим в стеке, после чего отображает строку. 12 : DELETE ( - ) COL @ 64 < ( Стереть символ) 13 IF CPOS 1+ CPOS BLEFT CMOVE BL LEND C! 14 TYPELINE 15 THEN ;
Стирание символа выполняется путем перемещений текста между курсором и концом строки (в буфере редактора) на одну позицию влево, записи пробела в конец строки (в буфере), стирания строки на экране и отображения измененной строки.
Следующий блок работает с невидимыми на экране буферами (строчный и кольцевой буферы). Строчный буфер используется для получения копии строки, которая заменит строку где-либо еще. Он особенно полезен для запоминания строки заголовка, которая будет использоваться в нескольких блоках. Кольцевой буфер воспринимает одну или более строк, которые были стерты. Эти строки затем могут быть вставлены где-либо еще в том же или другом блоке. Если вы хотите избавиться от некоторых строк совсем, то укоротите их с помощью оператора TRUNC при положении курсора на левом поле, прежде чем стирать и помещать их в кольцевой буфер. Слово BCLEAR очистит все строки в кольцевом буфере (см. его описание в блоке 5). То, что вы видите на экране, если блок помечен (UPDATE), то и будет записано на диск. 0 ( 20 июля Экранный редактор NS 08 из 10) 1 : CLINE ( --- ) LSTART LBUFF 64 CMOVE ; ( Копирование строки в буфер) 2 3 : PLINE ( - ) LBUFF LSTART 64 CMOVE ( Извлечь строку из буфера ) 4 CURSOR TYPELINE !CURSOR ;
CLINE ("copy-line" - скопировать строку) и PLINE ("put-line" - вставить строку) работают с буфером строки (по адресу LBUFF), предназначенным для копирования и замещения отдельной строки. Полезной модификацией редактора было бы постоянное отображение содержимого строчного буфера сразу под текстом блока на экране, если ваш экран имеет достаточно места. 5 6 : KLINE ( -- ) ( Стереть строку, занеся ее в кольцевой буфер) 7 LSTART BLINE 64 CMOVE ( Перенос текущей строки) 8 LSTART 64 + LSTART BBELOW CMOVE ( Сдвинуть буфер вверх) 9 SHOWLINES ; ( Отображение измененных строк)
Слово KLINE ("kill-line" - стереть строку) стирает строку, на которую указывает курсор, из отображаемой части буфера редактора и переносит ее в кольцевой буфер по адресу BLINE. Затем весь буфер редактора, начиная со строки ниже курсора и кончая BLINE, сдвигается на одну строку (64 символа), так что перекрывает стертую строку. Слово SHOWLINES отображает строки, начиная с помеченной курсором до конца экрана. Последней строкой экрана станет верхняя строка кольцевого буфера, в котором строки при работе перемещаются по ротационной схеме. 11 : ILINE ( --) ( Ввести строку из кольцевого буфера) 12 LSTART DUP 64 + BBELOW ) ( сдвинуть буфер) 13 BLINE LSTART 64 CMOVE ( Перенести строку с низа буфера) 14 SHOWLINES ; ( Отображение измененных строк)
Слово ILINE ("insert-line" - ввести строку) сдвигает кольцевой буфер на одну позицию. Весь буфер редактора, начиная со строки с курсором и кончая строкой перед BLINE. смещаются вниз на 64 символа, а последняя строка (теперь по адресу BLINE) переносится, чтобы заместить строку, где был раньше курсор; изменения отображаются оператором SHOWLINES. Это выглядит как вращение кольцевого буфера вниз на одну строку. Здесь описаны все функции, используемые редактором. Остается только соединить эти функции вместе, чтобы редактор выполнял то, что было вначале задумано.
Соединение частей в единое целое
Наиболее интересная фаза написания любой программы - это внесение последних поправок и наблюдение за тем, как она, наконец, работает. В Форте последняя фаза доставляет даже большое удовольствие, так как вы проверяете слова промежуточного уровня и теперь видите, как они собраны и как работают совместно. Если вы хорошо поставили задачу и выразительно назвали ваши слова, сборка всего вместе - обычно наиболее простая часть написания программы на Форте.
Простейший способ доступа ко всем 23 командам редактора - это создать исполнительный вектор для командных слов. Если используется клавиша CTRL в комбинации с буквой от "А" до "Z", то это дает 26 возможных команд.
Слово -- было создано в блоке 1, чтобы поставить в соответствие неиспользуемым управляющим символам пустое слово, не выполняющее никакой работы.
0 ( 20 июля 85 Экранный редактор NS 09 ИЗ 10) 1 CREATE KEYVECTORS ] ( Исполнительный вектор команд редактора) 2 LEFT ( А Курсор влево ) ?BLK# ( В Номер блока ) 3 GLINE ( С Копирует строку ) DELETE ( D Стирает символ ) 4 ?EXIT ( Е Уход из редактора ) --- ( F ) 5 MODE ( G Переход в новый режим ) --- ( Н ) 6 ILINE ( I Ввод строки ) --- ( J ) 7 KLINE ( К Стирание строки ) -BLK ( L Последний блок ) 8 NEWLINE ( М или ENTER, CR+LF ) +BLK ( N Следующий блок ) 9 OPEN ( O Раздвинуть текст ) PLINE ( Р Вставить строку ) 10 ( Q Курсор на место ) RESTORE ( R Восстановить экран ) 11 RIGHT ( S Курсор вправо ) TRUNC ( Т Укоротить строку ) 12 UPDATES ( U Поместить буфер ) --- ( V ) 13 UP ( W Курсор вверх ) ?CLEAR ( X Очистить буфер/экран ) 14 --- ( Y ) DOWN ( Z Курсор вниз ) [
Дополнительные приложения слов ] и [ описаны в гл.15, но здесь достаточно знать, что они помещают адреса слов, записанных между ними, в тело описания KEYVECTORS, как это требуется для исполнительных векторов. Функции, присваиваемые клавишам, произвольны, и мы действительно меняли местами команды в процессе написания программы; вы можете делать то же самое. Оператор KEYVECTORS может быть написан так, чтобы занимать намного меньше места на экране. Но тогда для документирования функций клавиш потребовалась бы отдельная таблица команд; более эффективно было бы позволить описанию слова документировать само себя. 15 : KEYDO ( n -) 1- 2* KEYVECTORS + @ EXECUTE ;
Слово KEYDO воспринимает число (от 1 до 26, от CTRL-A до CTRL-Z) и исполняет n-ю команду в KEYVECTORS. Теперь любая редактирующая команда может быть использована нажатием одной клавиши с пульта. Блок 10 как раз то место, где все соединяется в единое целое. 0 ( 20 июля 85 Экранный редактор NS 10 из 10) 1 : EDITCASE ( флаг симв - флаг') 2 : DUP 27 < OVER 0 > AND ( Легальный управляющий символ ?) 3 IF KEYDO ( Если так, то исполняем команду) 4 ELSE DUP 31 > OVER 127
EDITCASE - ключевое слово редактора; это место, где анализируется то, что вводится с клавиатуры, и производятся соответствующие действия. Слово EDITCASE предполагает наличие флага и кода символа в стеке. Сначала анализируется символ и проверяется, лежит ли он в интервале между CTRL-A и CTRL-Z включительно, и если да, то выполняется соответствующая команда. Если это не управляющий символ, его код проверяется еще раз, при этом выясняется возможность отображения через ASCII-код. Если это так, то в зависимости от статуса ввод-замещение производится одно из двух: символ либо вставляется в текст, либо впечатывается поверх того, который был в буфере редактора. Если символ непечатный, он игнорируется. Флаг в EDITCASE служит для управления выходом из редактора. Флаг (0) заносится в стек словом EDITLOOP (описанным ниже) и заменяется на 1, если в ?EXIT (в блоке 6) был сделан выбор "exit" (выход). Слово --- может быть заменено как здесь, так и в KEYVECTORS словом, описанным с помощью CONTROL, чтобы выдать звуковой сигнал, если ваша ЭВМ это позволяет.
Слово EDITCASE суммирует основные операции редактора и является Форт-интерпретацией части, связанной со спецификацией клавиш, приведенной в первой части этой главы. Слово ts ( так было в оригинале -прим. OCR-man)
EDITCASE может быть записано так, чтобы читаться почти так же легко, как словесное описание, если бы оно было написано с использованием трех слов, описанных для того, чтобы исключить три предложения, как это видно из нижеприведенного примера.
ПсевдоФорт из табл 13.2 Действительный текст программы
: EDIT ( n-) BLOCK-TO-BUFFER BLOCK-TO-SCREEN CURSOR-TO-START BEGIN : EDITCASE ( флаг симв-флаг') FETCH-CHARACTER CONTROL-CHAR? DUP CONTROL-CHAR? IF KEYDO IF KEYDO ELSE PRINTABLE-CHAR? ELSE DUP PRINTABLE-CHAR? IF INSERT-MODE? IF INSERT-MODE? IF INSERT IF INSERT ELSE OVERTYPE ELSE OVERTYPE THEN THEN ELSE DROP ELSE DROP --- THEN THEN THEN THEN ; END-FLAG UNTIL ;
Обратите внимание на сходство между EDITCASE и псевдо Форт-спецификацией для редактора в табл.13.2.
Все части оказались взаимно согласованными.
Хотя EDITCASE содержит базовую логику редактора, как показано в табл.13.2, нужно еще много сделать, чтобы редактор был вполне функционирующим. Блок, который нужно отредактировать, должен быть загружен в буфер редактора и должен быть запущен бесконечный цикл приема кодов от клавиатуры, которые нужны для работы EDITCASE. 11 : EDINIT ( blk -) SCR ! EMPTY-BUFFERS LOADBLK SHOWBLK ; Слово EDINIT ("edit-initialize" - вход в редактирование) запоминает номер блока в SCR, очищает блочный буфер с помощью EMPTY-BUFFERS, загружает содержимое блока в буфер редактора и отображает его на экране терминала. 12 : EDITLOOP ( -- ) EDINIT 0 BEGIN KEY EDITCASE DUP UNTIL 13 DROP ;
Слово EDITLOOP запускает редактор с помощью EDINIT и помещает флаг 0 в стек для того, чтобы редактор оставался в бесконечном цикле, пока флаг не будет заменен на 1 в ?EXIT (что заставит UNTIL прервать цикл), после чего этот флаг убирается. 14 : EDIT ( blk -) EDITLOOP ; 15 : E SCR @ EDIT ;
Описание закончено. Чтобы использовать редактор, следует напечатать слова EDIT и E. EDIT устанавливает курсор в исходное положение, очищает экран и входит в EDITLOOP с номером блока в стеке, по завершении редактирования экран вновь очищается. (Если вы хотите, измените EDIT так, чтобы курсор появился под текстом блока, редактирование которого вы прервали по команде QUIT, без стирания экрана.) Слово Е представляет удобный способ возврата к работе с только что редактированным блоком, чей номер хранится в SCR. Если SCR - часть вашей Форт-системы, слово Е перейдет к редактированию блока, для которого последней выполнялась команда LIST. Редактор завершен, если вы, конечно, не хотите его модифицировать. В гл.14 мы увидим,как для редактора можно организовать отдельный словарь, но это сейчас необязательно. Даже если вы найдете этот редактор проще того, который имеется в вашем Форте, наш детальный анализ может помочь вам понять его и, может быть, даже улучшить.
Но наше основное соображение, почему мы дали столь детальное описание редактора, было не просто документирование программы или оказание помощи для модификации вашего собственного редактора: мы надеемся, что вы научились разрабатывать и писать сложные программы.
Этот процесс обсуждается в следующем разделе.
Упражнение
1. Как бы вы отлаживали слова, описанные в блоках 9 и 10? 2. Мы дали два описания EDITCASE, последнее требует описания слов CONTROL-CHAR?. PRINTABLE-CHAR? и INSERT-MODE?. Определите эти слова. Этот процесс выделения части программы и развертывания ее в виде отдельных слов называется факторизацией описания. Подробнее вы прочтете об этом в следующем разделе.
Комментарии
Теперь, когда вы знакомы с текстом редактора и некоторыми причинами того, что она написана именно так, посмотрим на процесс программирования с более общих позиций. Наш первый шаг заключается в формировании четкой идеи основных функций редактора. Это может быть сделано многими путями: с помощью функциональной схемы (рис.13.1), словесного описания (табл.13.1) и псевдо форт-программы (табл.13.2). Для простой программы эти планы можно было бы держать в голове. Но план нужен для того, чтобы направлять усилия при программировании.
Мы не можем чрезмерно подчеркивать важность процесса планирования. Соблазн сесть и написать программу немедленно очень силен, особенно если пользуетесь языком Форт. Лучше обдумать проблему в течение часов, дней или иногда недель. Если вам не терпится сесть за терминал, примите решение по части проекта, которая вам ясна, и приступайте к работе над ней. Например, мы упражняемся с описаниями CONTROL, чтобы проще описать ESC- последовательность. То же самое было сделано, когда мы присваивали функции клавишам в KEYVECTORS. Упражнение с частью программы может быть очень полезным, например, для прояснения общего плана.
Упражняясь с частью программы, вы часто обнаруживаете, что вам нужно отладить слово, которое использует еще неописанные слова. В этом случае полезно использовать слова, называемые подставками. Подставки не выполняют никакой работы, но сообщают вам, что слово исполнено. Например, KEYVECTORS может быть проверено путем замены слов между ] и [ на CRTLA, CTRLB и т.д. до CTRLZ. CTRLA будет подставкой: : CTRLA ." Control A typed" ; KEYVECTORS и KEYDO и даже примитивная версия EDITCASE и EDITLOOP могут быть таким образом протестированы.
Такого рода упражнения могут быть весьма полезны при формировании вашего плана. Мы выполнили это отчасти еще до начала программирования и еще больше в процессе написания редактора. Так как наши основные планы были сформулированы, мы приступили к написанию настоящей программы.
Мы сначала написали редактор с минимальным числом функций для того, чтобы иметь базовую версию, в пределах которой не могли отлаживать новые команды. Таким образом, EDITCASE и EDITLOOP были написаны прежде, чем многие более ранние слова. По мере формирования редактора связанные слова группировались в некоторых блоках, а слова низкого уровня были помещены в первую пару блоков, если необходимость в них становилась очевидной. Когда редактор начал работать, были добавлены и отлажены некоторые новые команды. Процедура программирования состояла из создания слов низкого уровня, от которых все зависит, слов высокого уровня для обеспечения тестовых задач, слов среднего уровня для большинства редактирующих команд (добавления слов низкого уровня по мере необходимости), и в заключение проводились отладка и доводка всего в целом.
Благодаря возможностям, которые предоставляются в сфере тестирования и модификации, Форт идеально подходит для внедрения новых идей в работающие программы. Ключом решения проблемы является разработка базового набора слов. Другими словами, следует разработать удобную номенклатуру слов. Это ключевая концепция программирования на Форте. Номенклатура - это "система наименований, используемых в конкретной области знаний или искусства какой-либо школой или личностью, в частности названий, служащих при классификации для достижения различия с другими техническими терминами (Webster's New Collegiate Dictionary G. & С. Merriam Co., Springfield, Mass., 1959). Спецификация программ - первый шаг в процессе конкретизации проблемы. Следующий шаг - сокращение спецификации до номенклатуры, приемлемой для данной задачи. Если номенклатура зафиксирована, тогда известны слова среднего уровня и остается написать программу, используя эту номенклатуру.
Этот процесс разделения проблемы на более мелкие, которые решить легче, называется факторизацией. Хотя факторизация могла бы быть выполнена на любом универсальном языке ЭВМ с тем, чтобы расщепить проблему на части, с которыми можно работать на этом языке, эта процедура особенно проста и интуитивна на Форте. В Бейсике, Паскале и большинстве других языков факторизация состоит в разработке подпрограмм или процедур, которые вызываются при необходимости. Но в этих языках (в особенности в Бейсике) факторизация часто очень трудна для начинающих. В Форте подход к решению проблем является более интуитивным (и в действительности диктуется самим языком), так как каждое слово Форта является фактором. Каждое слово - это часть номенклатуры, которая разработана в процессе решения проблемы и имеет большое значение в описании решения. В идеале каждое слово воплощает лишь одну идею, так что его использование при построении более сложных концепций в последующих словах довольно легко понять. Слова высокого уровня, хорошо написанной Форт-программы должны читаться почти так же, как словесное описание проблемы. В действительности программа Форта в заметной мере самодокументируется. (Вы видели это в упражнении 2 предшествующего набора задач.)
Существуют различные пути, какими можно факторизовать задачу. В редакторе позиция курсора может быть выражена через число байтов от начала буфера или номера строчки и столбца. Эти два метода представления положения курсора эквивалентны потому, что номера строки и столбца могут быть вычислены на номера байта с помощью операции 64/MOD. Так какой же способ предпочтительнее? Имеются два соображения; положение курсора легче воспринять, зная строку и столбец, да и программировать так легче: строка и столбец используются независимо более часто, чем номер байта в буфере (который можно легко вычислить с помощью ROW и COL). Часто .способ, которым произведена факторизация, сильно влияет на то, как пишется остальная программа. Хотя имена слов в стандартном Форте могут быть весьма информативны (до 31 символа), длина немногих из них достигает даже трети от этой величины.
Почему? По одной причине: длинные имена долго печатать. Может быть, более важно, что они занимают больше место на диске, которого всегда не хватает. Часто сокращения - наилучший путь сохранить информативность имени. Слова базового Форта (такие как @ и !) могут использоваться в именах слов, обозначая извлечение и запоминание чисел (как в @CURSOR и !CLIRSOR), Если слово заносит флаг, включает в себя процедуру выбора или ответа на вопрос, его имя может содержать "?" (как ?BLK# и ?EXIT). И конечно, может использоваться любое число индивидуальных сокращений. Очевидно, баланс должен лежать между чрезмерно длинными словами - описателями и короткими крайне непонятными. Ясно, также, что все сокращения и личные системы кодирования имен слов должны применяться согласованно, что-бы иметь какую-то ценность. Хорошим тестом является проверка можете ли вы читать имена слов вслух, что скажет вам, разумны ли ваши сокращения и, таким образом, будут ли они понятны спустя какое-то время. Другой полезной идеей является присвоение словам имен, которые говорят о том, что они делают, а не как. Важно рассматривать слова Форта как концепцию и часть решения проблемы, а не как часть программы, которая что-то делает. Слова Форта часто проще назвать, если вы отслеживаете их функцию в контексте всей программы. Тезаурус, кстати, может быть таким же ценным инструментом в Форте, каким он является при письме.
Комментарии идут "рука об руку" с хорошими именами слов и имеют целью сделать ваш текст программы понятным как при написании, так и в дальнейшем. Комментарии и состояние стека следует вводить щедро в процессе программирования, чтобы вы могли отслеживать то, что вы ожидаете от ваших слов. Лучше всего вычислить и записать эволюцию стека в слове сразу при его описании, даже если никакого изменения стека не происходит. Эта привычка может весьма упростить задачу ознакомления с вашими новыми словами, так как вы не должны каждый раз просматривать их описания, когда вы их используете.
Конечно, после того, как программа написана, вы должны потратить столько времени, сколько нужно на тщательное оформление вашей программы и блоков, чтобы они были как можно более читаемыми.
Другой полезной привычкой является использование первой строки каждого блока для информации о содержании блока. Информация, обычно включаемая в эту индексную строку, содержит дату последней модификации, заголовок программы, инициалы автора, относительный номер блока и общее число блоков в программе. Некоторые программисты любят перечислять в индексной строке имена слов, описанных в блоке, чтобы облегчить поиск описаний. Порядок размещения этих данных не играет роли, если использованная очередность позволяет выделить важную информацию.
Выводы
Намного легче ремонтировать автомобиль, который на ходу, чем тот, который даже не поставлен на колеса. То же справедливо и для программирования на Форте: как только программа готова, чтобы что-то делать, появляются идеи по ее улучшению и совершенствованию. Если окончательная отладка программы доставляет удовольствие, то полная переделка, когда вы думали, что почти все завершено, - вряд ли. Единственный путь избежать больших задержек при программировании - попытаться предвидеть трудности до того, как они случились. Это делается путем формирования ясной идеи относительно проблемы в целом, прежде чем писать программу.
В любом случае вы должны иметь ясное представление о вашей задаче и целях, прежде чем вы приступите к программированию (на любом языке). Программирование на Форте более творческое, интерактивное и итеративное, чем на других языках. Чарльз Мур создал Форт для своих собственных приложений с целью увеличения личной производительности как программиста. Мы надеемся, что эта глава прояснила, почему форт увеличивает производительность. Форт называют усилителем идей. Существует несколько причин этого. Расширяемость Форта предлагает вам большой выбор. Применение слов Форта разделяет проблему на части, стимулируя логическое мышление.Интерактивная природа Форта способствует быстрой проверке, позволяет вам оттачивать ваши идеи, а использование в Форте длинных имен, аналогичных словам естественного языка, позволяет писать легко читаемые программы. Наконец, Форт дает вам больше власти над ЭВМ, чем может дать какой-либо другой язык. Но было также сказано, что Форт может сделать хорошего программиста великим, а плохого - ужасным. Если вы не будете дисциплинированны в определении вашей задачи, в факторизации и субфакторизации проблем, в определении слов с функциональными именами, в написании хороших комментариев и в расположении вашей программы и блоков разумным образом, вы попадете в категорию ужасных. Надеемся, что эта глава поможет вам двигаться в другом направлении.