Опубликован: 06.10.2011 | Уровень: для всех | Доступ: платный
Лекция 4:

Языки программирования

< Лекция 3 || Лекция 4: 123 || Лекция 5 >
Аннотация: В лекции продолжается рассмотрение языков программирования. Вводятся понятия компилятора и интерпретатора программ. Даются основы компиляции. Рассматривается инструментарий, используемый на всех этапах работы с программами, начиная от текстовых редакторов, заканчивая рассмотрением интегрированных сред разработки, в частности среды разработки Eiffel Studio.

За последние четыре десятилетия программный инструментарий коренным образом изменил способы проектирования и создания промышленных изделий – автомобилей и лекарств, газет, зданий, мостов, список можно продолжать. Этот инструментарий получил специальное имя – CAD (Computer Aids Design), CAM (Computer Aids Manufacturing), системы автоматизированного проектирования и автоматизированного производства.

ПО – это тоже промышленное изделие, и его производство требует проектирования. Опровергая старую поговорку "Сапожник ходит без сапог", программисты позаботились о создании инструментария, обеспечивающего их потребности, начиная от простых текстовых редакторов до интегрированных сред разработки, подобных EiffelStudio или VisualStudio от Microsoft.

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

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

Программный инструментарий предлагает широкий набор возможностей, начиная со средств, непосредственно связанных с языком программирования: компилятор и интерпретатор, которые можно рассматривать как близнецов-братьев. Мы поговорим о средствах подготовки программ – текстовых редакторах. Поговорим и о более мощном средстве автоматизированного проектирования ПО – инструментарии CASE (Computer Aided Software Engineering).

Отладчики, статические анализаторы и средства тестирования позволяют нам оценить и улучшить надежность программ. Средства управления конфигурацией позволяют нам сохранять историю развития компонентов ПО. Браузеры и средства документирования помогают понимать работу ПО на разных уровнях абстракции, что позволяет справиться с его сложностью. Метрический инструментарий дает нам количественные оценки качества разработки. Закончим это перечисление упоминанием IDE – интегрированной среды разработки, включающей, как правило, большинство из отмеченных средств и предоставляющей разработчику единые рамки для его многогранной работы. В последнем разделе лекции будет дана краткая характеристика EiffelStudio, в частности, описан ее подход к компиляции – Технология Тающего Льда (Melting Ice Technology).

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

3.1. Стили языков программирования

Языки программирования играют центральную роль в разработке ПО. Ядро этого курса использует язык Eiffel, который хорош тем, что позволяет в первую очередь сосредоточиться на концепциях программирования, а не на специфике конкретного языка. В приложениях приведена специфика четырех практически наиболее важных для практики языков: Java, C#, C++ и C. Сейчас же мы рассмотрим общие критерии, позволяющие классифицировать языки программирования, познакомимся с семейством ОО-языков и с другим популярным семейством, обладающим своими исключительными свойствами.

Критерии классификации

Языки программирования могут классифицироваться по разным критериям. Вот несколько наиболее важных.

  • Область применения. Некоторые языки являются языками общецелевого использования. Другие ориентированы на определенную область, например, разработку Web-сайтов или разработку систем реального времени. Языки второй группы относят к языкам DSL (domain-specific languages), ПОЯ – проблемно-ориентированным языкам. Классификация языка со временем может быть подвергнута пересмотру, так как успешные языки в процессе развития преодолевают первоначальные замыслы. Примером является первый язык программирования Фортран, создававшийся как язык численного программирования (вычисления формул), а ставший общецелевым языком программирования в научных вычислениях. Другим примером служит язык Java, который первоначально создавался как язык для разработки ПО различных бытовых приборов, затем, с появлением Интернета, был переориентирован на создание апплетов – программ, загружаемых через Интернет, теперь же язык Java является ОО-языком общецелевого использования.
  • Область действия программы. Некоторые языки предназначены для создания масштабируемых приложений (большой размер кода, много разработчиков, разработка и сопровождение ведется в течение многих лет). У других – цели более простые: небольшие разработки, проведение различных экспериментов, проверка гипотез. Так называемые Script (скриптовые) языки обычно относят ко второму типу. Конечно, нет гарантии, что первоначально небольшая программа в ходе эволюции не превратится в многофункциональную, долго живущую, большую программу. Как следствие, успешные языки второй категории в конечном итоге часто применяются к большим разработкам. Примером может служить офисное программирование на языке, встроенном в систему Microsoft Office.
  • Возможность верификации. Некоторые языки проектируются так, чтобы компилятор мог найти потенциальные ошибки, – в результате анализа текста еще до выполнения можно будет гарантировать некоторые свойства периода выполнения. Обычно это накладывает на программиста дополнительные обязательства, так как верификация может требовать дополнительной информации (например, описания типов всех программных сущностей). Некоторые языки предпочитают простоту выражений, снимая требования и облегчая жизнь программиста в момент написания кода. Другое дело, что сделанные программистом ошибки проявятся в этом случае лишь на этапе выполнения.
  • Уровень абстракции. Некоторые языки опираются непосредственно на ниже лежащий машинный уровень. Другие предпочитают использовать абстрактную модель вычислений.
  • Роль жизненного цикла. Некоторые языки учитывают только проблемы реализации. Другие могут помочь на всех этапах жизненного цикла, на этапах моделирования системы, ее анализа и проектирования.
  • Императивный или декларативный стиль описания программы. В императивных языках программы выполняют команды, изменяющие состояние программы. Дескриптивные языки по духу ближе к математике, позволяя описать требуемые свойства, но не выписывая точных шагов по их достижению.
  • Архитектурный стиль. Он определяет то, как происходит декомпозиция системы на модули. Два главных подхода характерны для программирования – строить модули на основе функций или на основе типов объектов. Языки соответствующих двух стилей называются процедурными ("функциональные языки" означает нечто другое, о чем позже пойдет речь) и объектно-ориентированными языками.

Для конкретного языка возможна почти любая комбинация этих характеристик. Стиль программ этого курса, иллюстрируемый программами на Eiffel, характерен и для таких языков, как C# и Java. Он предполагает:

  • общецелевое использование (специализация допускается благодаря библиотекам), пригодность для больших разработок;
  • возможность статического контроля текста на этапе компиляции, высокий уровень абстракции (с возможностью учета нижнего машинного уровня опять-таки с помощью библиотек);
  • учет жизненного цикла (в частности, это верно для языка Eiffel, который повседневно используется для спецификации и проектирования);
  • императивность и объектную ориентированность языка.

Другим примером является язык С, императивный, но не объектно-ориентированный, с довольно низким уровнем абстракции, но позволяющий управление выполнением на почти машинном уровне. Язык С++ добавляет объектный слой к языку С.

Продолжая обсуждение языков программирования, рассмотрим:

  • введение в стиль языков программирования, относящихся к "дескриптивной" категории, – языков функционального программирования, радикально отличающихся от доминирующей практики сегодняшнего дня;
  • некоторые основы ОО-языков.

Функциональное программирование и функциональные языки

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

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

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

Будем использовать следующую терминологию.

Определения: императивный, аппликативный
Стиль программирования, основанный на изменении состояния, называется императивным. Синоним для противоположного стиля – аппликативный.

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

Первым функциональным языком, включающим и некоторые императивные свойства, был язык Лисп (Lisp – List Processing), разработанный Джоном Маккарти и представленный в 1959 году.

Джон Маккарти

Рис. 3.1. Джон Маккарти

Центральной концепцией языка является список, записываемый в круглых скобках:

(A B C)
        
Заметьте, в Лиспе термин "список" используется в особом смысле, который отличается от списковой структуры, изучаемой в предыдущих лекциях. Списки Лиспа фактически близки к бинарным деревьям – структуре данных, которую мы будем изучать в отдельной лекции.

Списки являются структурой данных (фактически, единственной структурой данных в Лиспе, достаточно общей для покрытия широкого разнообразия приложений). Более того, они также задают структуру программы. Если A обозначает функцию, то в вышеприведенном примере список обозначает применение этой функции к списку аргументов, заданному оставшейся частью списка, – в более привычной нотации этот список записывается в виде A(B, C). Мощь, простота и элегантность идей вдохновила многих людей и привела к тому, что большинство ранних работ по ИИ (искусственному интеллекту) основывались на Лиспе. Язык и до сей поры продолжает развиваться и активно использоваться, имея многочисленных потомков, в частности, язык Scheme, сохранивший основные концепции.

Даже не детализируя остальные концепции языка, заметим, что в самой базисной концепции заложена возможность использования функционалов – функций высшего порядка, то есть таких функций, аргументами которых являются функции. Рассмотрим список:

((F A B)(G C) D)
        

Это список из трех элементов, первый из которых сам является списком с тремя элементами, второй – список из двух элементов. Можно рассматривать F как функцию от двух аргументов, возвращающую в качестве результата функцию от двух аргументов. Обозначим через H функцию, задающую результат применения функции F к ее аргументам, а через E – результат применения функции G к аргументу C; тогда представленное выражение списка в целом (H E D) может означать применение функции H к ее аргументам E и D. Это общий и замечательно мощный механизм.

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


Рис. 3.2. Симон Джонс и Фил Вадлер

Для более близкого знакомства со стилем функционального программирования позвольте перейти от Лиспа к более современному и популярному языку Haskell. Основной вклад в разработку этого языка внесли Симон Пейтон Джонс и Фил Вадлер.

Ограничимся только одним примером, иллюстрирующим идеи функционального программирования, а также представляющим прекрасную подготовку для предстоящего изучения рекурсии.

Вспомним алгоритм обращения списковой структуры. Он включает манипуляции с указателем позиции списка. А нет ли способа получения результата, – ведь само понятие обращенного списка достаточно просто, – без обращения к таким деталям, как указатели позиции? Функциональное программирование такую возможность предоставляет, если вы согласны не обращать внимания на проблемы производительности и возлагаете их решение на "умный" компилятор. Список в Haskell записывается в квадратных скобках. Следующее определение задает функцию, обращающую список:

reverse:: [T] –> [T]
reversed [ ]                     = [ ]                              [1]
        
Листинг 3.1.
reversed (first:remainder) = reversed remainder ++ [first]          [2]
        
Листинг 3.2.

Это все, что нужно написать. Первая строка является описанием типа и говорит, что reverse – это функция, на вход которой подается список элементов типа T и которая возвращает в качестве результата список элементов того же типа. Определение этой функции дается в последующих двух строчках, представляющих разбор случаев.

  • Первая строчка (случай [1] задает нерекурсивную ветвь определения), устанавливает, что обращение пустого списка является пустым списком.
  • Вторая строчка (случай [2] задает рекурсивную часть определения) рассматривает обращение непустого списка.

Непустой список может быть всегда представлен в виде first : remainder, нотации, которая представляет список состоящим из непустого первого элемента – first, и остатка списка – remainder, который является списком, возможно, пустым. При таком представлении обращение списка может быть задано конкатенацией обращения остатка remainder и головы спискаfirst. Для обращения остатка рекурсивно может использоваться функция reversed. Заметьте, в Haskell для конкатенации применяется знак операции ++, при вызове функции не используются круглые скобки и вызов функции связывает сильнее (имеет больший приоритет, чем операция конкатенации). В более привычной математической нотации правая часть в [2] могла бы быть записана в виде: (reversed(remainder))++ [first].

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

Это определение аппликативно, здесь нет ссылок на состояния и их изменения, не вводятся переменные, которым программа может присваивать новые значения.

Хотя, как вы можете догадаться, в Haskell и других функциональных языках есть много других элементов, но и этот небольшой пример передает простоту и элегантность концепций функционального программирования.

Почему же мы все не переключились до сих пор на функциональное программирование? Вопрос для полемики – горячие сторонники функционального программирования считают, что мир должен переключиться, но следует отметить три важные проблемы.

  • Производительность. Простота решения, продемонстрированного выше, может потребовать существенных накладных расходов – памяти и/или времени выполнения. Стоит заметить, что документация по функциональному программированию начинается обычно с приведения примеров, подобных рассмотренному, а затем следует рекомендация использовать более сложные варианты, обеспечивающие эффективность вычислений.
  • Масштабируемость. Для структурирования больших систем понятие класса более эффективно, чем понятие функции. Следует заметить, что многие функциональные языки встраивают в язык ОО-конструкции.
  • Отсутствие понятия состояния. Несмотря на то, что чисто аппликативные языки могут существенно упрощать понятие алгоритма, работая на возможно сложных структурах данных, некоторые аспекты вычислений фундаментально требуют введения понятия состояния. Уже упоминались операции по вводу и выводу данных, можно упомянуть и системы реального времени. Функциональные языки, Haskell, в частности, добавляют императивные аспекты программирования, но они не так просты, как базисная функциональная модель.

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

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

ОО-языки

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



Рис. 3.3. Кристин Нигард и Улле Дал

Достаточно просто проследить за местом, временем и создателями этой технологии. Место – это город Осло в Норвегии, время – начало и середина 60-х годов прошлого столетия, создатели – Уле-Йохан Дал из университета Осло и Кристин Нигард из Норвежского вычислительного центра.

Совместно они спроектировали язык для моделирования дискретных событий. Первая версия языка – Симула 1 – предназначалась для моделирования на компьютере производственных процессов, рассматриваемых как последовательность событий. В 1967 году в журнале Communications of the ACM появилась их статья, посвященная описанию уже общецелевого языка программирования, где были введены основные идеи ОО-программирования. Прошло уже более сорока лет, но можно только удивляться, как много было предусмотрено в этой работе: классы, объекты, динамическое распределение памяти и сборка мусора, наследование (только одиночное), динамическое связывание и полиморфизм.

Странным образом эта работа настолько опередила время, что академическое сообщество оказалось незаинтересованным. Только несколько лет спустя появилась теория, поясняющая объектную технологию. Работа Парнаса по скрытию информации появилась в 1972 году. Абстрактные типы данных (АТД) были представлены в короткой статье в 1974 году Барбарой Лисков и Стефаном Зиллесом. Прочный математический фундамент АТД получили в диссертации Джона Гуттага в 1975 году.

Барбара Лисков с Дональдом Кнутом

Рис. 3.4. Барбара Лисков с Дональдом Кнутом

Первоначально, как было отмечено, язык Симула появился как язык моделирования. С этих пор одна из центральных идей ОО-подхода состоит в том, что программа является средством моделирования. Нигард в своих выступлениях часто использовал лозунг "Запрограммировать – значит понять" и гордился тем, что первый отчет о языке Симула назывался "Язык для Программирования и Моделирования". Моделирование дискретных событий с упором на описание внешних процессов представляло идеальную целевую область для разработки такой точки зрения. Эволюция от специализированной Симулы 1 до общецелевого языка Симула 67 показала общую применимость предлагаемых идей. ОО-концепции успешны, поскольку они эффективно поддерживают моделирование процессов в самых различных проблемных областях – банковских системах, обработке изображений, подготовке текстовых документов. Мы описываем такие системы, вводя соответствующие типы данных (ACCOUNT, IMAGE, PARAGRAPH), организуя иерархию наследования (INTERNAL_ACCOUNT как специальный случай ACCOUNT), применяя скрытие информации, чтобы быть уверенными, что каждый такой тип можно разрабатывать независимо. Введение контрактов позволяет задать спецификацию таких типов объектов. Эти идеи, за исключением последней, были уже представлены в Симуле – ее создатели ясно осознавали потенциал языка.

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

Но идеи со временем пробивают себе дорогу. В университете Юта – центре графических исследований – Алан Кей в своей диссертации объединил идеи Симулы и Лиспа. Он был приглашен в Исследовательский центр фирмы Xerox (PARC), расположенный в Пало-Альто, в Калифорнии – месте, где рождены многие замечательные новинки, аппаратные и программные, Там он создал первую версию языка Smalltalk и его программного окружения. Двумя другими ключевыми членами группы разработчиков были Адель Голдберг и Дан Инголс.



Рис. 3.5. Алан Кей, Адель Голдберг

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

С появлением успешных версий Smalltalk 76 и Smalltalk 80 интерес к языку возрастал, но поклонники языка, подобно поклонникам Симулы, составляли скорее клуб, чем широкий поток в океане программирования. В 1981 году журнал Byte – флагман быстро растущего сообщества энтузиастов персональных компьютеров – решил опубликовать специальный номер, посвященный исключительно Smalltalk, хотя он не был доступен на компьютерах большинства читателей. В Августе 1981 года такой номер (теперь библиографическая редкость) вышел под редакцией Адель Голдберг, открыв широкую дорогу новому взлету ОО-программирования. Когда в 1986 году ACM организовал конференцию OOPSLA (Object-Oriented Programming, Systems, Languages, Applications, с тех пор ежегодную), ожидалось, что число ее участников будет представлять сотню человек, а оказалось, что оно перевалило за тысячу. Стали появляться новые языки. Бред Кокс использовал Smalltalk в ITT – мощной телекоммуникационной компании; Бьерн Страуструп из AT&T, рассматривавший Симулу в своей диссертации, решил расширить наиболее модный тогда язык С новыми идеями – так появились языки Objective-C и C++. Появление языка Eiffel также относится к этому периоду.

В течение нескольких лет эти языки преодолели первоначальное сопротивление индустрии, в частности, доказав эффективность реализации, и стали вначале важными игроками, а затем заняли доминирующие позиции. Позже, в 1995 году, появился язык Java, а еще через четыре года – и язык C#.

Со времен первой конференции OOPSLA критики предсказывают конец "эры объектов". Но никаких признаков такого исхода никогда не наблюдалось. Объекты продолжают процветать, как говорил один из героев – "в городе нет другой игры".

< Лекция 3 || Лекция 4: 123 || Лекция 5 >
Ольга Попова
Ольга Попова
Россия
Михаил Окнов
Михаил Окнов
Россия