Обсуждения императивного и декларативного программирования возникают часто. Разница между двумя парадигмами может сбивать с толку. Я часто вижу примеры, которые представляют несправедливое сравнение или просто неверны. Пришло время установить рекорд. Декларативная парадигма не предназначена для передачи меньшего количества информации; это абстракция (подробнее об этом позже). Речь идет о передаче информации другим способом.

Определения

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

декларативный

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

Примеры:

  • Я видел их на прошлой неделе.
  • Некоторые курсы начинаются в январе.
  • Дверь оставили открытой.
  • Это не единственные билеты.
  • Ты мог бы передать мне ложку.
  • Я счастлив.
  • х равно 5.

Императив

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

Примеры:

  • Иди к ним!
  • Не начинайте курс в январе.
  • Не оставляйте дверь открытой.
  • Получите последние билеты.
  • Передай мне ложку.
  • Будь счастлив.
  • Установите х на 5.

Тыквенный пирог

Прежде чем мы перейдем к сравнению кода, давайте рассмотрим пример на простом английском языке с использованием delicious pumpkin pie.

Ингредиенты

  • 3 яичных желтка
  • 1 большое яйцо
  • 1/2 чайной ложки соли
  • 2 чайные ложки молотой корицы
  • 1 чайная ложка молотого имбиря
  • 1/4 чайной ложки молотого мускатного ореха
  • 1/4 чайной ложки молотой гвоздики
  • 1 (15 унций) банка тыквенного пюре
  • 1 (12 унций) банка сгущенного молока
  • 1 замороженная корочка для пирога

Императивный рецепт

  1. Разогрейте духовку до 425 градусов F (220 градусов C).
  2. Смешайте тыквенное пюре, яичные желтки и яйцо в большой миске. Добавьте сгущенное молоко, корицу, имбирь, гвоздику, мускатный орех и соль; взбейте до полного смешивания.
  3. Поместите корку пирога в 9-дюймовую тарелку для пирога и обожмите края.
  4. Вылейте начинку в форму для пирога и слегка постучите по рабочей поверхности, чтобы вышли пузырьки воздуха.
  5. Выпекать в предварительно разогретой духовке 15 минут.
  6. Уменьшите температуру до 350 градусов по Фаренгейту (175 градусов по Цельсию) и выпекайте, пока не застынет в середине, еще 30-40 минут. Дайте полностью остыть перед подачей на стол.

Декларативный рецепт

  • Основа — тыквенное пюре, яичные желтки и яйцо, взбитые вместе в большой миске.
  • Начинка представляет собой сгущенное молоко, корицу, имбирь, гвоздику, мускатный орех и соль, взбитые в основу до полного смешивания.
  • Оболочка пирога представляет собой корку пирога, помещающуюся в 9-дюймовую тарелку для пирога с гофрированными краями.
  • Сырой пирог представляет собой оболочку для пирога с налитой начинкой, которую слегка постукивают, чтобы выпустить пузырьки воздуха.
  • Полуфабрикатный пирог - это сырой пирог, выпеченный в духовке, предварительно разогретой до 425 градусов по Фаренгейту в течение 15 минут.
  • Приготовленный пирог - это полуфабрикат, выпекаемый при температуре 350 градусов по Фаренгейту в течение 30-40 минут.
  • Готовый пирог можно подавать после того, как он полностью остынет.

Какая разница?

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

Декларативный рецепт (перестроенный)

- Приготовленный пирог можно подавать после того, как он полностью остынет.

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

- Основа – тыквенное пюре, яичные желтки и яйцо, взбитые вместе в большой миске.

- Оболочка пирога представляет собой корку пирога, помещающуюся в 9-дюймовую форму для пирога с гофрированными краями.

– Готовый пирог – это полуфабрикат, выпекаемый при температуре 350 градусов F в течение 30–40 минут.

– Полуфабрикатный пирог – это сырой пирог, выпеченный в духовке, предварительно разогретой до 425 градусов F, в течение 15 минут.

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

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

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

Сравнение программирования (JavaScript)

Разница между императивным и декларативным программированием определяется концепцией состояния.² ³ Императивное программирование предполагает использование явного состояния, то есть информации, которая запоминается с течением времени.⁴ Декларативное программирование описывается как не имеющее состояния. Мы можем использовать рекурсию в (функциональном) декларативном программировании, которое можно рассматривать как сохранение неявного состояния, но поскольку контекст меняется при каждом рекурсивном вызове функции, явное состояние, сохраняющееся на протяжении всей операции, отсутствует.

Теперь давайте сравним императивный и декларативный код. Я предпочитаю использовать JavaScript, потому что этот язык поддерживает как императивный, так и декларативный способы написания кода.

Получить четные числа, императив

Цель состоит в том, чтобы получить все четные числа из заданного списка чисел.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const even = x => x % 2 === 0;

let evensImperative = [];
for (const num of numbers) {
  if (even(num)) {
    evensImperative.push(num);
  }
}
console.log(evensImperative);

Явное состояние — evensImperative, которое со временем меняет свое значение, накапливая все четные числа.

Получить четные числа, декларативная версия 1

Та же цель, используя (функциональное) декларативное программирование.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const even = x => x % 2 === 0;

const getEvens = (xs, acc) => (
  (xs.length === 0) ? acc
    : even(xs[0]) ? getEvens(xs.slice(1), [...acc, xs[0]])
    : getEvens(xs.slice(1), acc)
);
const evensDeclarative = getEvens(numbers, []);
console.log(evensDeclarative);

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

Получить четные числа, декларативная версия 2

Использование reduce — это усовершенствование предыдущей версии, шаг вперед в абстракции. Reduce — это более конкретная, но все же довольно выразительная функция для преобразования массива во что-то новое.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const even = x => x % 2 === 0;

const evensDeclarative = numbers.reduce((acc, x) => (
  even(x) ? [...acc, x] : acc
), []);
console.log(evensDeclarative);

Получить четные числа, декларативная версия 3

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

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const even = x => x % 2 === 0;

const evensDeclarative = numbers.filter(even);
console.log(evensDeclarative);

Так какой из них лучше?

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

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

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

Еще одним следствием такого разделения проблем является возможность повторного использования частей. Отдельный оператор может быть отделен от решения в целом и повторно использован для решения другой проблемы. Например, декларативный тыквенный пирог говорит о начинке. Может быть, вместо этого мы хотим использовать черничную начинку. Если у нас есть аналогичный декларативный рецепт черничного пирога, мы можем просто поменять начинку черничного пирога, а остальное оставить без изменений. Теперь у нас есть рецепт пирога с черникой! (На самом деле это не сработает для данного рецепта тыквенного пирога, потому что он слишком отличается от приготовления пирога с черникой, но я надеюсь, что точка зрения остается в силе.)

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

Как насчет абстракции?

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

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

Декларативный рецепт (обрезанный)

- Оболочка пирога представляет собой корку пирога, помещающуюся в 9-дюймовую форму для пирога с гофрированными краями.

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

– Полуфабрикатный пирог – это сырой пирог, выпеченный в духовке, предварительно разогретой до 425 градусов F, в течение 15 минут.

– Готовый пирог – это полуфабрикат, выпекаемый при температуре 350 градусов F в течение 30–40 минут.

Связанный с абстракцией, декларативный рецепт также позволяет нам определить, что важно. С императивным рецептом трудно сказать, к чему все идет. Какой смысл смешивать ингредиенты в миске? Мы будем варить суп одновременно? В декларативном рецепте ясно, что взбивание ингредиентов образует основу, которая затем используется для приготовления начинки.

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

Исправление дезинформации

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

Книга: «Основы LINQ»

(Калверт, К., и Кулкарни, Д. (2009). Essential LINQ. Addison-Wesley Professional)

Императивное программирование требует, чтобы разработчики шаг за шагом определяли, как должен выполняться код. Чтобы указать направление в повелительной форме, вы говорите: «Идите на 1-ю улицу, поверните налево на главную, проедьте два квартала, поверните направо на Мэйпл и остановитесь у третьего дома слева». Декларативная версия может звучать примерно так: «Поехали к Сью домой». Один говорит, как что-то сделать; другой говорит, что нужно сделать.

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

Императивное и декларативное программирование, пост Тайлера МакГинниса

«Императивное программирование похоже на то, как вы что-то делаете, а декларативное программирование больше похоже на то, что вы делаете».

Я не считаю это определение полезным, потому что «то, что вы делаете», кажется запутанным или просто неправильным. Декларативное программирование — это то, что люди делают, но это не может быть тем, что оно означает. Может быть, это означает «то, что вы говорите компьютеру делать», но это звучит как команда, которая делает ее императивной. Я не знаю, как это интерпретировать. Я знаю, что это определение не следует воспринимать всерьез, но я думаю, что оно делает вещи еще менее ясными.

Императивный подход (КАК): «Я вижу, что столик, расположенный под вывеской «Ушел на рыбалку», пуст. Мы с мужем пройдем туда и сядем».

Декларативный подход (ЧТО): «Стол на двоих, пожалуйста».

Это только у меня или это наоборот? Первый — это пара утверждений (декларативных), а второй — команда (императивных).

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

Stack Overflow принял ответ на вопрос «Разница между декларативным и императивным в React.js?»

https://stackoverflow.com/a/33656983

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

Go to the kitchen Open fridge Remove chicken from fridge ... Bring food to the table

В декларативном мире вы бы просто описали, что хотите

I want dinner with chicken.

Более справедливым декларативным вариантом было бы: «Я хочу курицу из холодильника, который стоит на кухне, и я хочу съесть ее за столом».

Если ваш дворецкий не умеет готовить курицу, вы не можете действовать в декларативном стиле.

Это не имеет смысла. Как мы видели, мы можем легко преобразовать императивный рецепт в набор утверждений, которые можно сообщить дворецкому.

Сноски

¹ Кембриджский словарь

² Фаланд, Д., Любке, Д., Мендлинг, Дж., Рейерс, Х., Вебер, Б., Вайдлих, М., и Зугал, С. (2009). Декларативные и императивные языки моделирования процессов: проблема понятности. Конспекты лекций по обработке деловой информации, 353–366.

³ Рой, П.В., Хариди, С.: Концепции, методы и модели компьютерного программирования. MIT Press, Кембридж (2004 г.)

https://www.info.ucl.ac.be/~pvr/paradigms.html

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

Первоначально опубликовано на https://timjohns.ca.