Лекции (1129116), страница 2
Текст из файла (страница 2)
Есть еще один способ реализации виртуальной машины. В 60-е годы предпринимались попытки преодоления семантического разрыва между архитектурой компьютера и языком программирования, с помощью аппаратной реализации языков программирования. Было реализовано два очень интересных проекта – это машина Symbol, внутренним языком которой был Алгол-60, и машина МИР. МИР (Машина Инженерных Расчетов) была создана в Советском Союзе (на Украине), внутренним языком этой машины был язык Аналитик, который представлял собой некоторое слегка суженое расширение языка Алгол-60. Причем некоторые возможности этой машины потрясают до сих пор (в язык были встроены средства дифференцирования и интегрирования). Однако такой подход в мире не пошел, и сейчас популярность за подходом, основанном на байт-коде.
Часть I. Традиционные Языки Программирования
К традиционным языкам программирования относятся такие языки, как Паскаль, Си, Алгол-60, Java, Фортран, и др. Это те языки, которые основаны на Фон-неймановской модели (с учетом ее развития) поведения программы. Есть программа, есть данные (которые хранятся отдельно от программы), объектами данных являются переменные и константы. Все языки программирования, основанные на такой модели, с нашей точки зрения являются традиционными.
Поскольку традиционные языки программирования построены на понятии переменной, то основной операцией в традиционных языках программирования, является операция присваивания. Поэтому все традиционные языки программирования несколько похожи друг на друга.
Традиционные языки программирования отличаются методом классификации объектов данных, средствами изменения операций над данными, а также способами группировки операторов друг с другом (управляющие структуры).
Глава 1. Управляющие структуры. Процедурные абстракции.
Эта глав самая первая, но не самая важная, потому что, с точки зрения классификации управляющих структур, в этом вопросе достигнут некоторый консенсус, как между программиста, так и между разработчиками языков программирования. Управляющие структуры во всех языках программирования очень похожи друг на друга, поэтому эти языки очень хорошо и легко изучаются. Прежде чем был достигнут этот консенсус, а он был достигнут в 70-х годах, языки программирования достаточно сильно отличались. В первых языках программирования основными управляющими структурами были переходы (условные или безусловные), и условные операторы. Кроме того, появилось понятие цикла, который в Фортране был очень примитивным (предок цикла for), в Алголе-68 цикл был очень переусложнен (была попытка создать конструкцию, с помощью которой можно было бы создавать любые циклы). Программы выглядели довольно затейливо, с точки зрения своей структуры. Из-за обилия переходов они представляли собой блюдо спагетти, и читать их было нелегко.
Ситуацию совершенно взорвало появление в 68-ом году знаменитой статьи Дейкстры "О вредности оператора goto", и это было во времена, когда без оператора goto не обходилась ни одна программа. Оказалось, что квалификация программиста обратно пропорциональна количеству операторов перехода, которое он употребил в своей программе. Это был революционный шок не только в области программирования, но и в методологии программирования вообще. Впервые предлагалось оценивать квалификацию программиста не в терминах наличия какого-то свойства, а в терминах отсутствия. Вначале эта точка зрения была принята в штыки, но в дальнейшем, как уже было сказано, был достигнут некий консенсус.
В чем состояла основная идея? Текст программы становится гораздо лучше и удобочитаемей, если управляющие структуры принадлежат одному из трех классов:
-
обычная последовательность операторов
-
цикл (типа ПОКА или ДО)
-
конструкция if then [else];
В 69-ом году появился язык Паскаль, который в чистом виде реализовывал идеи Дейкстры, хотя допускался и цикл for, управляемый некоторой целой переменной. Кроме того, допускалась конструкция типа case (развилка по нескольким направлениям). Все эти (ветвящиеся) структуры роднит то, что, если их поместить в "черный ящик", то они будут выглядеть одинаково – нечто, где происходит какая-то обработка данных, и имеет один вход и один выход. Оказывается разработку программы можно рассматривать как некий пошаговый процесс. Любой процесс можно разбить на три блока: подготовка, выполнение, завершение. Каждый блок можно опять же детализировать с помощью какой-то последовательности "черных ящиков", каждый "ящик" может также состоять из каких-то частей. В любом случае всегда есть только один вход и один выход. В результате оказывается возможным сосредоточить свое внимание сначала на крупных абстракциях, потом детализировать эти абстракции в терминах более мелких, и т.д. Структура программы становиться более ясной, и вырастает производительность труда. Если человека не учат бегать, то он бегает сам, как может, но если его некоторое время заставляют бегать правильно, он начинает бегать быстрее. То же самое произошло с программистами.
Основная идея структурной конструкции – один вход и один выход. Такая дисциплина получила название структурного программирования, потому что программирование происходит в терминах структур. В итоге все программисты приняли эту концепцию.
Так можно или нельзя употреблять оператор goto? В 1974-ом году появилась статья Кнута "Структурное программирование с использованием оператора goto". Основная идея статьи заключалась в том, что употребление слов из четырех букв иногда уместно даже в самом лучшем обществе. Дело не в том, употреблять или не употреблять goto, а дело в том, какие структуры используются. Зачастую необходимо выйти из середины цикла. В этом случае, выход из середины цикла с помощью goto структурность программы не нарушает (все равно есть один вход и один выход). В тоже время, использование goto (в данном случае) упрощает программу, по сравнению с использованием канонической структуры.
С этого момента в этом вопросе наступила полная ясность. В настоящее время набор управляющих структур во всех языках программирования практически одинаков. Он включает три перечисленных выше типа структур. Кроме того, во многих языках появился составной оператор.
В тех языках, где составного оператора нет, есть некоторое терминальное слово, которое определяет конец последовательности операторов. Однако такое решение может привести к не очень красивым последствиям:
IF E1 THEN … END ELSE {* Пример линейного выбора на МОДУЛА-2 *}
IF E2 THEN … END ELSE
…
IF EN THEN … END ELSE …
ENDIF ENDIF … ENDIF {* Строка противоречит эстетике, а кроме того их еще надо сосчитать! *}
Чтобы избежать этих неудобств, надо ввести оператор ELSEIF, чтобы подчеркнуть, что ELSE "липнет" к IF и тогда, длинная последовательность ENDIF в конце структуры исчезнет. Такой подход принят в языках МОДУЛА-2 и АДА.
Правила хорошего тона говорят о том, что в языке должно быть как минимум два вида циклов: WHILE и REPEAT … UNTIL, а также допускается цикл FOR.
А что же делать с оператором goto? Интересно, что в языках МОДУЛА-2 и Java этот оператор вообще отсутствует, причем в Java оператор goto является к тому же зарезервированным словом (чтоб боялись). Возникает вопрос: а как же быть с той самой Кнутовской структурой, для которой выход из цикла с помощью goto очень уместен? Авторы языка Си решили эту проблему очень просто: не нравится goto – есть операторы break и continue (хотя goto в Си остался). В чем недостатки оператора break? С помощью этого оператора нельзя выйти из вложенного цикла второго уровня, поэтому в языке Си сохранился goto.
В МОДУЛА-2 предлагается писать канонически, либо, используя цикл LOOP … END (бесконечный цикл), и при этом использовать оператор EXIT, который выводит из этого цикла. При этом проблема выхода из двойного цикла не решается. Намек, в стиле профессора Вирта, авторитарного по своим убеждениям, – не пишите двойных циклов (программируйте так, потому что так надо).
J Все языки программирования делятся на фашистские и не фашистские языки, в зависимости от того, насколько они заставляют принимать программистов какой-то стиль. Это связано со следующим случаем. Когда Бьярн Страуструп разрабатывал C++, то столкнулся с ситуацией, что многие синтаксические конструкции можно сделать легче, если запретить неявное объявление типа (тип функции по умолчанию - int), кроме того, умолчание противоречит духу языка C++. Как только он попытался реализовать это, пользователь прислал ему письмо, в котором написал: "Вы заставляете нас программировать на фашистском языке". С этой точки зрения, Java, Оберон, Паскаль, более фашистские языки. Однако Страуструп принял претензию пользователя, и приводит это письмо, как причину сохранения неявного объявления типа.
В языке Java есть оператор break L, где L - метка. Однако этой меткой можно пометить только цикл, из которого потом требуется выйти (то же самое для continue). Аналогичная конструкция есть в языке Ада (в Аде есть все из традиционных языков программирования, плюс еще кое-что) - оператор EXIT L. Кроме того, в Аде с помощью понятия LOOP моделируются все возможные циклы: LOOP [условие]. Если условие отсутствует, то это цикл языка МОДУЛА-2. Если [условие]="WHILE E DO", тогда это, знакомый нам цикл WHILE. Если [условие]="I IN <диапазон> DO", тогда это цикл FOR, причем переменную I не требуется описывать (как следствие - эта переменная определена только в теле цикла).
Последняя структура, которой мы коснемся – это оператор case. Аналоги этой структуры есть во всех языках программирования (в Си и C++ - это оператор switch). Семантику оператора case вы все знаете.
Лекция 4
Базисные свойства языков программирования
Базисная программа на любом традиционном ЯП состоит из операторов присваивания и управляющих операторов, изменяющих нормальный ход выполнения (нормальный ход выполнения - последовательной выполнение каких-либо операций).
Конечно, кроме операции присваивания, среди основных есть еще и операции ввода-вывода. Что интересно - в первых ЯП, таких как Fortran, Алгол и др., средства ввода-вывода были встроены, а уже язык C отказался от этого принципа - он был первым языков, в котором ввод-вывод был вынесен из базиса. В середине 70х это был резкий шаг вперед в отношении вопроса: что должно быть, а что не должно быть в ЯП.
В языке С была использована процедурная абстракция. Вообще говоря, это очень мощный механизм, который позволяет выкидывать из языка большие «куски» (средства общения с ОС, ввод-вывод и другие) и реализовать их посредством процедурных и других абстракций.
В результате, становится интересным размышление на тему «А что должно вообще остаться в базисе ЯП?». Что необходимо, чтобы можно было развивать этот ЯП? Не в том плане, чтобы добавлять новые конструкции. Это было модно в 60-70х годах на изменяемых ЯП, которые позволяли изменять сами себя - не только формировать программу, но и расширять набор своих конструкций - в настоящее время это направление практически заглохло. В чем были проблемы расширяемых языков? Очевидно, что одно дело - добавление новых процедур, другое дело - добавление языковых конструкций. В последнем случае следует, как минимум изменить алгоритм синтаксического анализатора, а точнее говоря, сделать механизм синтаксического анализатора настолько изощренным, чтобы он мог воспринимать различные конструкции, в том числе и новые. Программы же, написанные на таком языке были более криптографические и сложные в понимании.
Так какой же минимум должен давать ЯП, чтобы он продолжал жить полнокровной жизнью, т.е. люди на нем писали. Если с это точки зрения посмотреть на ЯП, то минимальными (критическими) пунктами будут всего три:
-
процедурная абстракция;
-
раздельная компиляция (менее жесткое требование):
-
межмодульная коммуникация (по данным).
Все эти три пункта присутствуют, например, в языке Fortran. Даже в самой первой версии уже были подпрограммы. Неизвестно из каких соображений автор ввел в язык это понятие, но понятно, что при вычислении сложных математических формул очень удобно распределить вычисления по функциям.
Кроме этого, в Фортране была раздельная компиляция, т.е. каждая процедура и функция транслировалась отдельно.
Также в Фортране имелись зачаточные средства межмодульной коммуникации - а именно, понятие общего блока памяти. В этом блоке каждый модуль мог создавать свои переменные, причем, эти переменные накладывались друг на друга. В результате, можно было объявлять глобальные переменные, но возникали проблемы, если эти переменные были объявлены в модулях различными типами. Таких общих блоков могло быть несколько, они могли именоваться.