Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Язык программирования (и ОС) ФОРТ (FORTH) Глава 1. Философия Форта


Информационный Канал Subscribe.Ru

Глава 1. Философия Форта


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

Что она такое ? Как ее можно применять для решения задач программирования ?

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

Сказание об истории элегантности программ

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

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

В даном разделе мы проследим историю инструментария и технологий, предназначенных для написания более элегантных программ.

Запоминаемость

Первые программы для ЭВМ выглядели как-то вроде:

00110101
11010011
11011001
Программисты вводили их, устанавливая ряды переключателей в положение "вкл." для единиц и "выкл." для нулей. Эти значения были "машинными инструкциями" для ЭВМ, и каждая заставляла ее производить некие элементарные операции типа "переместить содержимое регистра А в регистр Б" или "добавить содержимое регистра В к содержимому регистра А".

Это оказалось несколько скучноватым.

Скука -- мачеха изобретения, поэтому некоторые умные программисты осознали, что машину и саму можно затставить помочь им. Так они написали программу, которая переводила легкозапоминаемые аббревиатуры в труднозапоминаемые последовательности битов. Новый язык выглядел примерно так:

MOV B,A
ADD C,A
JNC REC1
Переводчик (транслятор) программ был назван `аcсемблером`, а новый язык -- `языком ассемблера`. Каждая инструкция "собирала" ("ассемблировала") соответствующую последовательность битов для себя при сохранении точного соотношения между ассемблерной инструкцией и машинной командой. Но ведь имена программистам запоминать легче. По этой причине новые инструкции были названы `мнемониками`.

Мощность

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

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

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

Абстрактность

Важным достижением было изобретение "языка высокого уровня". Это опять была программа-переводчик, но более мощная. Высокоуровневые языки делают возможным для программистов записывать выражения вида:

X = Y ( 456 / A ) - 2
которые сильно похожи на алгебраические. Благодаря языкам высокого уровня инженеры, а не только странноватые бит-жокеи, смогли начать писать программы. BASIC и FORTRAN -- примеры высокоуровневых языков.

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

Реальные инструкции зависят от каждого "выражения" в исходном тексте, взятом как единое целое. Операторы вроде + и = сами по себе не имеют смысла. Они -- просто часть сложной символики, которая зависит от синтаксиса и позиции оператора в тексте.

Это нелинейное, зависящее от синтаксиса соответствие между исходным текстом и реальным (обектным) кодом обычно рассматривается как неоценимый вклад в прогресс методологии программирования. Но, как мы увидим впоследствии, такой подход неизбежно предоставляет больше ограничений, чем свободы.

Управляемость

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

Как ассемблер, так и высокоуровневый язык обеспечивают возможности для переходов и циклов. В ассемблерах мы используем команды типа "jump" ("прыжок"), в некоторых языках высокого уровня пользуемся конструкциями типа "GO TO" ("перейти к"). Когда эти возможности используются в сильной степени, программы начинают становиться похожими на такую же неразбериху, как на рисунке 1-1.

Рис.1-1. Неструктурированный код, использующий инструкции типа "jump" или "GOTO".

ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ПРОВЕРКА УСЛОВИЯ
ПЕРЕХОД
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ПЕРЕХОД
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ИНСТРУКЦИЯ
ПРОВЕРКА УСЛОВИЯ
ПЕРЕХОД
ПРОВЕРКА УСЛОВИЯ
ПЕРЕХОД
Этот подход, до сих пор широко представленный в таких языках, как Фортран и Бейсик, создает трудности при написании и трудности при внесении изменений. При такой "кашеобразной" манере написания программ невозможно протестировать отдельный участок кода или найти почему выполняется что-то, что выполняться не должно.

Трудности с кашевидными программами привели к открытию "блок-схем". Это были нарисованные карандашом и ручкой картинки, показывающие "течение" процесса, которые использовались программистом в качестве шпаргалки для понимания создаваемого кода. К несчастью, программист вынужден был осуществлять переход от кода к диаграмме и наоборот вручную. Многие программисты осознали бесполезность старомодных диаграмм.

Модульность

Существенное движение вперед произошло с внедрением "структурированного программирования", методологии, основанной на том, что, как показал опыт, большие задачи проще решаются, если рассматривать их как совокупность меньших задач [1]. Каждый такой кусочек называется `модулем`. Программы состоят из модулей внутри других модулей.

Структурированное программирование подавляет кашеобразность кода, поскольку процессы переходов прослеживаются только в пределах модуля. Нельзя перепрыгнуть из середины одного модуля в середину другого.

Например, на рис.1-2 показана блок-схема модуля под названием "приготовление завтрака", который состоит из четырех подмодулей. Внутри каждого подмодуля можно найти новый уровень сложности, которую вовсе не нужно показывать на нашем уровне.

Рис.1-2. Проект структурированной программы.

Приготовление завтрака
                          |
 +------------------------|------------------------+
 |            +-----------+----------+             |
 |            | Решение: Вы спешите? |             |
 |            +----------------------+             |
 |                /               \                |
 |              да                 нет             |
 | +-----------/--------------+   +--\-----------+ |
 | | Остановиться на холодной |   | Сварить яйца | |
 | |        овсянке           |   |              | |
 | +-------------------\------+   +--/-----------+ |
 |                      \           /              |
 |                     +-\---------/---+           |
 |                     | Вымыть посуду |           |
 |                     +-------|-------+           |
 +-----------------------------|-------------------+
                               |
Решение о переходе внутри нашего модуля принимается при выборе между модулем "холодная овсянка" и модулем "яйца", но линии переходов входят только в наружный модуль.

Структурированное программирование имеет три преимущества:

  1. Каждая программа представляется линейной последовательностью содержательных функций, называемых `модулями`. Каждый модуль имеет ровно один вход и ровно один выход.
  2. Каждый модуль состоит из одной или нескольких функций, каждая из которых имеет также ровно один вход и ровно один выход и сама может рассматриваться как модуль.
  3. Модуль может содержать:
    1. операции или другие модули;
    2. структуры принятия решений (выражения типа ЕСЛИ ТО);
    3. структуры для организации циклов.

Смысл модулей, имеющих "один вход, один выход", состоит в том, что Вы можете вынуть их, изменить их начинку и вставить обратно без развинчивания остальных соединений в программе. Это означает, что Вы можете попробовать каждый кусок по отдельности. Такое возможно когда Вы точно знаете что имеется при входе в модуль и что наблюдается после выхода из него.

В "приготовлении завтрака" Вы либо останавливаетесь на овсянке, либо варите яйца, но не одновременно. А потом Вы обязательно моете посуду. (Насколько мне известно, некоторые программисты обходят этот последний модуль, переезжая на новую квартиру каждые три месяца.)

Структурированное программирование было изначально задумано как подход к проектированию. Модули были воображаемыми обектами, которые существовали в голове программиста или разработчика и не были частями реального кода. Когда техника структурированного программирования применяется к неструктурированным языкам типа Бейсика, результат получается похожим на то, что показано на рис.1-3.

Рис.1-3. Структурированное программирование на неструктурированном языке.

10   ИНСТРУКЦИЯ
20   ИНСТРУКЦИЯ                       ' Решить - спешим?
30   ЕСЛИ Н=ВЕРНО ТО ПЕРЕЙТИ К 80     ' если да, то на 80
40   ИНСТРУКЦИЯ
50   ИНСТРУКЦИЯ                       ' Варка яиц
60   ИНСТРУКЦИЯ
70   ПЕРЕЙТИ К 110                    ' на 110
80   ИНСТРУКЦИЯ
90   ИНСТРУКЦИЯ                       ' Приготовление овсянки
100  ИНСТРУКЦИЯ
110  ИНСТРУКЦИЯ                       ' Мытье посуды
120  ИНСТРУКЦИЯ

Удобство написания

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

На рисунке 1-4 показано, как этот тип языка позволяет переписать программу "приготовление завтрака".

Рис.1-4. Использование структурированного языка.

ИНСТРУКЦИЯ
ИНСТРУКЦИЯ         Решение - спешим?
 ЕСЛИ ДА, ТО
    ИНСТРУКЦИЯ
    ИНСТРУКЦИЯ          Варка яиц
    ИНСТРУКЦИЯ
 ИНАЧЕ
    ИНСТРУКЦИЯ
    ИНСТРУКЦИЯ          Приготовление овсянки
    ИНСТРУКЦИЯ
 ДАЛЬШЕ
ИНСТРУКЦИЯ         Мытье посуды
ИНСТРУКЦИЯ
Языки структурированного программирования имеют управляющие структурные операторы типа ЕСЛИ и ТО для подчеркивания модульности в организации передачи управления. Как Вы можете заметить, отступы в тексте программы важны для ее читабельности, хотя все равно все инструкции внутри модуля написаны полностью вместо замены модуля его именем (например, "приготовление-овсянки"). Законченная программа может занимать десять страниц, с оператором ИНАЧЕ на странице пять.

Разработка "с вершины"

Как приступать к разработке подобных модулей ? Методология, называемая "разработкой сверху-вниз", утверждает, что модули следует строить, начиная с самого общего, главного и далее прорабатывать до уровня самых мелких модулей.

Последователи такого подхода могут засвидетельствовать возникновение позорно огромных потерь времени в результате ошибок в планировании. На горьком опыте они познали, что попытки корректировать программу после того, как она была написана -- такая практика известна как "наложение заплат" -- подобны попытке запереть двери конюшни после того, как лошадь уже понесла.

Поэтому как контрмеру они предлагают следующее официальное правило программирования сверху-вниз:

Не писать ни строчки текста до тех пор, пока план не проработан до мельчайших деталей.

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

Подпрограммы

Мы обсуждали "модули" только как абстрактные объекты. Но любые высокоуровневые языки имеют аппарат, позволяющий кодировать модули проекта как модули реального кода -- отдельные куски, которым можно дать имена и "вызывать" из других кусков кода. Эти куски называются подпрограммами, процедурами или функциями, в зависимости от языка программирования и способа реализации.

Предположим, мы написали "приготовление овсянки" в виде подпрограммы. Это могло бы выглядеть как-нибудь вроде:

процедура приготовление-овсянки
 взять чистую тарелку
 открыть коробку с овсянкой
 насыпать овсянки
 открыть молоко
 налить молоко
 взять ложку
конец
Мы можем также написать и "варку-яиц", и "мытье-посуды" в виде подпрограмм. В этом случае можно определить "приготовление-завтрака" как простую программу, которая вызывает эти подпрограммы:
процедура приготовление-завтрака
 переменная С: булевская (означает спешку)
  `проверить наличие спешки`
  если С=истина, то
    вызвать приготовление-овсянки
  иначе
    вызвать варку-яиц
  конец
  вызвать мытье-посуды
конец
Фраза "вызвать приготовление-овсянки" заставляет выполниться подпрограмму с таким именем. Когда выполнение заканчивается, управление возвращается назад в вызывающую программу в точку, следующую за вызовом. Подпрограммы повинуются законам структурированного программирования.

Как можно видеть, эффект при использовании подпрограмм такой же, как если бы тело этих подпрограмм присутствовало бы в вызывающем модуле. Но, в отличие от кода, производимого макроассемблером, подпрограмма может быть скомпилирована где угодно в памяти, после чего на нее можно просто ссылаться. Не обязательно компилировать ее внутри реального кода главной программы (см. рис. 1-5).

Рис.1-5. Главная программа и подпрограмма в памяти.

  Главная программа
   |-----------------------|    ____
   |_______________________|   /   \/      Подпрограмма
   |_______________________|  /  +-----------------------+
   |_______________________| /   | Приготовление-овсянки |
   | вызвать               |/    |_______________________|
   | приготовление-овсянки |     |_______________________|
   |_______________________|\    |_______________________|
   |_______________________| \   |_______________________|
   |                       |  \_____/
Годами ученые-компьютерщики совершенствовались в искусстве использования многочисленных маленьких подпрограмм в сильно разветвленных, протяженных программах. Они могут быть написаны и отлажены независимо друг от друга. Это облегчает повторное использование ранее написанных программ, и так легче распределять части работы между различными программистами. Короткие куски проще продумывать и проверять их правильность.

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

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

Существенен также сам способ вызова и передачи параметров подпрограмме. Для автономного тестирования подпрограммы приходится писать специальные тестовые программы для ее вызова.

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

Постепенная детализация

Один из подходов, существенно опирающихся на использование подпрограмм, называется "постепенной детализацией" [2]. Идея состоит в том, что Вы начинаете с написания скелетной версии программы, использующей естественные имена процедур и структур данных. Затем Вы пишете версии для каждой из именованных процедур. Процесс продолжается в сторону большего уровня детализации до тех пор, пока процедуры не смогут быть выражены непосредственно на компьютерном языке.

При каждом шаге программист должен принимать решения об используемых алгоритмах и о типах структур данных, которые обрабатываются этими алгоритмами. Такие решения должны приниматься параллельно.

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

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

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


Subscribe.Ru
Поддержка подписчиков
Другие рассылки этой тематики
Другие рассылки этого автора
Подписан адрес:
Код этой рассылки: comp.soft.prog.forth
Отписаться
Вспомнить пароль

В избранное