Алексей Яковлев
ENGLISH
Избранные проекты » Аппарат развития на синтаксическом уровне
Главная страница
Последние новости
Избранные проекты
Загрузка файлов
Чужие проекты
Ссылки по теме
Панель управления
Выбор языка
Русский (по умолчанию)
English
Выбор палитры
Сирень (по умолчанию)
Кирпичные стены
Серебристые тени
Тонкие линии
Стиль отображения
Графический (по умолчанию)
Только текст (для печати)

Аппарат развития на синтаксическом уровне

А. Н. Яковлев

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

1. Введение

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

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

Следует отметить, что в настоящей статье рассматриваются лишь проекты, исполняемые модули которых ориентированы на одну целевую программно-аппаратную платформу. В данном случае имеется в виду не переносимость, а монолитность программ. Проектами подобного рода являются, например, сервер Apache, семейство компиляторов gcc, операционная система Windows NT и другие. Исходный текст этих проектов может быть скомпилирован для разных платформ, однако каждая отдельная сборка подразумевает лишь одну платформу (например, Apache для Linux/x86, Windows NT/Alpha и т.п).

2. Ситуации смешивания разных языков

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

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

2. Применение какого-либо специализированного языка для взаимодействия разных подсистем в рамках одного проекта. Примером может быть использование SQL-запросов для доступа к источникам данных в проектах автоматизации документооборота или бухгалтерских программах. На Win32/Win64-платформах также довольно часто встречается использование языков сценариев вроде VBScript или JScript. В случае с SQL-запросами мы имеем дело с достаточно стандартизованным языком, поэтому мы вправе рассчитывать на то, что при переходе на другие источники данных запросы не придется составлять заново. Полагаясь на языки сценариев вроде VBScript, мы попадаем в зависимость к производителю платформы Win32/Win64 и отказываемся от всех других платформ. Кроме того, в случае использования языков запросов или сценариев, мы имеем дело с компиляцией на этапе выполнения программы, что может совершенно непредсказуемым образом повлиять на эффективность программы при каких-либо изменениях исходных данных. Подобного рода непредсказуемость совершенно недопустима в т.н. системах жесткого реального времени (например, при управлении технологическими процессами).

3. Наконец, специфический случай использования смеси языков разного уровня в рамках одной единицы трансляции. Этот прием чаще всего используется в библиотеках языков высокого уровня для реализации взаимодействия каких-либо разнородных модулей (например, для доступа к функциям операционной системы). Примером подобной смеси является оператор "asm" для записи встраиваемых команд языка ассемблера в языках Паскаль, Си и Си++. Практически всегда это крайне плохо стандартизованное средство, опирающееся на определенный синтаксис ассемблера для определенной аппаратной платформы. Использование подобного низкоуровнего средства может приветствоваться исключительно в библиотеках, ориентированных только на определенный компилятор. Преимущество данного подхода – в его максимальной эффективности. Кроме того, иногда такой подход является совершенно необходимым. Недостатки его – плохая переносимость, небезопасность и необходимость усложнения компилятора исходного языка высокого уровня. Из этих недостатков производители компиляторов обычно стараются бороться только с плохой переносимостью (использование директив #pragma aux в компиляторах WATCOM или расширенного синтаксиса ассемблера в компиляторах семейства gcc – GNU Compiler Collection).

3. Преимущества и недостатки смешивания языков

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

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

Каких недостатков любой из описанных технологий мы хотели бы избежать:

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

Скорее всего, это означает:

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

Одну из возможных альтернатив представляет собой семейство компиляторов gcc, использующих один генератор кода, поддерживающий несколько платформ, и множество front-end компиляторов для разных языков (C, C++, Objective C, Fortran 77 и другие). Другая альтернатива – язык, допускающий развитие на уровне изменения лексики и синтаксиса – сейчас обсуждается как возможность, существующая лишь гипотетически.

4. Концепция аппарата развития языка на уровне синтаксиса

Используя терминологию [1], под аппаратом развития языка будем понимать средства описания и переопределения языковых абстракций, а также сопутствующие им средства конкретизации. При этом будем различать "развитие вверх" (аппарат определения и использования новых абстракций) и "развитие вниз" (уточнение и переопределение компонент базиса или ранее введенных абстракций). Аппарат развития иногда называют аппаратом абстракции-конкретизации (или обобщения-спецификации).

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

   -- листинг 1

   select bookTitle, bookAuthor, publPublisher
   from tblBooks
     left join tblPublishers on bookPublId = publId
   where
     bookYear = 2003

Составными частями запроса являются:

  • "select" – список полей
  • "from" – список таблиц, список присоединяемых таблиц
  • "where" – выражение

Язык SQL является специализированным языком для доступа к источникам данных. Прикладные программы, которые используют SQL-запросы, чаще всего пишутся на универсальных языках программирования. Посылка запросов в этих программах чаще всего выглядит так:

    dataSource.execute("select bookTitle, bookAuthor from ...");

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

   // листинг 2

   // прототип:
   // DataSource.select(FieldList fl, TableList tl, Expression ex);

   // вызов метода:
   dataSource.select(
     // список полей
     new FieldList("bookTitle",
       new FieldList("bookAuthor",
         new FieldList("publPublisher", null))),
     // список таблиц
     new TableList("tblBooks",
       new JoinList("tblPublishers",
         new Expression("bookPublId", "publId",
           Expression.opEqual),
         null),
       null),
     // выражение where
     new Expression("bookYear", "2003", Expression.opEqual)
   );

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

Попробуем предложить какой-нибудь способ сохранить удобный синтаксис языка запросов, не занимаясь синтаксическим анализом на этапе выполнения. Для этого язык общего назначения, на котором реализуется базовая логика программы, должен позволять каким-то образом изменять собственный синтаксис, дополняя его требуемыми конструкциями. Обратим внимание: исходный SQL-запрос в приведенном примере (листинг 1) дословно соответствует конструкции языка общего назначения (листинг 2). Трансляция одного синтаксиса в другой представляет собой в данном случае тривиальное преобразование, которое можно задать с помощью контекстно-свободной грамматики (подчеркиванием выделены нетерминальные символы):

НетерминалИсходный синтаксисЦелевой синтаксис
Select select FieldList
from TableList
where Expression
dataSource.Select(
  FieldList, TableList, Expression
)
FieldList Identifier FieldListR new FieldList(
  "Identifier", FieldListR )
FieldListR , Identifier FieldListR new FieldList(
  "Identifier", FieldListR )
FieldListRεnull
TableList Identifier JoinList
  TableListR
new TableList("Identifier",
  JoinList, TableListR )
TableListR , Identifier JoinList
  TableListR
new TableList("Identifier",
  JoinList, TableListR )
TableListRεnull
JoinList left join Identifier
  on Expression
  JoinList
new JoinList("Identifier",
  Expression, JoinList )
JoinListεnull
Expression Identifier = Identifier new Expression(Identifier,
  Identifier, Expression.opEqual )
Expression Identifier = Constant new Expression(Identifier,
  Constant, Expression.opEqual )

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

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

Отметим, что обработка нетерминала Expression в приведенной таблице совершенно неудовлетворительна с практической точки зрения. Это вызвано тем, что компиляция выражений исчерпывающим образом рассмотрена в литературе [2, 3], а интерфейс и реализация необходимой функциональности объекта Expression представляется очевидной. Не слишком информативная реализация выражения, поддерживающего приоритеты операций, заняла бы недопустимо много места в нашей таблице, поэтому мы оставили только правила, нужные для трансляции SQL-фрагмента.

5. Возможные проблемы практического применения

Итак, дополнив исходный язык какими-то средствами описания грамматики (например, на основе нотации БНФ), мы можем получить возможность задания нового синтаксиса введенных или встроенных абстракций. Исходный язык при этом является источником семантических элементов, на базе которых описывается смысл новых синтаксических конструкций. В литературе описано достаточно много алгоритмов синтеза синтаксических анализаторов текста на основе формальной грамматики, поэтому принципиальную реализуемость подобного механизма следует считать доказанной. Однако из принципиальной реализуемости не обязательно следует практическая ценность аппарата развития на синтаксическом уровне.

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

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

Следует учесть, что все перечисленные проблемы являются плодом анализа, а не практического применения описанных механизмов. Можно не сомневаться, что реализация (сама по себе являющаяся сложной задачей) и применение на практике этих механизмов покажет ряд других серьезных проблем. В то же время очевидно, что значительная часть указанных проблем может (и должна) относиться к проектированию языкового средства, а не к разработке прикладных проектов на его основе. Несмотря на специфику абстракций синтаксического уровня, аппарат их описания можно проектировать на основе испытанных методов борьбы со сложностью, применяемых в традиционном семантическом аппарате развития существующих языков программирования [1, 3].

6. Выводы

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

Литература

  1. В. Ш. Кауфман. Языки программирования. Концепции и принципы. М.: Радио и Связь, 1993.
  2. А. Ахо, Р. Сети, Дж. Ульман. Компиляторы. Принципы, технологии, инструменты. М.: Вильямс, 2001.
  3. Р. У. Себеста. Основные концепции языков программирования. Пятое издание. М.: Вильямс, 2001.

Последнее обновление: 09 марта 2003


Copyright © 2000-2003 YALLIE, Inc. All Rights Reserved
webmaster: yallie@yandex.ru
Используются технологии uCoz