Можно ли использовать декларативный и императивный стили написания программ одновременно?
Внимание!!!
Данная статья содержит описание синтаксиса NewLnag предыдущей версии.
Актуальную версию синтаксиса языка можно посмотреть тут.
При разработке своего собственного языка программирования автор должен определиться, какими свойствами его язык должен обладать, ведь в конечном итоге именно это и будет определять назначение языка и особенности его использования. Причем некоторые свойства настолько сильно влияют на итоговую концепцию языка и его синтаксис, что впоследствии их изменение невозможно в принципе, либо новый синтаксис получается очень запутанным и непонятным. Наверно из-за этого многие свойства языков программирования рассматриваются и изучаются как взаимоисключающие.
До недавнего времени я считал, что императивная и декларативная парадигмы написания программ являются антагонистами и взаимоисключающими понятиями. Ведь выбор стиля написания кода, изначально определяется автором языка еще на стадии его проектирования и оказывает влияние на все последующие аспекты.
Но сейчас я думаю, что это не верно. Императивный и декларативный стили программирования не являются взаимоисключающими, а писать программы только в одной из указанных парадигм подталкивают правила синтаксиса, реализованные в языке!
Другими словами, использовать только императивную или только декларативную парадигмы разработчик вынужден не потому, что он не умеет или не хочет писать код по другому, а из-за того, что все более-менее мейнстримные языки программирования ориентированы на использование только одной парадигмы. И выбор только одной парадигмы написания кода, это ограничение, которое навязано программистам создателями языка. Ведь если они изначально ориентировались только на одну концепцию разработки кода, то и разрабатывали синтаксис языка в соответствии с этой парадигмой.
Данная статья — размышление о совместимости декларативной и императивной парадигм программирования и возможности их одновременного использования в рамках одного языка программирования одновременно.
Что такое “Декларативное программирование”?
Вначале мне захотелось разобраться, а существуют ли чисто декларативные языки программирования? Такие, которые могут полностью обходятся без императивных конструкций?
Но чуть не споткнулся уже в самом начале, т.к. даже само определение «декларативное программировании», в вики описывается следующим образом:
Декларати́вное программи́рование — парадигма программирования, в которой задаётся спецификация решения задачи, то есть описывается, что представляет собой проблема и ожидаемый результат. Противоположностью декларативного является императивное программирование, описывающее на том или ином уровне детализации, как решить задачу и представить результат.
Уже в изначальном определении закладывается противопоставление парадигм, а дальше идут рассуждения о «направлении» программирования. И хоть вики сама и не является авторитетным источником информации, но даже такая заготовка статьи смогла показать направление поиска ответов на первый поставленный вопрос.
И заставил задуматься один абзац в конце статьи следующего содержания:
«Чисто декларативные» компьютерные языки зачастую неполны по Тьюрингу — так как теоретически не всегда возможно порождение исполняемого кода по декларативному описанию. Это иногда приводит к спорам о корректности термина «декларативное программирование» (менее спорным является «декларативное описание решения» или, что то же самое, «декларативное описание задачи»).
Судя по наличию такой оговорки, ставится под сомнение наличие «чисто декларативных» языков. И как только пришлось задуматься над это мыслью, то сразу стало очевидным, что какой бы декларативный язык программирования не существовал, то в нем должна быть как минимум одна императивная конструкция, которая будет запускать поиск решения! Ведь любой синоним слова «выполнить» или «запустить» по определению будет императивом!
Например в языке Prolog, который обычно и приводят в качестве примера декларативного языка программирования, “замаскированным” императивным оператором является знак вопроса. Он кажется естественным и логичным, как же без него? Но по своей сути, это императивная конструкция!
Тоже самое касается и языка SQL. Он тоже как бы декларативный, но неожиданно все его команды являются императивными по сути. SELECT, CREATE, ALTER, DROP, INSERT, UPDATE, DELETE и т. д., а декларативными являются только описания условий их выполнения!
В результате, у меня так и не получилось найти чисто-декларативный язык программирования без императивных операторов (может быть, Вики права и такого языка программирования и вовсе не существует?).
А что отличает декларативные языки программирования от императивных?
Согласно определению, декларативными языками программирования их называют из-за возможности записать некие условия в декларативном стиле. Но кроме этого, в декларативных языках как правило присутствует и некий внутренний механизм поиска решений. Подобный “вычислитель” есть и в SQL и Prolog и во многих других декларативных языках.
Интересно, а является ли внутренняя система поиска решений обязательным признаком декларативного стиля программирования или это только особенность конкретного языка программирования, которая не зависит от декларативного стиля написания кода?
Чтобы проверить данные предположения, можно было попробовать написать классическую декларативную программу на императивном языке, у которого отсутствует внутренний механизм поиска решений.
А заодно, таким образом можно будет и изучить вопрос, получится ли написать программу в декларативном стиле используя обычный императивный язык программирования?
Для примера, решил попробовать повторить уже классическую программу в декларативном стиле на Прологе.
parent("Tom","Jake").
parent("Janna","Jake").
parent("Tom","Tim").
male("Tom").
male("Tim").
male("Jake").
female("Janna").
brother(X,Y):-parent(Z,X),parent(Z,Y),male(X),male(Y),X\=Y.
? brother
Получилась вот такая функционально - эквивалентная программа на С++, которая максимально приближена по стилю к декларативному прототипу:
enum sex_ {
male,
female
};
struct human;
typedef std::vector<human *> parent_;
struct human {
const char *name;
sex_ sex;
parent_ parent;
};
human Tom{"Tom", male,{}};
human Janna{"Janna", female,{}};
human Jake{"Jake", male,{&Tom, &Janna}};
human Tim{"Tim", male,{&Tom}};
std::vector<human *> humans{&Tom, &Janna, &Jake, &Tim};
auto brothers = [](human * a, human * b) {
auto intersec = [](parent_ &a, parent_ & b) {
for (auto elem_a : a) {
for (auto elem_b : b) {
if(elem_a && elem_b && elem_a == elem_b) {
return true;
}
}
}
return false;
};
return a && b && a != b && a->sex == male && b->sex == male && (intersec(a->parent, b->parent));
};
for (auto a : humans) {
for (auto b : humans) {
if(brothers(a, b)) {
std::cout << a->name << " and " << b->name << "\n";
}
}
}
Конечно, текст на С++ получается значительно многословней, чем вариант на Прологе, но по сути, это почти дословное повторение декларативного стиля написания кода. Тем более, не стоит забывать про рассуждения в начале статьи об изначальном выборе концепции при создании языка и “принуждении” программистов пользоваться только одной, изначально выбранной парадигмой разработки.
Таким образом, можно с высокой долей уверенности утверждать, что теоретически можно писать код одновременно в разных стилях.
Но что тогда мешает в рамках одного языка программирования писать с использованием императивного и декларативного стиля? Неужели только убежденность создателей языков в том, что императивный и декларативный стили программирования являются взаимоисключающими? И только из-за этого получаются языки программирования, синтаксис которых подходит для использования одной единственной парадигмы?
Но если это так, то что мешает попробовать разработать синтаксис языка программирования, в котором можно было бы использовать сразу императивный и декларативный стили программирования одновременно?
Что мешает совместить императивный и декларативный стили написания в рамках одной программы?
В любой компьютерной программе всегда существует разделение кода на описание данных и на языковые конструкции для их обработки, т.е. фактически это разделение на данные и на функции. Мне кажется, что основная сложность, которая не дает использовать разные стили программирования в рамках одного языка, это необходимость разделять описываемые сущности на «данные» и «управляющие конструкции». Ведь это свойство (необходимость разделения сущностей на “данные” и “функции”), является неотъемлемой частью любого языка программирования.
Это не удивительно, ведь на заре становления ИТ индустрии, создатели первых языков программирования ориентировались исключительно на императивный стиль, потому что назначением любого компилятора было - преобразовать исходный текст программы в машинные инструкции. И на примерах модульного, структурного и объекто-ориентированного подходах показана и доказана необходимость оформления исполняемого кода в выделенные процедуры с последующей их группировкой в модули и классы.
А декларативный стиль написания программ стал появляться только после создания высокоуровневых языков программирования. И основная цель создания данных языков сместилась на поиск решения конечного пользователя, а не на упрощение генерации бинарных файлов с машинными инструкциями. Если внимательно посмотреть на приведенные выше примеры кода, то можно заметить, что в них определения данных и операторы для их обработки идут вперемешку, (для примера на С++ это определение лямбд функции), что кардинально отличается от императивного подхода.
Так может быть, основная особенность декларативного стиля как раз и заключается в том, что в нем не разделяются «данные» и «действия над данными»? Или как вариант, можно не указывать выполняемые действия над данными вовсе (как в некоторых SQL конструкциях)?
Может быть как раз именно эта особенность (возможность последовательной записи программного кода в соответствии с собственными логическими рассуждениями, в которых могут перемежаться «данные» и «функции», как это происходит при человеческом мыслительном процессе), не дает в полной мере реализовать возможность совмещения императивных и декларативных стилей программирования?
И если это так, тогда можно попробовать разработать такой синтаксис, который будет поддерживать определение как данных, так и декларацию функций в рамках единого потока языковых конструкций!
Проверка гипотезы в новом языке программирования
Для поверки данного предположения, я решил добавить в свой новый язык программирования (синтаксис которого допускает определение функций в одном потоке с описанием обрабатываемых данных), недостающие алгоритмические конструкции, которые позволяли бы реализовать императивный стиль программирования, несмотря на изначальную ориентацию только на декларативную парадигму.
Правда с учетом изначальных ограничений синтаксиса нового языка (запрет на применение операторов в виде зарезервированных ключевых слов), в качестве оператора проверки условия была выбрана синтаксическая конструкция, соответствующая по смыслу термину «следует», т.е. тире и угловая скобка «->».
В результате, условный оператор получился практически математический, который легко объединяется в последовательности для реализации проверки множественных условий вида «else if». А для объединения сразу нескольких логического операторов между собой и для отделения их от последующего действия, операторы проверки условия можно заключать в круглые скобки.
В общем случае условный оператор в новом языке программирования имеет вид:
условие -> действие;
или
(условие) -> {действие};
или
(условие1 || условие2) -> {действие} -> {действие иначе};
Или расширенный вариант, для наглядности записанный с отступами:
(условие1) -> {действие1}
(условие2) -> {действие2}
(условие3) -> {действие3}
-> {действие_иначе};
Тогда и операторы циклов можно будет записать практически аналогично, только выбрав в качестве оператора, не оператор следования, а следование с возвратом (раз уж нужно указывать циклическое действие).
Цикл пока:
(условие) <-> {тело цикла};
счетный цикл для работы с итератором:
(счетчик_или_данные!)? <-> {тело цикла};
В этом случае, решение тестового примера декларативной программы на Прологе можно будет легко оформить, как в императивном, так и в декларативном стилях одновременно!
м := "муж.";
ж := "жен.";
human @= term(пол=, parent=[,]);
Tom @= human(пол=м);
Janna @= human(пол=ж);
Jake @= human(м, [&Tom, &Janna,]);
Tim @= human(пол=м, parent=[&Tom,]);
brother(h1, h2) &&= $h1!=$h2, $h1.пол==м, $h2.пол==м, $h1.parent & $h2.parent;
// Оператор «&» побитовое «И» для чисел или операция пересечения для множеств
// Запись алгоритма поиска решения в императивном стиле
(h1=human!, h2=human!)? <-> {
(brother(h1, h2)) -> {
@print($h1, $h2, «\n»);
}
};
// Краткая запись алгоритма поиска решения в императивном стиле
(h1=human!, h2=human!)? <->
brother($h1, $h2) -> @print($h1, $h2, «\n»);
// Запись поиска решения в декларативном стиле
brother(human!, human!)?;
Примерно это и требовалось показать!
З.Ы.
Я почти закончил эксперименты с синтаксисом языка. И теперь, после добавления в него недостающих алгоритмический конструкций (ветвления и циклов), планирую, что следующая статья о новом языке программирования будет посвящена описанию его полного синтаксиса и публикации исходников прототипа компилятора для возможности поиграться на реальных примерах.
З.З.Ы
В комментариях @gbg привел очень простой ответ на один из поднятых вопросов. Причем в более элегантном виде и без написания кода!
Ну и пять копеечек про ООП. Как ни странно:
- В нем таки есть декларативная часть, все эти pubic, private, virtual, etc
- Эта декларативная часть, внезапно, элегантно позволяет реализовывать декомпозицию и управление сложностью.
- При наличии перегрузок, можно поиграть в алгебраическое мышление, рассматривая взаимодействие двух объектов как бинарную операцию.
Ух ты, ООП - это обкатанная практикой технология, которая совмещает декларатив с императивом и вроде позволяет освоившим ее вкусно кушать и мягко спать!