Хабр это замечательное место, где можно смело делиться своими идеями (даже если они и выглядят безумно). Хабр видел много самодельных языков программирования, расскажу и я о своих экспериментах в этой области. Но мой рассказ будет отличаться от остальных. Во-первых, это будет не просто язык программирования, а гибридный язык, сочетающий в себе несколько парадигм программирования. Во-вторых, одна из парадигм будет довольно необычной — она будет предназначена для декларативного описания модели предметной области. А в-третьих, сочетание в одном языке декларативных средств моделирования и традиционных объектно-ориентированного или функционального подходов способно породить новый оригинальный стиль программирования — онтологически-ориентированное программирование. Я планирую раскрыть в первую очередь теоретические проблемы и вопросы, с которыми я столкнулся, и рассказать не только о результате, но и о процессе создания дизайна такого языка. Будет много обзоров технологий и научных подходов, а также философских рассуждений. Материала очень много, придется разбить его на целую серию статей. Если вас заинтересовала такая масштабная и сложная задача, приготовьтесь к долгому чтению и погружению в мир компьютерной логики и гибридных языков программирования.

Вкратце опишу основную задачу


Она заключается в том, чтобы создать такой язык программирования, который был бы удобен как для описания модели предметной области, так и для работы с ней. Чтобы описание модели было как можно более естественным, понятным для человека и близким к спецификациям программного обеспечения. Но при этом оно должно быть частью кода на полноценном языке программирования. Для этого модель будет иметь форму онтологии и состоять из конкретных фактов, абстрактных понятий и отношений между ними. Факты будут описывать непосредственные знания о предметной области, а понятия и логические отношения между ними — ее структуру.

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

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

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

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


Многим из нас приходилось заниматься поддержкой сложных проектов, созданных другими людьми. Хорошо, если в команде есть люди, которые ориентируются в коде проекта и могут объяснить, как он работает, есть документация, код чист и понятен. Но в реальности часто бывает и по-другому — авторы кода уволились задолго до того, как вы попали на этот проект, документации или нет совсем или она очень обрывочная и давным-давно устарела, а о бизнес-логике нужного компонента бизнес-аналитик или проект-менеджер может рассказать лишь в общих чертах. В этом случае чистота и понятность кода является критическим показателем.
Качество кода имеет много аспектов, один из них — правильный выбор языка программирования, который должен соответствовать решаемой задаче. Чем проще и естественней разработчик может реализовать свои замыслы в коде, тем быстрее он сможет решить задачу и меньше ошибок допустит. Сейчас у нас есть выбор из довольно большого числа парадигм программирования, каждая из которых имеет свою область применения. Например, функциональное программирование предпочтительней для приложений с акцентом на вычисления, так как оно дает больше возможностей для структурирования, комбинирования и повторного использования функций, реализующих операции над данными. Объектно-ориентированное программирование упрощает создание структур из данных и функций за счет инкапсуляции, наследования, полиморфизма. ООП подходит для приложений, ориентированных на данные. Логическое программирование удобно для задач, ориентированных на правила, требующих работы со сложными, рекурсивно-определенными типами данных, такими как деревья и графы, подходит для решения комбинаторных задач. Также свои сферы применения имеют реактивное, событийно-ориентированное, мультиагентное программирование.

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

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

Почему я считаю декларативный стиль программирование недооцененным


Попробую обосновать свою точку зрения.

Для этого рассмотрим, что из себя может представлять программное решение. Ее основные компоненты это: клиентская часть (десктоп, мобильные, web приложения); серверная часть (набор отдельных сервисов, микросервисов или монолитное приложение); системы управления данными (реляционные, документо-ориентированные, объектно-ориентированные, графовые базы данных, сервисы кэширования, поисковые индексы). Программному решению приходится взаимодействовать не только с людьми — пользователями. Частой задачей является интеграция с внешними сервисами, предоставляющими информацию через API. Так же источниками данных могут быть аудио и видеодокументы, тексты на естественном языке, содержимое web-страниц, журналы событий, медицинские данные, показания датчиков и т. п.

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

Задача связывания содержимого базы данных с объектами приложения тоже не так проста. Если структура таблиц в хранилище соответствует структуре понятий на уровне приложения, то можно воспользоваться ORM технологией. Но для более сложных случаев чем доступ к записям по первичному ключу и CRUD операций приходится выделить отдельный слой логики работы с базой данных. Обычно схема базы данных имеет максимально общую форму, так, что с ней могут работать разные сервисы. Каждый из которых отображает эту схему данных на свою объектную модель. Структура приложения становится еще более запутанной, если приложение работает не с одним хранилищем данных, а с несколькими, разного типа, загружает данные из сторонних источников, например, через API других сервисов. В этом случае необходимо создать унифицированную модель предметной области и отобразить на нее данные из разных источников.
В некоторых случаях модель предметной области может иметь сложную многоуровневую структуру. Например, при составлении аналитических отчетов одни показатели могут строиться на основе других, которые в свою очередь будут источником для построения третьих и т. д. Также входные данные могут иметь слабоструктурированную форму. Эти данные не имеют строгой схемы как, например, у реляционной модели данных, но все же содержат какую-либо разметку, позволяющую выделить из них полезную информацию. Примерами таких данных могут быть ресурсы семантической паутины, результаты парсинга WEB-страниц, документы, журналы событий, показания датчиков, результаты предварительной обработки неструктурированных данных, таких как тексты, видео и изображения и т.п. Схема данных этих источников будет строиться исключительно на уровне приложения. Там же будет и код, преобразующий исходные данных в объекты бизнес-логики.

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

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


Предположим, у нас есть 2 CSV файла. В первом файле:

В первой колонке хранится идентификатор клиента.
Во второй — дата.
В третьей — выставленная сумма счета,
В четвертой — сумма оплаты.

Во втором файле:
В первой колонке хранится идентификатор клиента.
Во второй — имя.
В третьей — адрес электронной почты.

Введем некоторые определения:
Счет включает в себя идентификатор клиента, дату, выставленную суммы, суммы оплаты и долга из ячеек одной строки файла 1.
Сумма долга это разница между выставленной суммой и суммой оплаты.
Клиент описывается с помощью идентификатора клиента, имени и адреса электронной почты из ячеек одной строки файла 2.
Неоплаченный счет — это счет с положительным долгом.
Счета связаны с клиентом по значению идентификатора клиента.
Должник — это клиент, у которого существует хотя бы один неоплаченный счет, дата которого старше текущей даты на 1 месяц.
Злостный неплательщик — это клиент, имеющий более 3х неоплаченных счетов.

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

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

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

В примере с должниками мы можем описать классы, описывающие структуру понятий «Счет» и «Клиент». Но логика создание объектов, связывание объектов счетов и клиентов между собой часто реализуется отдельно в фабричных классах или методах. Для понятий должников и неоплаченных счетов отдельные классы вообще не нужны, их объекты могут быть получены путем фильтрации клиентов и счетов в том месте, где они понадобятся. В результате часть понятий модели будет реализована в виде классов явно, часть — неявно, на уровне объектов. Часть отношений между понятиями — в методах соответствующих классов, часть — отдельно. Реализация модели будет размазана по классам и методам, перемешана с вспомогательной логикой ее хранения, поиска, обработки, преобразования форматов. Для того, чтобы найти эту модель в коде и понять ее, потребуются усилия.

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

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

Сначала мы объявляем факты с содержимым таблиц в формате: ID таблицы, строка, колонка, значение:

cell(“Table1”,1,1,”John”). 

Затем дадим имена каждой из колонок:

clientId(Row, Value) :- cell(“Table1”, Row, 1, Value).

После чего можно объединить все колонки в одно понятие:

bill(Row, ClientId, Date, AmountToPay, AmountPaid) :- clientId(Row, ClientId), date(Row, Date), amountToPay(Row, AmountToPay), amountPaid(Row, AmountPaid).
unpaidBill(Row, ClientId, Date, AmountToPay, AmountPaid) :- bill(Row, ClientId, Date, AmountToPay, AmountPaid),  AmountToPay >  AmountPaid.
debtor(ClientId, Name, Email) :- client(ClientId, Name, Email), unpaidBill(_, ClientId, _, _, _).

И так далее.

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

Как же нам совместить основной функциональный или объектно-ориентированный язык разработки с декларативной природой модели предметной области?


Наиболее известным подходом является предметно-ориентированное проектирование (Domain-Driven Design). Эта методология облегчает создание и реализацию сложных моделей предметной области. Она предписывает, чтобы все понятия модели были выражены в коде явным образом в слое бизнес логики. Понятия модели и реализующие их элементы программы должны быть как можно ближе к друг другу и соответствовать единому языку, понятному как программистам, так и экспертам по предметной области.

Богатая модель предметной области для примера с должниками будет дополнительно содержать классы для понятий «Неоплаченный счет» и «Должник», классы-агрегаты для объединения понятий счетов и клиентов, фабрики для создания объектов. Реализация и поддержка такой модели более трудоемкая, а код громоздкий — то, что раньше можно было сделать одной строчкой, в богатой модели требует нескольких классов. В итоге, на практике такой подход имеет смысл только при работе больших команд над сложными масштабными моделями.

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

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

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

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

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

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

На первый раз достаточно. В следующей публикации я хочу поговорить о некоторых современных технологиях, совмещающих императивный и декларативный стили — PL/SQL, Microsoft LINQ и GraphQL. Для тех, кто не хочет ждать выхода всех публикаций на Хабре, есть полный текст в научном стиле на английском языке, доступный по ссылке:
Hybrid Ontology-Oriented Programming for Semi-Structured Data Processing.