Предыстория


Не так давно я была в активном поиске работы и просматривала кучу сайтов с вакансиями/проектами. На некоторых из них «проходные» вопросы можно посмотреть еще до подачи отклика.

Большинство было вполне обычными, типа «сколько лет вы пользуетесь фреймворком Х?», но один мне показался интересным (я даже туда откликнулась, но #меняневзяли).

Это собственно и был вопрос из заголовка. И я решила разобраться. А лучший способ понять — это, как известно, объяснить. Так что добро пожаловать под кат.

В чем же отличия?


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

Императивный стиль


Это такой стиль программирования, при котором вы описываете, как добиться желаемого результата. Например я пишу:

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


Это что ни на есть декларативный стиль, но при этом с примесью императивного.

Декларативный стиль


Такой стиль, в котором вы описываете, какой именно результат вам нужен.

Тут я просто пишу:

— приготовь яичницу

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

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

Пример на JS


Мой любимый пример такой: дан массив чисел, надо написать функцию, которая вернет массив чисел, где каждое число из исходного массива удваивается. Т.е. [1, 2, 3] -> [2, 3, 6]

Императивный стиль:

function double (arr) {
  let results = [];
  for (let i = 0; i < arr.length; i++){
    results.push(arr[i] * 2);
  }
  return results;
}

Декларативный стиль:

function double (arr) {
  return arr.map((item) => item * 2);
}

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

Наверное, правильно называть один стиль «более декларативным», а другой — «более императивным».
Поделиться с друзьями
-->

Комментарии (48)


  1. Elsedar
    23.03.2017 14:59
    +6

    Вы ошиблись с точностью до наоборот.


    1. sheepwalker
      23.03.2017 15:06

      Спасибо! Это правда, уже поправила


  1. lair
    23.03.2017 15:04
    +11

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


    1. sheepwalker
      23.03.2017 15:07

      а можно в таком случае декларативный пример?


      1. shai_hulud
        23.03.2017 15:13

        1) Яйца х2
        2) Сковорода х1
        3) Огонь х1
        4) Option<Соль>
        5) Функция готовки
        6)…
        7) Применить!


        1. echi
          23.03.2017 15:34
          +1

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


      1. lair
        23.03.2017 15:17
        +1

        яичница(яйца) = яйца |> чистка |> жарка


      1. echi
        23.03.2017 15:19

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

        Таким языком может быть

        SQL: SELECT item * 2 FROM arr
        
        , но даже в этом случае мы определяем императивную операцию с умножением. В чисто декларативном подходе, мы должны сказать «здесь удвоенный список». Каким образом — не рассматривается в этой парадигме.


        1. S_A
          23.03.2017 15:41
          +1

          Пролог вроде изначально был полностью декларативный, нет?

          Разница между декларацией и императивом пролегает не в «что» или «как» (это следствие, просто описание), а в том что в императиве используется информация предыдущего шага. В декларативном подходе шагов вычислений нет.

          Операции и отображения (функции) тоже можно описывать декларативно: вместо y = f(x) = 2 * x (пусть x: int) можно сказать, y — четное. Для любой g(x) функции можно можно сказать что её значения — «g'ные». Это позволяет до некоторой степени проворачивать фарш назад — вообще не определяя g, а описывая (декларативно) её свойства, получать ответы на вопросы (путем проверки, выполняется ли свойство для компонент агрегата). Приблизительно так и работает Пролог (несмотря на то что там есть "+")).

          И кстати класс — это пример декларации — он описывает множество объектов со свойствами.


        1. samsergey
          24.03.2017 13:55

          Декларативный подход, как таковой, существует. Это аксиоматика, денотационная семантика, лежащая в основе языка, даже императивного, наконец, просто способ декомпозиции и композиции задачи. Чаще всего, он используется в комплексе с императивными конструкциями, но иногда и сам по себе: HTML, CSS и SVG тому примеры.


      1. ainu
        23.03.2017 16:16
        +5

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

        Сковорода: умеет жарить, если горячая. Вмещается 1-4 яйца.
        Огонь: умеет нагревать, если включен.
        Мраморная плита: умеет жарить, если горячая. Вмещается 1-2 яйца.
        Процессор без кулера. Умеет жарить. Вмещается 1 яйцо.


        1. ainu
          23.03.2017 16:21

          Попробую в виде программы:

          Мне надо: Яичница, из яиц, жареных, две штуки, с солью.

          Яйца пожареные, соленые.
          Яйца сырые: если пожарить, будут пожаренные.
          Яйца пожаренные: если добавить соль, будут соленые.
          Соль: если добавить к чему-то, это что то станет соленым.
          Яблоко. Если пожарить — будет пожареное.
          Сковорода: умеет жарить, если горячая. Вмещается 1-4 яйца.
          Огонь: умеет нагревать, если включен.
          Мраморная плита: умеет жарить, если горячая. Вмещается 1-2 яйца.
          Процессор без кулера. Умеет жарить. Вмещается 1 яйцо.

          Прикол в том, что все строки я могу поменять местами.
          А ещё могу написать: мне надо соленый жареный процессор.
          Или Соленые сырые яйца
          Или пожареные несоленые яйца (сковороды нет).
          Или пожареные яблоки


          1. samsergey
            24.03.2017 13:58

            Добавьте в ваше описание типы, а к типам свойства, или лучше, аксиоматическую семантику, и абсурдные комбинации отпадут.


        1. Unrul
          24.03.2017 12:33
          +1

          Ага, или в ООП стиле на C++:

          auto omelette = 
          Fried { 
            MixOf {
              Two { Egg { } },
              One { SpoonOf { Salt{ } } } },
            OnPan { HeatedTo { OneHundred { Degrees { } } } },
            For{ Ten { Minutes { } } }
          };
          

          Как-то давно на Хабре в комментариях разгорелась длинная дискуссия о декларативном программировании с подобными примерами. Никак не могу найти тот пост.


          1. lair
            24.03.2017 12:35

            … мне, конечно, очень интересно, где же в этом примере ООП.


            1. Unrul
              24.03.2017 12:39
              +1

              Да тут все сущности являются объектами. Есть мнение, что хорошее ООП должно быть преимущественно декларативно.


              1. lair
                24.03.2017 12:46

                Как отличить, являются эти сущности объектами или просто древовидной структурой данных?


                Иными словами, я могу с минимальными синтаксическими изменениями переписать это на функциональный язык, и получить все то же самое, разве нет?


                1. Unrul
                  24.03.2017 12:56

                  В отличие от структуры данных объекты имеют чётко определённое поведение. В данном примере у омлета может быть метод omelette.taste(). Просто они организованы в дерево с помощью композиции.

                  Да, можете. В таком ООП многие вещи взяты из функционального программирования. К примеру неизменяемость, минимальный интерфейс и композиция over наследование.


                  1. lair
                    24.03.2017 13:00

                    В отличие от структуры данных объекты имеют чётко определённое поведение.

                    Но у вас в коде его нет.


                    В данном примере у омлета может быть метод omelette.taste()

                    Как только вы его вызовете, код разве не станет императивным?


                    1. Unrul
                      24.03.2017 13:06
                      +1

                      Но программа же должна что-то делать? Императивная работа на низком уровне скрывается за декларативной конфигурацией на высоком уровне.


                      1. lair
                        24.03.2017 13:13

                        Но программа же должна что-то делать?

                        Предположительно. Но трюк в том, что (императивная) программа, работающая с декларативным участком, и сам этот участок — это, очевидно, не одно и то же.


                    1. Unrul
                      24.03.2017 13:12

                      На более высоком уровне может быть

                      auto breakfast = 
                      Table {
                        With { omelette, tea, salad },
                        OnIt { }
                      };
                      

                      а на более низком уже более императивный код.


                      1. lair
                        24.03.2017 13:14

                        Так вот, пойнт в том, что на этом "более высоком уровне" ООП (в его нормальном понимании) — нет.


                        1. Unrul
                          24.03.2017 13:34

                          Это зависит от того, что понимать под ООП :) Чёткого определения не может дать даже Алан Кей. В последнее время появляется мнение, что правильное ООП ближе к функциональному и декларативному стилю, чем к ООП в традиционном его понимании.


                          1. lair
                            24.03.2017 13:36

                            Вопрос, однако, состоит в том, что же такое "правильное ООП" и где вы берете его определение.


                            (с Кеевским определением все понятно, под него этот код не попадает)


                            1. Unrul
                              24.03.2017 13:50

          1. samsergey
            24.03.2017 13:15
            +1

            Это у вас свободная монада получилась. Поздравляю!


      1. potan
        24.03.2017 20:52
        +2

        приготовленная яичница из n яиц :- разбитые на горячую сковородку яйца в количестве n штук и выдержанные в таком состоянии до отвердения белка.
        горячая сковородка :- сковородка, поставленная на огонь 4 минуты назад.
        разбитое яйцо :- содержимое яйца отделенное от скорлупы



  1. echi
    23.03.2017 15:06
    +2

    Вы действительно не перепутали заголовки?

    Декларативный описывает «что?» — «готовая яичница», и если он чисто декларативный, то не содержит совершенно никакой информации как ее сделать. Пример HTML, XML.

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

    А последний пример, это функциональный стиль.


  1. js605451
    23.03.2017 16:01
    +5

    Императивный подход — КАК добиться нужного результата. Например: button.setBackgroundColor(RED) (есть эта строчка кода, которая выполняется до каких-то других и после каких-то других; эта строчка — это команда что-то сделать — "установить цвет фона")


    Декларативный подход — ЧТО собой представляет нужный результат. Например: <button backgroundColor="red">... (это не команда — мы не говорим "во-первых я хочу кнопку, а во-вторых — давайте ей установим красный цвет фона"; вместо этого мы описываем результат — кнопка с красным фоном)


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


    1. Vjatcheslav3345
      23.03.2017 20:20
      +1

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


      1. evkochurov
        24.03.2017 08:28

        Звучит-то красиво, но попробуйте-ка перенести кусок HTML (он же декларативный) из одного веб-документа в другой.

        Модульность за бесплатно нигде не раздают, за нее надо еще побороться. Просто в разных стилях программирования эта борьба принимает разные формы.


        1. Vjatcheslav3345
          24.03.2017 11:38
          -2

          Считаете — модульность и объектность при программировании пора включать в олимпиадное программирование?


      1. samsergey
        24.03.2017 14:02

        За это мы, махровые декларативщики, и боремся! Практически идеальны в этом отношении конкатенативные языки, такие как Joy или J.


        1. Vjatcheslav3345
          24.03.2017 15:14

          Получится "Шахбокс" или "офицерское многоборье" для программистов — в первом раунде решаешь с выводом математический алгоритм (в т. ч. декларативно) и демонстрируешь вывод в LaTexe.
          Во втором — разрабатываешь модульно-объектную структуру программы.
          В третьем — набираешь и правишь стиль и успешно компилируешь.
          В четвертом — показываешь навыки оптимизации с измерением быстродействия.
          Всё, все стадии — на время и на очки.
          И тогда мнения — "программист-олимпиадник — это феееее!" — не будет.


    1. Maxmyd
      23.03.2017 21:33

      Декларативно яичница выглядит вот так: «яичница»

      Ну, я бы так:
      яичница.жареная = true;

      ну и дальше уже, по фантазии :)
      public bool жареная { get { return _жареная;}; set { пожаритьЯичницу(value);}


      1. lair
        24.03.2017 09:36

        Это как раз императивный код.


        1. Maxmyd
          24.03.2017 21:24

          Любой декларативный подход лежит поверх чего-то. Чтобы работал декларативный подход, нужна реализация ниже уровнем.


          1. lair
            24.03.2017 21:32

            Во-первых, нет: "ниже уровнем" HTML ничего нет — HTML просто чем-то интерпретируется.


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


  1. andres_kovalev
    24.03.2017 08:28

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


    Имеративно:


    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 {

    }


    1. sheepwalker
      24.03.2017 08:30

      соглашусь, что может и не очень правильно с точки зрения кого-то научить (тк сама запуталась), но после пары-тройки комментариев мне кажется, что получилось все же разобраться :)


  1. samsergey
    24.03.2017 13:31
    +1

    Пока единственный пример декларативного описания яичницы дал @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()
      ...
    }


  1. samsergey
    24.03.2017 13:43
    +2

    Не удержусь, приведу пример функционального декларативного описания на 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

    Для абстракции времени тут используется реактивное программирование.


  1. Unrul
    24.03.2017 15:11

    Насчёт второго примера в статье. Если уж говорить совсем декларативно, то мы хотим получить что-то вроде

    return SequenceFrom { array, WithEachValue { Two { Times { } }, TheOriginal { } } };
    


    1. Unrul
      24.03.2017 16:06

          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;
      }
      


  1. michael_vostrikov
    24.03.2017 17:09
    +2

    Видите ли какая штука. Инструкция "как приготовить яичницу" сама по себе императивна.


    1. lair
      24.03.2017 17:15
      +1

      … в отличие от описания "яичница — это..."


  1. vintage
    25.03.2017 13:03
    -1

    Подкину уголька..


    Императивный функциональный код:


    яичница = ()=> последовательность(
        ()=> яйцо ,
        яйцо => разбей( яйцо ) ,
        разбитое_яйцо => убери_скорлупу( разбитое_яйцо ),
        яйцо_без_скорлупы => пожарь( сковорода )( яйцо_без_скорлупы ),
        жаренное_яйцо => добавь_приправы( жаренное_яйцо )
    )

    Или то же самое:


    яичница = ()=> последовательность( ()=> яйцо , разбей, убери_скорлупу, пожарь( сковорода ), добавь_приправы )

    Или то же самое:


    яичница = ()=> добавь_приправы( пожарь( сковорода )( убери_скорлупу( разбей( яйцо ) ) ) )

    Декларативный объектный код:


    класс Процесс_готовки_яичницы {
        получить сковорода() { вернуть новое Яйцо }
        получить яйцо() { вернуть новое Яйцо }
        получить разбитое_яйцо() { вернуть это.яйцо.разбить() }
        получить яйцо_без_скорлупы() { вернуть это.разбитое_яйцо.убрать_скорлупу() }
        получить жаренное_яйцо() { вернуть это.сковорода.пожарь( это.яйцо_без_скорлупы ) }
        получить яичница() { вернуть это.жаренное_яйцо.добавь_приправы() }
    }

    Или то же самое:


    класс Процесс_готовки_яичницы {
        получить яичница() { вернуть это.жаренное_яйцо.добавь_приправы() }
        получить жаренное_яйцо() { вернуть это.сковорода.пожарь( это.яйцо_без_скорлупы ) }
        получить сковорода() { вернуть новое Яйцо }
        получить яйцо_без_скорлупы() { вернуть это.разбитое_яйцо.убрать_скорлупу() }
        получить разбитое_яйцо() { вернуть это.яйцо.разбить() }
        получить яйцо() { вернуть новое Яйцо }
    }