Рейтинг пользователей: / 0
ХудшийЛучший 

УДК 519.683.8

 

Головин И.Г., Столяров А.В.

ОБ ОДНОМ ПОДХОДЕ К ИНТЕГРАЦИИ РАЗНОРОДНЫХ СЕМАНТИЧЕСКИХ ВОЗМОЖНОСТЕЙ В РАМКАХ ОДНОЯЗЫЧНЫХ СИСТЕМ ПРОГРАММИРОВАНИЯ

Московский Государственный Университет им.М.В.Ломоносова

 

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

Ключевые слова: языки программирования, непосредственная интеграция, Си++, Лисп.

The report is devoted to a practical solution of the problem of alternative expressive means integration within a single programming project. A brief classification of existing methods is given; their drawbacks are discussed. An approach named “immediate integration” is presented as a solution for the problem.

Key words: programming languages, immediate integration, C++, Lisp.

При реализации большинства современных программистских проектов используется один избранный язык программирования; чаще всего это представитель группы императивных объектно-ориентированных языков, таких как C++ [1], Java, Delphi или Ada95 [2]. Императивная составляющая такого языка позволяет описывать работу программы в терминах, приближенных к возможностям компьютера (последовательное выполнение инструкций по модификации значений в оперативной памяти). Объектно-ориентированная составляющая [3] позволяет строить модели предметной области в терминах объектов - «черных ящиков», некоторым образом меняющих свое состояние в ответ на получение сообщений.

В то же время существуют языки, предоставляющие совершенно иные изобразительные средства и стимулирующие мышление в иных терминах. Так, функциональные языки (Lisp [4], Hope [5] и др.) предлагают представление программы в виде системы взаимнорекурсивных функций, построенных на обращениях к неким базовым («системным») функциям. Язык Refal [6] также предоставляет возможность описания программы как системы функций, но основным механизмом работы отдельной функции является сопоставление выражений с образцами и преобразование выражений. Системные функции в Refal'е также присутствуют, но играют скорее вспомогательную роль. Наконец, языки логического программирования [7], и прежде всего Prolog (а также Datalog, Loglisp и др.), предлагают описывать программу в виде набора логических фактов и связок.

Часто в рамках одного проекта возникает потребность использовать различные выразительные средства, наиболее подходящие для решения частных подзадач. Так, если в некотором проекте требуется подсистема лексического анализа, для ее реализации был бы идеальным язык Refal. Символьные преобразования математических формул удобно задавать с помощью языка Lisp. Для решения переборной задачи методом проб и ошибок лучше всего подойдет Prolog или другой логический язык со встроенным механизмом поиска с возвратом. Использование этих языков для решения соответствующих подзадач могло бы существенно снизить трудозатраты на создание всей программы и повысить ее качество.

К сожалению, технические трудности, возникающие при интеграции языков, имеющих разную природу, на практике перевешивают преимущества многоязыкового (мультипарадигмального) программирования. Ниже коротко рассматриваются основные существующие способы решения данной проблемы, а также предлагается новый подход - так называемый метод непосредственной интеграции. Метод основан на создании библиотек классов для какого-либо популярного языка программирования, например С++, реализующих вычислительную модель и структуры данных альтернативных языков программирования. Работоспособность предложенного подхода демонстрируется на примере библиотеки классов InteLib для программирования на С++ в стиле языка Lisp.

 

ОБЗОР СУЩЕСТВУЮЩИХ ПОДХОДОВ

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

• пакеты взаимосвязанных программ;

• встраиваемые интерпретаторы;

• расширяемые интерпретаторы;

• компиляция из одного языка в другой;

• создание нового языка;

• расширение существующего языка.

 

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

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

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

Сформулируем основные недостатки рассмотренной методики.

Модуль, написанный в альтернативной системе программирования, не может вызывать функции основной программы, и наоборот. Также отсутствует возможность совместного использования глобальных переменных. При применении интерпретаторов на этапе выполнения приходится выполнять лексический и синтаксический анализ кода, что снижает эффект от применения в проекте компилируемых языков. При организации обмена данными через текстовое представление мы вынуждены анализировать текстовые результаты во всех программах пакета, в том числе и на базовом языке (например, С++). Необходимо отметить, что лексический и синтаксический анализ - это отдельный класс задач, для которых был бы удобнее, например, язык Refal. При таком же решении вынести анализ текста в код на другом языке, очевидно, невозможно.

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

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

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

Еще один подход основан на компиляции исходного текста не в объектный код, как это обычно делается, а в код на другом языке высокого уровня, чаще всего - С. В частности, так построены некоторые компиляторы языка Scheme.

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

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

Решить проблему мультипарадигмального программирования можно также путем создания некоторого нового языка программирования, в который вносятся изобразительные средства большого числа различных парадигм (в идеале - всех или почти всех концепций программирования). В качестве характерных примеров можно назвать языки Leda [10], Oz [11] и другие.

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

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

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

 

НЕПОСРЕДСТВЕННАЯ ИНТЕГРАЦИЯ

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

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

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

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

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

Этим требованиям удовлетворяют, в частности, такие языки, как C++ и Ada95. В то же время Java, Delphi и некоторые другие языки удовлетворяют первым двум требованиям, но не дают возможности перекрытия стандартных символов операций.

Для экспериментальной реализации метода мы выбрали в качестве базового язык C++. В настоящее время реализованы библиотеки классов, моделирующие изобразительный стиль языков Lisp, Scheme, Planner и Refal (библиотека InteLib[8]), ведутся работы по реализации моделей языков Prolog и Erlang и других.

Рассмотрим примеры конструкций C++, использующих библиотеку InteLib и моделирующих S-выражения языка Lisp.

 

(L| 25, 36, 49)                // (25 36 49)

(L| "I am the walrus", 1965)   // ("I am the walrus"
                               //1965)

(L| 1, 2, (L| 3, 4), 5, 6)     // (1 2 (3 4) 5 6)

(L| (L| 1, 2), 3, 4)           // ((1 2) 3 4)

(L| MEMBER, 1, ~(L| 1, 3, 5))  // (member 1 '(1 3 5))

(L| 1 || 2)                    // (1 . 2)

((L| 1, 2, 3)|| 4)             // (1 2 3 . 4)

 

Поясним, что L - это экземпляр класса LListConstructor, операция | которого преобразует свой правый операнд в S-выражение -список из одного элемента; операция «запятая» добавляет новый элемент в конец существующего списка; унарная операция ~ (тильда) конструирует S-список вида (QUOTE <операнд>), то есть заменяет традиционный лисповский апостроф. Традиционное лисповское понятие S-выражения реализовано в виде иерархии полиморфных классов.

Рассмотрим лисповское определение функции:

                                        

  (defun isomorphic (tree1 tree2)

   (cond ((atom tree1) (atom tree2))

         ((atom tree2) NIL)

         (t (and (isomorphic (car tree1)

                             (car tree2))

                 (isomorphic (cdr tree1)

                             (cdr tree2))))))

 

Эквивалентный код на языке C++ может выглядеть так:

 

#include <intelib/sexpress/sexpress.hpp>

#include <intelib/lisp/lisp.hpp>

#include <intelib/lisp/lsymbol.hpp>

#include <intelib/lisp/lform.hpp>

#include <intelib/lfun_std.hpp>

#include <intelib/lfun_sel.hpp>

LSymbol ISOMORPHIC("ISOMORPHIC");

void LispInit_isomorphic() {

  static LSymbol TREE1("TREE1");

  static LSymbol TREE2("TREE2");

  static LFunctionalSymbol<LFunctionDefun>
               DEFUN("DEFUN");

  static LFunctionalSymbol<LFunctionCond>
               COND("COND");

  static LFunctionalSymbol<LFunctionAtom>
               ATOM("ATOM");

  static LFunctionalSymbol<LFunctionAnd> AND("AND");

  static LFunctionalSymbol<LFunctionCar> CAR("CAR");

  static LFunctionalSymbol<LFunctionCdr> CDR("CDR");

  (L|DEFUN, ISOMORPHIC, (L|TREE1, TREE2),

    (L|COND,

      (L|(L|ATOM, TREE1), (L|ATOM, TREE2)),

         (L|(L|ATOM, TREE2), NIL),

         (L|T, (L|AND,

           (L|ISOMORPHIC, (L|CAR, TREE1),

                          (L|CAR, TREE2)),

           (L|ISOMORPHIC, (L|CDR, TREE1),

                  (L|CDR, TREE2)))))).Evaluate();

}

 

Здесь Evaluate() - это метод класса, представляющего S-выражение, который производит вычисление выражения в смысле языка Lisp.

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

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

 

ЗАКЛЮЧЕНИЕ

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

 

 

 

Литература:

1. Stroustrup B., The Design and Evolution of C++. Addison-Wesley. Reading, Massachusets, 1994.

2. Mitchell R., Abstract Data Types and Ada. Prentice Hall, 1996.

3. Booch G., Object-Oriented Analysis and Design.Addison-Wesley. 1994.

4. Steele G. L., Common Lisp the Language, 2nd edition. Digital Press, 1990.

5. Field A., Harrison P., Functional Programming. Addison-Wesley, Reading, Massachusets, 1988.

6. Turchin V., REFAL-5, Programming Guide and Reference Manual. New England Publishing Co., Holyoke, 1989.

7. Robinson J., Logic programming - past, present and future // New Generation Computing, 1, 1983, p.107-121.

8. Stolyarov A., Bolshakova E., Building Functional Techniques into an Object-oriented System. Knowledge-Based Software Engineering // Proceedings of the 4th JCKBSE, Brno, Czech Republic, 2000. IOS Press, pages 101-106.

9. И. Г. Головин, А. В. Столяров. Объектно-ориентированный подход к мультипарадигмальному программированию. Вестник МГУ, сер. 15 (ВМиК), N 1, 2002 г., стр. 46--50.

10. Stolyarov A., A framework of heterogenous dynamic data structures for object-oriented environment: the S-expression model // Proceedings of the 6th JCKBSE, vol.108 of Frontiers in Artificial Intelligence and Applications, Protvino, Russia, August 2004. IOS Press, pages 75-82.

11. А. В. Столяров. Об одном подходе к построению универсальных языков программирования. Сборник статей молодых учёных факультета ВМиК МГУ, N 4. М.: Издательский отдел факультета ВМК МГУ, 2007, стр. 135--146.

12. T.A. Budd., Multy-Paradigm Programming in LEDA. Addison- Wesley, Reading, Massachusets, 1995.

13. M. Mueller, T. Mueller, and P. Van Roy., Multiparadigm programming in Oz. // Workshop on the Future of Logic Programming. International Logic Programming Symposium, 1995.