О том, чего мы достигли
Размышляя о рассматриваемой нами структуре программ, я метафорически представлял себе программу как бусы из отдельных жемчужных зерен.
Мы описывали программу с помощью уровней, и каждый уровень содержал "конкретизации" понятий, которые предполагались имеющимися на более высоких уровнях. Эти конкретизации были либо динамическими (алгоритмы), либо статическими (структуры данных), и их должна была понимать соответствующая машина. Я использую термин "зерно" для такой машины вместе с относящимися к ней конкретизациями.
Наша предыдущая программа представляет собой бусы из шести зерен либо в порядке
ВЫЧНАЧ ЧИСТНАЧ IСЧИТЫВАНИЕ ВЫЧПОЗ СТРОП ДЛИНЦИК
либо в порядке
ВЫЧНАЧ ЧИСТНАЧ IСЧИТЫВАНИЕ ВЫЧПОЗ СТРОП КОРЦИК
ДЛИНЦИК и КОРЦИК - это два разных зерна; они сводят одинаковые понятия ("верхнего уровня") к одним и тем же понятиям ("нижнего уровня"); отличаются только способы конкретизации: это разные варианты программ решения одной и той же задачи на одной и той же машине.
Изменение программы будет рассматриваться как замена одного или более зерен в первоначальных бусах на одно или несколько других зерен. Каждое зерно - это отдельный элемент, и из них составляются программы. Теперь мы считаем составление программы (как представителя класса взаимосвязанных программ) процессом из двух этапов: изготовление зерен (в большем количестве, чем строго необходимо), а затем выборочное нанизывание их в соответствующих бусах.
Для такого двухэтапного подхода имеется много причин. Проектируя программу, мы должны рассмотреть очень много различных программ, а когда наша программа будет готова, нам потребуется изменять ее (заменяя на один из эквивалентных вариантов). Коль скоро мы воспринимаем программы как линейные строки основных символов языка программирования, а к изменению программы относимся как к обработке текста такого уровня, то каждое изменение программы следует понимать в масштабе всей совокупности программ (правильных или ошибочных!), которые могут быть написаны на этом языке программирования.
Неудивительно, что при этом изменение программы оказывается весьма рискованным! Основной символ - это слишком маленький и лишенный смысловой нагрузки объект для того, чтобы описывать с его помощью такие процессы. Естественным элементом для описания таких модификаций представляется зерно, воплощающее в себе независимое проектное решение или, быть может, изолированный аспект исходной постановки задачи.
Еще одно подтверждение той же мысли. С появлением языка АЛГОЛ-60 выяснилось, что синтаксис является весьма эффективным средством для выражения структуры текста программы. (Синтаксис окружили таким ореолом, что многие специалисты стали отождествлять системное программирование с синтаксическим анализом.) Однако при этом несколько упускали из виду, что при выражении структуры через синтаксис эта структура задается очень неявно, т. е. должна выводиться путем применения алгоритма грамматического разбора к линейной последовательности основных символов. Это огорчительно, если учесть, что весьма часто модификация программы оставляет без изменений большие части структуры, так что после утомительного нового грамматического разбора модифицированного текста повторно выявляется та же самая структура. У меня сложилось твердое мнение, что пригодность методов контекстно-свободного анализа для представления структуры сильно преувеличивалась. (Мои ближайшие сотрудники показали мне следующую ошибку в одной программе на языке АЛГОЛ-60. Программа выдавала ошибочные результаты, хотя трансляция производилась с соответствующим полным контролем, который требовал в добавление к тексту программы заключительный символ progend после последнего end. Этот добавочный символ в неподходящем месте программы служил сигналом об ошибке; благодаря этому, можно было установить правильность соответствия скобок begin-end. Оказалось, что
- где-то в программе была пропущена кавычка, закрывающая строку;
- где-то в последующем тексте программы была пропущена кавычка, открывающая строку;
- структура begin-end полученной программы была синтаксически правильна;
- идентификаторы, описанные между двумя пропусками, использовались только в этом участке программы, так что даже контекстно-зависимый контроль не мог сигнализировать об ошибке.
К тому времени у меня уже были сомнения в пригодности контекстно-свободных методов для выражения макроскопической структуры, и поэтому я пришел в восторг, когда мне показали эту ошибку).
Чем больше я размышляю о зернах, тем больше убеждаюсь в том, что нечто вроде зерен - это единственный выход из положения, если мы сознаем, что должны принимать во внимание (для большой программы) порядка тысячи возможных вариантов. Нельзя ожидать, что программист будет изготовлять каждый вариант из этой тысячи с самого начала, независимо один от другого. Я вижу единственный способ обеспечения такого потенциального разнообразия в применении комбинаторных методов, т. е. в том, чтобы изготовить больше зерен (скажем, 250), чем нужно для одних бус (скажем, 200) и выборочно нанизывать их в бусы. Другого подходящего способа я не вижу. Можно было бы достигнуть большого разнообразия, применяя комбинаторные перестановки, но для нас это не годится, потому что окончательные бусы должны быть надлежащим образом подогнаны, и если заданы зерна, то вполне определен порядок нанизывания их на нить для получения подогнанных бус. К тому же, если это и не так, то допустимые изменения порядка оказываются отнюдь не по существу.
К тому же с помощью зерен проясняется смысл "неполной" программы, состоящей из верхней части бус: ее можно рассматривать как полную программу для выполнения на соответствующей машине (возможная реализация которой задается нижней частью бус). В сущности, правильность верхней части бус может быть доказана безотносительно к выбору нижней части. Между двумя последовательными зернами мы можем сделать "зазор" в виде руководства по машине, реализуемой частью бус снизу от зазора и используемой в программе, которая представлена частью бус сверху от зазора. Это руководство служит средством сопряжения между, двумя частями бус. Такая форма сопряжения кажется нам более удобной, чем интерпретация представления данных как средства сопряжения операций; в частности, она лучше обеспечивает комбинаторную гибкость, которая требуется, когда программа должна обладать приспособляемостью.
Сделаем еще замечание относительно областей действия понятий вдоль бус.
Например, понятие "отражение" вводится в нашем верхнем зерне "ВЫЧНАЧ" и исчерпывающе объясняется в предпоследнем снизу зерне "СТРОП". Если мы теперь придем к заключению, что программа в рассматриваемом виде требует слишком много машинной памяти и поэтому мы не можем позволить себе введение переменной "отражение", то нам предстоит пересмотр основной программы и мы должны будем заменить верхние пять зерен на другие, потому что ими исчерпывается область действия понятия "отражение". Нижнее зерно (либо "ДЛИНЦИК", либо "КОРЦИК") может быть сохранено без изменений. (Мы упоминаем об этом, чтобы проиллюстрировать тот факт, что замена зерна никоим образом не предопределяет замену нижнего зерна.)
В связи с вопросом об областях действия понятий вдоль бус мне хотелось бы привлечь внимание читателей к одному наблюдению, которое произвело на меня сильное впечатление, когда я впервые сделал его. (В ретроспективе оно представляется совершенно очевидным, и именно поэтому имеет смысл изложить его.) Для каждого зерна мы принимаем "независимое проектное решение", и поэтому порядок зерен вдоль бус подразумевает порядок проектных решений. Можем ли мы изменить этот порядок? Да, можем, хотя тогда у нас будут другие зерна. Экспериментируя, я следовал известному совету: если у вас есть два элемента (в нашем случае "строить" и "печатать"), первым делом выявите их взаимосвязь (в нашем случае "отражение"), и тогда оба элемента можно будет конкретизировать независимо один от другого. Я так и сделал и пришел к следующему виду бус:
ВЫЧНАЧ СТРОП' ЧИСТНАЧ' IСЧИТЫВАНИЕ' ВЫЧПОЗ' КОРЦИК
(Четыре промежуточных зерна помечены для указания того, что ими заменены другие зерна, хотя в них воплощены те же самые решения, что и в соответствующих исходных зернах.) Получившаяся программа более запутанная. Почему?
Вдоль бус мы можем указывать для каждого понятия его область действия. Разумеется, они перекрещиваются, и мы можем рассматривать их как отдельные нити, которые сплетаются в цельное толкование как некий вид "логического каната".
Запутанному варианту соответствует логический канат, свитый из большего числа местами более длинных нитей; такой логический канат "толще", а вся конструкция более тесно взаимосвязана. Это наблюдение произвело на меня сильное впечатление потому, что оно очень наглядно показало (по крайней мере мне), что изящество, ясность и тому подобное в значительной степени определяются количественными аспектами. (Этим владел Моцарт: многие его произведения, от которых замирает дыхание, обманчиво просты; кажется, будто они созданы практически из ничего!)
Можно сформулировать это наблюдение в более научных терминах. Помеченный вариант запутан, потому что отражение сводится к строкам на слишком ранней стадии, а это вынуждает нас описывать "ВЫЧНАЧ", "IСЧИТЫВАНИЕ" и "ВЫЧПОЗ" в терминах строк, хотя их можно было бы еще объяснять непосредственно .через отражение, т. е. независимо от выбираемого представления отражения. Другими словами, в первоначальном, варианте мы искуснее использовали свою способность к абстракции, чем в помеченном варианте. Чем больше число зерен, которые не зависят от конкретного представления, тем больше приспособляемость программы и тем легче ее понимать, поскольку эти зерна доступны пониманию на более высоком уровне абстракции. По-видимому, практика показывает, что требования приспособляемости и ясности имеют общую основу и, более того, сочетаются самым естественным образом. Это весьма радует (хотя и не удивляет). Это также проясняет (по крайней мере для меня) предмет нашего рассмотрения. К чему бы мы ни пришли, это не будет универсальным методом решения проблем автоматическим путем или с участием человека. Однако если человек сумеет найти решение, то мы можем помочь ему правильно оценить различные аспекты "изящества" этого решения. А уже это послужит ему ориентиром.