Комментарии 55
Добавлю к комментарию выше: оба ваших примера про яичницу — императивные, просто с разной степенью детализации.
2) Сковорода х1
3) Огонь х1
4) Option<Соль>
5) Функция готовки
6)…
7) Применить!
яичница(яйца) = яйца |> чистка |> жарка
Таким языком может быть
SQL: SELECT item * 2 FROM arr
, но даже в этом случае мы определяем императивную операцию с умножением. В чисто декларативном подходе, мы должны сказать «здесь удвоенный список». Каким образом — не рассматривается в этой парадигме.Разница между декларацией и императивом пролегает не в «что» или «как» (это следствие, просто описание), а в том что в императиве используется информация предыдущего шага. В декларативном подходе шагов вычислений нет.
Операции и отображения (функции) тоже можно описывать декларативно: вместо y = f(x) = 2 * x (пусть x: int) можно сказать, y — четное. Для любой g(x) функции можно можно сказать что её значения — «g'ные». Это позволяет до некоторой степени проворачивать фарш назад — вообще не определяя g, а описывая (декларативно) её свойства, получать ответы на вопросы (путем проверки, выполняется ли свойство для компонент агрегата). Приблизительно так и работает Пролог (несмотря на то что там есть "+")).
И кстати класс — это пример декларации — он описывает множество объектов со свойствами.
Декларативный подход, как таковой, существует. Это аксиоматика, денотационная семантика, лежащая в основе языка, даже императивного, наконец, просто способ декомпозиции и композиции задачи. Чаще всего, он используется в комплексе с императивными конструкциями, но иногда и сам по себе: HTML, CSS и SVG тому примеры.
Яйца: соленые, пожаренные.
Сковорода: умеет жарить, если горячая. Вмещается 1-4 яйца.
Огонь: умеет нагревать, если включен.
Мраморная плита: умеет жарить, если горячая. Вмещается 1-2 яйца.
Процессор без кулера. Умеет жарить. Вмещается 1 яйцо.
Мне надо: Яичница, из яиц, жареных, две штуки, с солью.
Яйца пожареные, соленые.
Яйца сырые: если пожарить, будут пожаренные.
Яйца пожаренные: если добавить соль, будут соленые.
Соль: если добавить к чему-то, это что то станет соленым.
Яблоко. Если пожарить — будет пожареное.
Сковорода: умеет жарить, если горячая. Вмещается 1-4 яйца.
Огонь: умеет нагревать, если включен.
Мраморная плита: умеет жарить, если горячая. Вмещается 1-2 яйца.
Процессор без кулера. Умеет жарить. Вмещается 1 яйцо.
Прикол в том, что все строки я могу поменять местами.
А ещё могу написать: мне надо соленый жареный процессор.
Или Соленые сырые яйца
Или пожареные несоленые яйца (сковороды нет).
Или пожареные яблоки
auto omelette =
Fried {
MixOf {
Two { Egg { } },
One { SpoonOf { Salt{ } } } },
OnPan { HeatedTo { OneHundred { Degrees { } } } },
For{ Ten { Minutes { } } }
};
Как-то давно на Хабре в комментариях разгорелась длинная дискуссия о декларативном программировании с подобными примерами. Никак не могу найти тот пост.
… мне, конечно, очень интересно, где же в этом примере ООП.
Как отличить, являются эти сущности объектами или просто древовидной структурой данных?
Иными словами, я могу с минимальными синтаксическими изменениями переписать это на функциональный язык, и получить все то же самое, разве нет?
Да, можете. В таком ООП многие вещи взяты из функционального программирования. К примеру неизменяемость, минимальный интерфейс и композиция over наследование.
В отличие от структуры данных объекты имеют чётко определённое поведение.
Но у вас в коде его нет.
В данном примере у омлета может быть метод omelette.taste()
Как только вы его вызовете, код разве не станет императивным?
auto breakfast =
Table {
With { omelette, tea, salad },
OnIt { }
};
а на более низком уже более императивный код.
Так вот, пойнт в том, что на этом "более высоком уровне" ООП (в его нормальном понимании) — нет.
Это у вас свободная монада получилась. Поздравляю!
Scenario: Some determinable business situation
Given some precondition
And some other precondition
When some action by the actor
And some other action
And yet another action
Then some testable outcome is achieved
And something else we can check happens too
горячая сковородка :- сковородка, поставленная на огонь 4 минуты назад.
разбитое яйцо :- содержимое яйца отделенное от скорлупы
…
Декларативный описывает «что?» — «готовая яичница», и если он чисто декларативный, то не содержит совершенно никакой информации как ее сделать. Пример HTML, XML.
Императивный говорит какие действия будут выполнены, а что из этого получится — не важно, это описание команд. Пример — Си.
А последний пример, это функциональный стиль.
Императивный подход — КАК добиться нужного результата. Например: button.setBackgroundColor(RED)
(есть эта строчка кода, которая выполняется до каких-то других и после каких-то других; эта строчка — это команда что-то сделать — "установить цвет фона")
Декларативный подход — ЧТО собой представляет нужный результат. Например: <button backgroundColor="red">...
(это не команда — мы не говорим "во-первых я хочу кнопку, а во-вторых — давайте ей установим красный цвет фона"; вместо этого мы описываем результат — кнопка с красным фоном)
Ваш пример для декларативного подхода "приготовь яичницу" — это на самом деле императивный подход, т.к. вы описываете не результат, а способ его достижения. Декларативно яичница выглядит вот так: "яичница".
Интересно — если в декларативном стиле утверждения не зависят от порядка следования и почти не зависят от друг друга — то получается, что они обладают свойством почти совершенной модульности: кусок декларативного текста можно вставить из программы "Домик в деревне" в программу "Катастрофа машины времени в Юрском периоде" и он прекрасно заработает, обжаривая яйца динозавров, вместо яиц их далёких пернатых потомков.
Модульность за бесплатно нигде не раздают, за нее надо еще побороться. Просто в разных стилях программирования эта борьба принимает разные формы.
За это мы, махровые декларативщики, и боремся! Практически идеальны в этом отношении конкатенативные языки, такие как Joy или J.
Получится "Шахбокс" или "офицерское многоборье" для программистов — в первом раунде решаешь с выводом математический алгоритм (в т. ч. декларативно) и демонстрируешь вывод в LaTexe.
Во втором — разрабатываешь модульно-объектную структуру программы.
В третьем — набираешь и правишь стиль и успешно компилируешь.
В четвертом — показываешь навыки оптимизации с измерением быстродействия.
Всё, все стадии — на время и на очки.
И тогда мнения — "программист-олимпиадник — это феееее!" — не будет.
Декларативно яичница выглядит вот так: «яичница»
Ну, я бы так:
яичница.жареная = true;
ну и дальше уже, по фантазии :)
public bool жареная { get { return _жареная;}; set { пожаритьЯичницу(value);}
Это как раз императивный код.
Мне кажется, не очень правильно писать статью с объяснениями, в которых автор сам не уверен и путается (судя по комментариям). Можно кого-то научить чему-то плохому.
В статье действительно приведено два примера имеративного подхода, лишь на разном уровне абстракции.
Имхо, имеративный подход описывает алгоритм через последовательность действий, а декларвтивный использует набор деклараций.
Имеративно:
div = docoment.getElementById("#someid");
for(var i=0; i<=10; i++)
div.appendChild(document.createElement("span"));
Декларвтивно (псевдокод):
<div id="someid">
<for var="i" start="0" end="10">
<span><span>
</for>
</div>
Хорошим примером декларативных подходов являются HTML, XML+XSLT. Различные файлы конфигураций (package.json etc) тоже в своем роде декларативных программы. Jasonette — декларативных фреймворк.
Отличным примером декларативного подхода в императивных языках являются аннотации (декораторы):
Имеративно:
class MyAnalyser extends Analyse {
…
}
…
Object.seal(MyAnalyser.constructor);
Object.seal(MyAnalyser.constructor.prototype);
Декларативно:
@Sealed
class MyAnalyser extends Analyser {
…
}
Пока единственный пример декларативного описания яичницы дал @Unrull. В нём хорошо разделены описание и интерпретация.
Декларативное описание состоит из неупорядоченной последовательности определений, позволяющей однозначно редуцировать выражение, определяющее конкретную яичницу к нормальной форме. Саму яичницу будет делать исполнитель, он получит императивный объектный код, в который транслируется нормальная форма, определяющая яичницу. В решении @Unrull дана нормальная форма в виде свободной монады. Её соответствующей F-алгеброй можно интерпретировать в последовательность действий, в том числе, побочных.
Гораздо, впрочем, лучше разницу между декларативным и императивным подходами показывает пример с описанием изображения, скажем, домика:
Декларативное:
house = (roof `above` (window `over` walls)) `at` (0,0)
roof = triangle 100 50 `color` "blue"
walls = square 100 `color` "brown"
window = w' `above` w'
where w = square 10
w' = w `beside` w
Эту программу пишет и отлаживает человек. Всю вычислительную работу выполняют конструкторы примитивов square
и triangle
, а также универсальные комбинаторы: above
, below
, over
и at
.
Императивное:
function DrawHouse () {
// roof
newPath()
moveTo(0,100)
lineTo(100,100)
lineTo(50,150)
lineTo(0,100)
setColor("blue")
stroke()
// walls
newPath()
moveTo(0,0)
lineTo(100,0)
lineTo(100,100)
lineTo(0,100)
lineTo(0,0)
setColor("brown")
stroke()
...
}
Не удержусь, приведу пример функционального декларативного описания на Haskell.
Начнём с описания омлета:
omelette :: Food f => Int -> f
omelette n = fried . scrambled . map broken $ eggs
where
eggs = take n $ repeat Egg
broken Egg = Scrambled Yolk <> Scrambled EggWhite
scrambled = getScrambled . fold
В этом описании используются стандартные функции для создания и обработки коллекций: repeat
, take
и foldMap
, универсальная функция: fried
, полугруппа Scrambled
ну, и, собственно, яйца.
Пусть для яйца и его частей будут определены атомарные типы:
data Yolk = Yolk
data EggWhite = EggWhite
data EggShell = EggShell
data Egg = Egg
Взбалтывать и жарить можно не только яйца. Для взболтанных продуктов определим тип-обёртку Scrambled
. Взболтанные продукты образуют полугруппу: смесь двух взболтанных продуктов тоже является взболтанным продуктом, причём их смешивание ассоциативно.
Наконец, опишем универсальный процесс жарки:
fried :: Food f => f -> f
fried x = heated 180 x `for` Minutes 8
Здесь используются универсальные функции из гипотетической библиотеки Reactive.Bananas.Cooking
с такими сигнатурами:
heated :: Temperature -> a -> Process a
for :: Process a -> Time -> a
Для абстракции времени тут используется реактивное программирование.
return SequenceFrom { array, WithEachValue { Two { Times { } }, TheOriginal { } } };
auto s1 =
IntSequenceFrom(
arr,
WithEachValueBe(
Doubled()));
#include "stdafx.h"
#include <vector>
#include <iostream>
using namespace std;
int main()
{
auto IntSequenceFrom = [&](auto seq, auto sel) {
return [=]() {
vector<int> res(seq.size());
for (size_t i = 0; i < seq.size(); ++i) sel(res, seq, i);
return res;
};
};
auto WithEachValueBe = [&](auto f) {
return [=](auto& res, auto seq, size_t i) { res[i] = f(seq[i]); };
};
auto WithEvenValuesBe = [&](auto f) {
return [=](auto& res, auto seq, size_t i) { if (i % 2 == 0) res[i] = f(seq[i]); };
};
auto WithOddValuesBe = [&](auto f) {
return [=](auto& res, auto seq, size_t i) { if (i % 2 != 0) res[i] = f(seq[i]); };
};
auto Both = [&](auto f1, auto f2) {
return [=](auto& res, auto seq, size_t i) { f1(res, seq, i) , f2(res, seq, i); };
};
auto TwoTimes = [&](auto f) {
return [=](auto val) { return 2 * f(val); };
};
auto TheProductOf = [&](auto f1, auto f2) {
return [=](auto val) { return f1(val) * f2(val); };
};
auto TheFirstElement = [&]() {
return [=](auto val) { return val.first; };
};
auto TheSecondElement = [&]() {
return [=](auto val) { return val.second; };
};
auto Doubled = [&]() {
return [=](auto val) { return val * 2; };
};
vector<int> arr{1,2,3};
auto s1 =
IntSequenceFrom(
arr,
WithEachValueBe(
Doubled()));
for (auto v : s1()) cout << v << " ";
cout << endl;
vector<pair<int, int>> pairs = {{2,11}, {3,22}, {4,33}};
auto s2 =
IntSequenceFrom(
pairs,
WithEachValueBe(
TheProductOf(
TheFirstElement(),
TheSecondElement())));
for (auto v : s2()) cout << v << " ";
cout << endl;
auto s3 =
IntSequenceFrom(
pairs,
Both(
WithEvenValuesBe(
TwoTimes(
TheProductOf(
TheSecondElement(),
TheFirstElement()))),
WithOddValuesBe(
TwoTimes(
TheFirstElement()))));
for (auto v : s3()) cout << v << " ";
cout << endl;
return 0;
}
Видите ли какая штука. Инструкция "как приготовить яичницу" сама по себе императивна.
Подкину уголька..
Императивный функциональный код:
яичница = ()=> последовательность(
()=> яйцо ,
яйцо => разбей( яйцо ) ,
разбитое_яйцо => убери_скорлупу( разбитое_яйцо ),
яйцо_без_скорлупы => пожарь( сковорода )( яйцо_без_скорлупы ),
жаренное_яйцо => добавь_приправы( жаренное_яйцо )
)
Или то же самое:
яичница = ()=> последовательность( ()=> яйцо , разбей, убери_скорлупу, пожарь( сковорода ), добавь_приправы )
Или то же самое:
яичница = ()=> добавь_приправы( пожарь( сковорода )( убери_скорлупу( разбей( яйцо ) ) ) )
Декларативный объектный код:
класс Процесс_готовки_яичницы {
получить сковорода() { вернуть новое Яйцо }
получить яйцо() { вернуть новое Яйцо }
получить разбитое_яйцо() { вернуть это.яйцо.разбить() }
получить яйцо_без_скорлупы() { вернуть это.разбитое_яйцо.убрать_скорлупу() }
получить жаренное_яйцо() { вернуть это.сковорода.пожарь( это.яйцо_без_скорлупы ) }
получить яичница() { вернуть это.жаренное_яйцо.добавь_приправы() }
}
Или то же самое:
класс Процесс_готовки_яичницы {
получить яичница() { вернуть это.жаренное_яйцо.добавь_приправы() }
получить жаренное_яйцо() { вернуть это.сковорода.пожарь( это.яйцо_без_скорлупы ) }
получить сковорода() { вернуть новое Яйцо }
получить яйцо_без_скорлупы() { вернуть это.разбитое_яйцо.убрать_скорлупу() }
получить разбитое_яйцо() { вернуть это.яйцо.разбить() }
получить яйцо() { вернуть новое Яйцо }
}
Императивное программирование позволяет описывать инструкции с использованием «низкоуровневых» (в рамках ЯП) выражений.
При этом, если подумать, любое выражение — в действтиельности задекларированная ЯП инструкция. Я бы это, своевольно, назвал одним из доказательств того что программа — это конечный автомат.
Декларативное программирование — это набор верхнеуровневых инструкций, детали реализации которых скрыты за исполнителем этих инструкций.
Императивное программирование позволяет описывать инструкции с использованием «низкоуровневых» (в рамках ЯП) выражений.
Это, всё же, вопрос не парадигмы, а уровня абстракции. На ФЯП можно написать на чистых лямбдах или SKI комбинаторах весьма низкоуровневый код, а императивный FORTH позволяет выйти на любой уровень абстракции, расширяя словарь.
При этом, если подумать, любое выражение — в действтиельности задекларированная ЯП инструкция. Я бы это, своевольно, назвал одним из доказательств того что программа — это конечный автомат.
Программа, это инструкция для автомата, а не сам автомат. Но вы недалеки от истины с точки зрения вычислителя: правда не конечный автомат, а автомат с магазинами, или счётчиками, эквивалентен машине Тьюринга, а ЯП для написания программ для него — полон по Тюрингу. А именно с полными ЯП мы, обычно, и работаем.
На правах попытки разобраться в сказанном (статье + комментариях к ней). Тоже на днях встретился с вопросом понимания и соотнесения императивного и декларативного программирований.
Есть в русском языке понятие всё — которое как универсум в логике, включает в себя вообще всё. Это понятие можно сделать корнем мысленного дерева (грубо говоря на что взгляд не упадёт, какая мысль в голову не придёт — перечисленное будет частью понятия всё).
Основа процесса мышления — это выявление разнокачественностей, т.е. способность всё разделить на части (простейшая из которых пара из это и все_остальное_не_это).
Сам процесс мышления — это соотнесение между собой выявленных разнокачественностей с выявлением их взаимосвязей, упорядоченности и придание значений/значимости разнокачественностям, связям.
В контексте мышления встаёт вопрос о выявлении первичных различий у частей всего (по которым их можно было бы соотносить). Другими словами, если понятие всё делить на части, то из суммы каких понятий оно будет состоять на первом уровне деления, на первом уровне детализации?
Один из возможных ответов: материя-информация-мера.
С такой точки зрения на что не кинь взгляд, в нём можно выделить образ (цвет например), меру (соотношения частей), носитель (из чего состоит — будь то бумага с текстом, либо электромагнитные поля, хранящие и передающие текст).
Декларация либо утверждение, описание — это отражение информации. И тогда предельной формой утверждения будет картина/фотография. Делая описание чего-либо текстом, нам приходиться выбирать набор понятий которые использовать, широту и детальность описания. Следуя примеру:
- Яичница состоит из яиц, соли, масла, при этом хорошая яичница — тёплая.
- Яйца в яичнице жаренные — зачастую по нраву потребителя.
const requiredScrambledEggs = { egg: 2, salt: 5, oil: 0.5, temperature: 35, roast: 'low' };
и да, должен быть кто-то/что-то способное её предоставить по требованию.
Выходит, что результатом декларативного программирование есть множество описаний чего-то (HTML+CSS — как должна выглядеть страница и что содержать, SVG — какие геометрические фигуры где и с какими параметрами отражать).
Описывать можно и меру — соотношения, алгоритмы, в частности задавать последовательность действий некоего исполнителя.
Императив либо повеление, команда, приказ. С такой точки зрения это тоже информация (информация направляемая исполнителю). Однако последовательность команд может отражать алгоритм, меру. Отсюда результатом императивного программирования есть набор последовательностей команд исполнителю (следуя которым исполнитель должен получить то, что мы от него хотели — яичницу).
При этом в декларативном программировании мы не задумываемся о том как будет получен результат (т.е. какие действия и на каком исполнители будут выполнены) — главное у нас есть набор описательных примитивов (div, input, span либо rect, path, p) — и нам нужно разбить выражаемую/отражаемую информацию на части и перенести в код (если в рамках примитивов это вообще выразимо), указав какие части и в каких взаимосвязях есть.
Императивное программирования предполагает отражения 2-х вещей: последовательности команд и параметров этих команд (что в сущности есть информация, часть из которой может быть введена декларативно, другая получена на ввод, третья — вычислена/преобразована из имеющейся). Тут мы так-же ограничены множеством возможных для выполнения команд исполнителя.
В js можно предположить, что у нас есть отражение робота повара в систему в виде встроенного объекта (cook), с методами отражающими известные ему команды. Для этого робота мы задаём последовательность действий типа найди сковородку, включи плиту, поставь сковородку на плиту, найди и налей масла… и т.д.
function getReadyScrambledEggs(meal){
const fryingPan = cook.findFryingPan();
cook.setOnFire(fryingPan);
while(fryingPan.getTemperature() < 50)
cook.wait();
// тут 50 - константа, т.е. некая фиксированная информация, задекларированная в условии цикла
const eggs = cook.findEggs(meal.egg); // из requiredScrambledEggs
...
}
Выходит:
- декларативное программирование в общем случае возможно без императивного (исполнителем может быть и человек, способности которого — его данность);
- императивное без деклараций (пускай и в скрытом виде) сложно себе представить (только если все данные неким образом вводятся извне, а код только их преобразование описывает);
- говоря о декларативном программировании ожидают отражение в код информации (которая потом будет построена либо по которой будет что-то построено — описание цели);
- говоря о императивном программировании ожидают создание управляющей программы которая будет давать набор команд исполнителю чего делать что-бы получить требуемый результат (в первом приближении сам набор команд записанный в должной последовательности — поскольку в общем случае возможны разные стартовые ситуации, есть условия/ветвления, то я уточнил — создание управляющей программы, которая выдаст должную для данной ситуации последовательность команд).
У вас оба примера это императивный стиль.
И "поставь сковородку на огонь" и "приготовь яичницу" это императивные команды только на разном уровне абстракции.
Да, "накормить вкусно" это тоже императивная команда более высокого уровня абстракции.
В декларативном виде все тоже самое будет выглядеть так:
Поставь сковородку на огонь - сковородка диаметром 20 см стоит на огне мощностью 2кВт
Возьми два яйца (куриных) - 2 куриных яйца лежат в левой руке
Нанести удар ножом по каждому - 2 куриных яйца разбиты (не имеет значения ножом или взглядом) в руке
Вылей содержимое на сковородку - 2 разбитых куриных яйца лежат в сковородке
Приготовь яичницу - яичница из 2-х яиц находится в тарелке
Императив это команда в виде повелительного глагола. Любая команда это императив.
Декларация это констатация факта.
Императив: Все на стачку!
Декларатив: Все на стачке!
Императив: Сюда иди!
Декларатив: Тут стоишь.
Чем отличается императивное программирование от декларативного