ЭВОЛЮЦИЯ И КЛАССИФИКАЦИЯ
Начнем обсуждение языков программирования с рассмотрения их истории и существующих в настоящий момент ос- новных подходов к программированию.
Ранние поколения. Первоначально процесс программирования предусматривал запись программистом всех алгорит- мов непосредственно на машинном языке. Такой подход усугублял и без того трудную задачу разработки алгоритмов и слишком часто приводил к ошибкам, которые необходимо было обнаружить и исправить (процесс, известный как отладка) до того, как работу можно было считать законченной.
Первым шагом на пути к облегчению задачи программирования был отказ от использования цифр для записи команд и операндов непосредственно в той форме, в которой они используются в машине. С этой целью при разработке программ стали широко применять мнемоническую запись различных команд вместо их шестнадцатеричного представления. Напри- мер, вместо цифрового кода команды загрузки регистра программист мог теперь написать LD (от Load), а вместо кода команды копирования содержимого регистра в память мог использовать мнемоническое обозначение ST (от Story). Для записи операндов были разработаны правила, в соответствии с которыми программист мог присваивать некоторым областям памяти описательные имена (идентификаторы) и использовать их при записи команд программы вместо адресов соответст- вующих ячеек памяти. Одним из специфических вариантов является присвоение мнемонических имен регистрам централь- ного процессора, например R0, R1, R2, ...
Используя идентификаторы для ячеек памяти и мнемонические обозначения для команд, программисты смогли значи- тельно повысить читабельность написанных ими последовательностей машинных команд. Давайте вернемся, например, к программе на машинном языке, приведенной в конце раздела 2.2. Эта программа суммировала содержимое ячеек с адресами '6С’ и '6D', после чего помещала результат в ячейку с адресом '6Е'. Напомним, что в шестнадцатеричном виде соответст- вующая последовательность команд имеет следующий вид:
156С
166D
5056
306Е С000
Если мы присвоим имя PRICE ячейке с адресом '6С’, имя TAX – ячейке адресом '6D' и имя TOTAL – ячейке с адресом '6Е', то сможем переписать ту же самую программу с использованием мнемонических записей команд так, как показано ни- же:
LD R5,PRICE LD R6,TAX ADDI R0,R5,R6 ST R0,TOTAL HLT
Большинство читателей, вероятно, согласятся, что второй способ записи текста программы намного лучше отражает ее смысл, чем первый (оставаясь, впрочем, также не вполне понятным). Заметим, что мнемоническая запись ADDI здесь ис- пользована для команды сложения двух целых чисел, в то время как команду сложения двух чисел с плавающей точкой можно мнемонически обозначить как ADDF.
Вначале программисты использовали такие обозначения при разработке программ на бумаге, а затем переводили их на машинный язык. Однако вскоре стало понятно, что такой перевод может выполнить и сама машина. В результате были раз- работаны программы, названные ассемблерами и предназначенные для перевода записанных в мнемоническом виде про- грамм на машинный язык. Название ассемблер (assembler – сборщик) эти программы получили потому, что их назначение заключалось в сборке машинных команд из кодов команд и операндов, полученных в результате перевода мнемонических обозначений и идентификаторов. Мнемонические системы записи программ стали, в свою очередь, рассматриваться как особые языки программирования, именуемые языками ассемблера.
В свое время разработка языков ассемблера считалась гигантским шагом вперед в поисках более совершенных техноло- гий программирования. Многие считали, что они представляют собой совершенно новое поколение языков программирова- ния. Со временем языки ассемблера стали называть языками программирования второго поколения, а к первому поколению были отнесены сами машинные языки.
Хотя языки второго поколения имели много преимуществ по сравнению с машинными языками, они все же не могли обеспечить завершенную среду программирования. Помимо всего прочего, применяемые в языке ассемблера языковые кон- струкции, по существу, совпадают с конструкциями соответствующих машинных языков. Разница заключается лишь в син- таксическом способе их выражения. По этой причине программы, написанные на языке ассемблера, являются принципиаль- но машинно-зависимыми, т.е. команды в этих программах выражаются в терминах определенных машинных атрибутов.
Программу на языке ассемблера достаточно сложно выполнить на другой машине, поскольку для этого ее нужно переписать с учетом новой конфигурации регистров и набора команд.
Кроссплатформенное программное обеспечение. При решении многих задач типичная прикладная программа вынуж- дена полагаться на операционную систему. Например, она может обратиться к диспетчеру окна для организации взаимодей- ствия с пользователем или диспетчеру файлов для считывания данных с устройств массовой памяти. К сожалению, различ- ные операционные системы выполняют такие запросы по-разному. Поэтому если программа предназначена для рассылки и выполнения в сети, объединяющей машины разного типа, которые имеют различные операционные системы, то она должна быть независимой как от операционных систем, так и от типа используемых машин. Чтобы отметить этот уровень независи- мости, используется термин кроссплатформенное программное обеспечение. Иными словами, кроссплатформенное про- граммное обеспечение – это программы, которые не зависят ни от операционной системы, ни от аппаратного обеспечения, а значит, могут выполняться на разных компьютерах, объединенных в сеть.
Кроме того, хотя программист и не обязан больше кодировать программу с помощью нулей и единиц, он все еще выну- жден мыслить в терминах пошагового выполнения команд машинного языка. Это аналогично проектированию дома из до- сок, гвоздей, кирпичей и других материалов. Конечно, реальная конструкция дома состоит именно из этих элементарных вещей, но проектировать его все же легче, имея дело с комнатами, окнами, дверьми и прочими подобными понятиями.
Короче говоря, элементарные примитивы, из которых в конечном счете должен быть сконструирован продукт, вовсе не обязательно должны использоваться и при разработке проекта этого продукта. При проектировании удобнее пользоваться примитивами более высокого уровня, каждый из которых представляет концепцию, связанную с некоторой функцией конеч- ного продукта достаточно высокого уровня. По окончании проектирования эти примитивы могут быть выражены с помо- щью концепций более низкого уровня, относящихся к деталям их реализации.
Следуя такому подходу, специалисты по компьютерам стали разрабатывать языки программирования, которые больше подходили для целей разработки программного обеспечения, чем низкоуровневые языки ассемблера. В результате появились языки программирования третьего поколения, которые отличались от предыдущих поколений тем, что их языковые конст- рукции имели более высокий уровень и были машинно-независимыми. Наиболее известными примерами ранних языков третьего поколения являются FORTRAN (FORmula TRANslator – переводчик формул), который был предназначен для научных и инженерных расчетов, и COBOL (COmmon Business-Oriented Language – язык общего назначения деловой ориентации), разра- ботанный специалистами военного морского флота США для решения экономических задач.
В общем случае язык программирования третьего поколения представляет собой определенный набор языковых конст- рукций достаточно высокого уровня, предназначенный для разработки программного обеспечения. По существу, точно так же был разработан и наш псевдокод, описанный в главе 4. Каждая из языковых конструкций была разработана так, чтобы ее можно было реализовать в виде последовательности низкоуровневых примитивов, существующих в машинных языках. Рас- смотрим следующий оператор:
Total ← Price + Tax
Он представляет собой выражение высокого уровня, в котором совершенно отсутствуют указания, как именно опреде- ленная машина должна выполнять поставленную задачу. Однако этот оператор вполне можно реализовать в виде последова- тельности машинных команд, которые мы обсуждали выше. Таким образом, показанная ниже структура потенциально явля- ется языковой конструкцией высокого уровня:
идентификатор ← выражение
После того как необходимый набор примитивов высокого уровня будет определен, пишется программа, называемая транслятором (translator – переводчик). Она предназначена для перевода программ, записанных с использованием примити- вов языка высокого уровня, на машинный язык. Подобный транслятор похож на программу-ассемблер второго поколения, за исключением того, что ему часто приходится объединять (или компилировать, от англ. compile) несколько машинных инст- рукций в короткие последовательности команд, предназначенные для имитации выполнения отдельных примитивов высоко- го уровня. Именно поэтому подобные программы-переводчики часто называют компиляторами. Разработку первого компи- лятора приписывают Грейс Хоппер (Grace Hopper), которая играла ведущую роль в продвижении концепции языков про- граммирования высокого уровня. Действительно, идея писать программы в форме, близкой к естественному языку, была настолько революционной, что многие руководители поначалу отвергали ее.
Популярной альтернативой трансляторам являются интерпретаторы (interpreters), предложенные как еще один способ выполнения программ, написанных на языках программирования третьего поколения. Эти программы подобны транслято- рам, однако они выполняют команды программы непосредственно после их перевода, а не записывают, подобно транслято- рам, переведенный код в виде выполняемого модуля, предназначенного для последующего использования. Это означает, что вместо создания копии программы на машинном языке, которую необходимо будет выполнить позже, интерпретатор немед- ленно выполняет все переведенные им инструкции.
Машинная независимость. С появлением языков программирования третьего поколения цель обеспечения машинной независимости программ была в основном достигнута. Поскольку операторы в языках третьего поколения не привязаны к особенностям какой-то конкретной машины, они легко могут быть скомпилированы на любом компьютере. Теоретически программа, написанная на языке третьего поколения, может быть выполнена на любой машине за счет использования соот- ветствующего компилятора.
В действительности не все так просто. При разработке самого компилятора приходится учитывать определенные огра- ничения, накладываемые той машиной, для которой он предназначен. В результате эти ограничения отражаются на языке программирования, который подлежит переводу на машинный язык. Например, размер машинных регистров и ячеек памяти влияет на максимальный размер значений целых переменных, которыми может непосредственно оперировать программа. Такие ограничения приводят к тому, что один и тот же язык программирования на разных машинах имеет свои особенности, или диалекты. Вследствие этого программистам часто приходится выполнять, как минимум, легкую модификацию про- граммы при переносе ее с одной машины на другую.
Проблема переноса программ с одной машины на другую заключается в отсутствии общей точки зрения на то, что именно считать стандартом данного языка программирования. В связи с этим Американский национальный институт стан- дартов (ANSI) и Международная организация по стандартизации (ISO) приняли и опубликовали стандарты для многих по- пулярных языков программирования. В других случаях применяются неформальные стандарты, которые являются следстви- ем популярности того или иного диалекта языка, а также желания многих разработчиков компиляторов создавать продукты, совместимые с другими, подобными им.
Тот факт, что языки третьего поколения не достигли истинной машинной независимости, на самом деле не имеет боль- шого значения по двум причинам. Во-первых, они все же являются достаточно машинно-независимыми, для того чтобы можно было относительно легко переносить программное обеспечение с одной машины на другую. Во-вторых, машинная независимость – это лишь промежуточная ступень на пути к достижению более важных целей. Со временем машинная неза- висимость стала вполне достижимой, однако она стала менее важной по сравнению с другими велениями времени. Действи- тельно, понимание того, что машина могла бы выполнять такие операторы высокого уровня, как
Total ← Price + Tax,
породило среди ученых в области компьютерных наук мечту о создании среды программирования, которая позволила бы людям общаться с машиной в терминах абстрактных понятий, а не заставляла их переводить эти понятия в машинно- совместимую форму. Более того, ученым понадобились машины, способные самостоятельно выбирать алгоритмы, а не про- сто выполнять действия, описанные с помощью набора инструкций. В результате спектр языков программирования заметно расширился, что в конечном счете привело к усложнению их прежней классификации в терминах простого разделения на поколения.
Парадигмы программирования. Классификация языков программирования по поколениям требует распределения их по линейной шкале (рис. 5.1) в соответствии с той степенью свободы от компьютерной тарабарщины, которую данный язык предоставляет программисту. Это позволяет ему мыслить понятиями, связанными непосредственно с решаемой задачей. В действительности развитие языков программирования происходило несколько иначе. Оно протекало по разным направлени- ям, связанным с альтернативными подходами к процессу программирования (называемыми парадигмами программирова- ния). Таким образом, историческую схему развития языков программирования наиболее точно можно изобразить составной диаграммой, показанной на рис. 5.2, на которой отдельные линии, символизирующие различные парадигмы программирова- ния, появляются и развиваются независимо друг от друга.
![]() |
Рис. 5.1. Схематическое представление поколений языков программирования
Рис. 5.2. Эволюция парадигм языков программирования
В частности, на рисунке показаны четыре независимых направления, соответствующие функциональной, объектно- ориентированной, императивной и декларативной парадигмам программирования, а также представлены различные относя- щиеся к ним языки. Месторасположение названия языка на линии соответствует времени его появления относительно дру- гих языков. Однако это вовсе не означает, что каждый последующий язык обязательно является наследником предыдущего.
Императивная (imperative paradigm), или процедурная парадигма (procedural paradigm), представляет традиционный подход к процессу программирования. Действительно, именно в соответствии с этой парадигмой построен цикл обработки команды центрального процессора: "извлечь-декодировать-выполнить". Как следует из названия, императивная парадигма определяет процесс программирования как запись последовательности команд, которая при выполнении выполнит обработ- ку данных, необходимую для получения желаемого результата. Таким образом, для решения задачи императивная парадигма предлагает попытаться найти алгоритм ее решения.
В противоположность этому, декларативная парадигма (declarative paradigm) во главу угла ставит вопрос "Что пред- ставляет собой задача?", а не "Какой алгоритм нужен для решения задачи?". Основная проблема здесь состоит в том, чтобы создать и реализовать общий алгоритм решения задач. После этого задачи можно формулировать в виде, совместимом с этим алгоритмом, а затем применять его. В этом случае роль программиста заключается в точной формулировке задачи, а не в поисках и реализации алгоритма ее решения.
Основной трудностью в разработке декларативных языков программирования является выбор базового алгоритма ре- шения задач. По этой причине ранние декларативные языки были узкоспециализированными по своей природе и ориентиро- ванными на специфические приложения. Например, декларативный подход уже многие годы применяется для моделирова- ния систем (экономических, физических, политических и т.п.) в целях проверки выдвинутых гипотез. В этом случае базовый алгоритм, в сущности, является процессом моделирования течения времени посредством многократно повторяющегося вы- числения значений параметров (роста внутреннего продукта, торгового дефицита и т.д.) исходя из вычисленных ранее зна- чений. Использование декларативного языка для выполнения такого моделирования сводится, прежде всего, к реализации алгоритма, выполняющего указанную повторяющуюся процедуру. В результате программисту остается лишь описать взаи- моотношения моделируемых параметров. Далее базовый алгоритм моделирования просто имитирует течение времени, ис- пользуя указанные соотношения для выполнения требуемых вычислений.
Не так давно декларативная парадигма нашла свое новое применение – благодаря осознанию того факта, что примене- ние методов математической формальной логики позволяет создавать простые алгоритмы решения задач, подходящие для использования в системах декларативного программирования общего назначения. Результатом пристального внимания уче- ных к декларативной парадигме явилось появление дисциплины логического программирования, которое будет обсуждаться в разделе 5.7.
Функциональная парадигма (functional paradigm) рассматривает процесс разработки программ как конструирование ее из неких "черных ящиков", каждый из которых получает некоторые исходные данные (на входе) и вырабатывает соответст- вующий результат (на выходе). Математики называют такие "ящики" функциями, поэтому этот подход называется функцио- нальной парадигмой. Языковые конструкции функциональных языков программирования состоят из элементарных функций, на основе которых программист должен создавать более сложные функции, необходимые для решения поставленной задачи. Таким образом, согласно функциональной парадигме, программист решает задачу, рассматривая исходные данные, требуе- мые результаты и преобразование, которое необходимо выполнить, чтобы получить результаты из исходных данных. Реше- ние требуемой задачи, вероятнее всего, можно получить, разбивая исходное преобразование на более простые преобразова- ния, порождающие промежуточные результаты, служащие, в свою очередь, исходными данными для других простых преоб- разований. Короче говоря, в соответствии с функциональной парадигмой процесс программирования заключается в конст- руировании требуемых функций в виде вложенных друг в друга совокупностей более простых функций.
Например, на рис. 5.3 показано, как можно построить функцию вычисления среднеарифметического нескольких чисел из трех более простых функций. Первая из них – Sum – получает на вход список чисел и вычисляет их сумму; вторая – Count – получает список чисел и подсчитывает их количество; третья – Divide – получает на вход два числа и вычисляет их частное. На языке LISP (популярном функциональном языке программирования) эта конструкция может быть записана в виде следующего выражения:
(Divide (Sum Numbers) (Count Numbers))
Использование в этом выражении вложенных структур отражает, что исходные данные для функции Divide являются результатами выполнения функций Sum и Count. В качестве другого примера предположим, что у нас есть функция Sort, которая сортирует список чисел, и функция First, которая находит первое число в этом списке. В этом случае приведенное ниже выражение позволяет извлечь из списка List наименьшее из чисел:
(First (Sort List))
![]() |
Рис. 5.3. Функция вычисления среднеарифметического для списка чисел, построенная из более простых функций Sum, Count и
Divide
В данном случае использование вложенных структур означает, что результат функции Sort является исходной инфор- мацией для функции First. Таким образом, список сначала сортируется, а затем из отсортированного списка извлекается
первое число.
Превосходство функциональной парадигмы программирования над императивной проявляется в том, что она стимули- рует модульный подход к конструированию программ. Действительно, то, что программы рассматриваются как функции, которые, в свою очередь, должны состоять из других функций, вынуждает программиста думать в терминах модулей. По этой причине сторонники функционального программирования утверждают, что этот подход приводит к созданию более высокоорганизованных программ, чем в случае применения императивной парадигмы. Более того, многие утверждают, что функциональная парадигма является естественной средой для метода, предусматривающего построение программ из "строи- тельных блоков". Данный подход напоминает скорее конструирование программ из заранее подготовленных блоков, нежели выполнение всей работы с нуля. Такому способу программирования отдают предпочтение в основном специалисты по раз- работке больших пакетов программ. Эти же аргументы приводятся и в защиту объектно-ориентированной парадигмы.
Объектно-ориентированная парадигма (object-oriented paradigm), которая предполагает применение методов объектно- ориентированного программирования (ООП), – это еще один подход к процессу разработки программного обеспечения. В рамках этого подхода элемент данных рассматривается как активный "объект", а не как пассивный элемент, как это принято в традиционной императивной парадигме. Поясним это на примере списка имен. В традиционной императивной парадигме этот список рассматривается просто как совокупность некоторых данных. Любая программа, получающая на вход этот спи- сок, должна содержать алгоритм выполнения над ним требуемых действий. Таким образом, список является пассивным объ- ектом, поскольку он обрабатывается управляющей программой, а не обрабатывает себя сам. Однако при объектно- ориентированном подходе список рассматривается как объект, содержащий некоторую совокупность данных вместе с набо- ром процедур для их обработки. Этот набор может включать процедуры для вставки в список нового элемента, удаления элемента из списка или сортировки списка. Поэтому программа, получающая доступ к списку для его обработки, не обязана содержать алгоритм для выполнения указанных действий. При необходимости она просто выполняет процедуры, предостав- ляемые самим объектом. В этом смысле объектно-ориентированная программа вместо сортировки списка (как при импера- тивной парадигме) скорее просит список отсортировать самого себя.
Язык Visual Basic. Visual Basic – это объектно-ориентированный язык программирования, разработанный компани- ей Microsoft в качестве инструмента, с помощью которого пользователи операционной системы Microsoft Windows мог- ли бы создавать собственные графические интерфейсы пользователя (GUI). В действительности Visual Basic – это нечто больше, чем просто язык программирования. Это – мощный интегрированный пакет разработки программного обеспе- чения, позволяющий программисту создавать графический интерфейс пользователя из заранее определенных компонентов (таких, как кнопки, флажки опций, текстовые поля, полосы прокрутки и т.п.) и настраивать работу этих компонентов в при- ложении, описывая их реакцию на различные события. Например, если речь идет о кнопке, программист должен описать, что должно случиться, если пользователь щелкнет на ней. В главе 6 мы увидим, что эта стратегия создания программ из готовых компонентов представляет собой важнейшую современную тенденцию в области разработки программного обеспечения.
Популярность операционной системы Windows и удобство пакета для разработки программ Visual Basic способство- вали тому, что язык Visual Basic в настоящее время стал одним из наиболее известных и широко используемых языков программирования.
В качестве другого примера использования объектно-ориентированного подхода рассмотрим задачу разработки графи- ческого интерфейса пользователя. В этом случае все отображаемые на экране графические элементы реализуются как объек- ты. Каждый из этих объектов включает собственный набор процедур, определяющих реакцию объекта на возникновение различных событий, – выбор этого объекта, щелчок на нем кнопкой мыши или перетаскивание его по экрану. Таким обра- зом, вся система в целом выглядит как совокупность объектов, каждый из которых знает, как реагировать на определенное событие.
Многие из преимуществ объектно-ориентированного проектирования являются следствием модульной структуры, ко- торая возникает как естественный побочный эффект от применения объектно-ориентированного подхода. В рамках этого подхода каждый объект реализуется в виде отдельного, точно определенного элемента. После того как свойства некоторой сущности будут определены подобным образом, полученное определение можно повторно использовать всякий раз, когда возникнет потребность в этой сущности. По этой причине сторонники объектно-ориентированного программирования ут- верждают, что объектно-ориентированная парадигма предоставляет естественную среду для конструирования программного обеспечения из "строительных блоков". Они предсказывают появление программных библиотек, содержащих определения различных объектов, с помощью которых новое программное обеспечение можно будет собирать точно так же, как обычные промышленные изделия собирают из готовых компонентов.
Объектно-ориентированная парадигма оказывает все большее влияние на область компьютерных наук, поэтому в раз- деле 5.5 мы детально обсудим ее особенности. Кроме того, в последующих главах мы вновь и вновь будем встречаться с проявлениями этой парадигмы. В частности, будет показано, какое влияние оказала объектно-ориентированная парадигма на методы разработки программного обеспечения (глава 6) и проектирования баз данных (глава 9), а в главе 7 мы увидим, как объектно-ориентированный подход к разработке программного обеспечения естественным образом обобщает результаты исследований в области структур данных.
Наконец, следует заметить, что процедуры объекта, описывающие, как объект должен отвечать на различные сообще- ния, в сущности, представляют собой небольшие императивные программные единицы. Поэтому большинство объектно- ориентированных языков программирования обладают свойствами императивных языков. Например, распространенный объ- ектно-ориентированный язык C++ был создан добавлением к императивному языку С объектно-ориентированных свойств. В раз- делах 5.2 и 5.3 мы рассмотрим общие характеристики императивных и объектно-ориентированных языков и понятия, кото- рые объединяют современное программное обеспечение.
1. В каком смысле программа на языке третьего поколения является машинно-независимой? В каком смысле она оста- ется машинно-зависимой?
2. Какая разница между ассемблером и компилятором?
3. Императивную парадигму программирования можно кратко охарактеризовать, просто сказав, что она делает акцент на описании процесса, который ведет к решению поставленной задачи. Дайте аналогичное краткое описание декларативной, функциональной и объектно-ориентированной парадигм программирования.
4. В каком смысле языки программирования третьего поколения являются языками более высокого уровня, чем языки предыдущих поколений?
Материалы на данной страницы взяты из открытых источников либо размещены пользователем в соответствии с договором-офертой сайта. Вы можете сообщить о нарушении.