Многие люди представляют функциональное программирование как нечто очень сложное и «наукоемкое», а представителей ФП-сообщества – эстетствующими философами, живущими в башне из слоновой кости.

До недавнего времени такой взгляд на вещи действительно был недалек от истины: говорим ФП, подразумеваем Хаскель и теорию категорий. В последнее время ситуация изменилась и функциональная парадигма набирает обороты в web-разработке, не без помощи F#, Scala и React. Попробуем взглянуть на «паттерны» функционального программирования, полезные для решения повседневных задач с точки зрения ООП – парадигмы.

ООП широко распространено в разработке прикладного ПО не одно десятилетие. Все мы знакомы с SOLID и GOF. Что будет их функциональным эквивалентом?.. Функции! Функциональное программирование просто «другое» и предлагает другие решения.



Основные принципы функционального проектирования (дизайна)



Функции как объекты первого класса


В отличие от «классического» ООП (первые версии C++, C#, Java) функции в ФП представляют собой самостоятельные объекты и не должны принадлежать какому-либо классу. Удобно представлять функцию как волшебный железнодорожный тоннель: подаете на вход яблоки, а на выходе получаете бананы (apple -> banana).

Синтаксис F# подчеркивает, что функции и значения равны в правах:

let z = 1
let add = x + y // int -> int ->int


Композиция как основной «строительный материал»




Если у нас есть две функции, одна преобразующая яблоки в бананы (apple -> banana), а другая бананы в вишни (banana -> cherry), объединив их мы получим функции преобразования яблок в вишни (apple -> cherry). С точки зрения программиста нет разницы получена эта функция с помощью композиции или написана вручную, главное – ее сигнатура.

Композиция применима как на уровне совсем небольших функций, так и на уровне целого приложения. Вы можете представить бизнес-процесс, как цепочку вариантов использования (use case) и скомпоновать их в функцию httpRequest -> httpResponse. Конечно это возможно только для синхронных операций, но для асинхронных есть реактивное функциональное программирование, позволяющее сделать тоже самое.



Можно представлять себе композицию функций как фрактал. Определение фрактала в строгом смысле не совпадает с определением композиции. Представляя фрактал вы можете визуализировать как ваш control flow состоит из скомпонованных функций, состоящих из скомпонованных функций, состоящих из…
Шаблон компоновщик (Composite) в ООП тоже можно представлять себе «фракталом», но компоновщик работает со структурами данных, а не преобразованиями.

Типы != классы


У системы типов в ФП больше общего с теорией множеств, чем с классами из ООП. int – это тип. Но тип не обязательно должен быть примитивом. Customer – это тоже тип. Функции могут принимать на вход и возвращать функции. int -> int – тоже тип. Так что «тип» — это название для некоторого множества.

Типы тоже можно компоновать. Большая часть функциональных ЯП работает с алгебраической системой типов, отличающейся от системы классов в ООП.

Перемножение (логическое «и», record type в F#)

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

type Birthday = Person * Date

Сложение (логическое «или», discriminated union type в F#)


type PaymentMethod =  
| Cash
| Cheque of ChequeNumber
| Card of CardType * CardNumber

Discriminated union – сложное название. Проще представлять себе этот тип как выбор. Например, вы можете на выбор оплатить товар наличными, банковским переводом или с помощью кредитной карты. Между этими вариантами нет ничего общего, кроме того, все они являются способом оплаты.
Однажды нам пригодились «объединения» для моделирования предметной модели.
Entity Framework умеет работать с такими типами из коробки, нужно лишь добавить id.

Стремление к «полноте»




Давайте рассмотрим функцию «разделить 12 на». Ее сигнатура int -> int и это ложь! Если мы подадим на вход 0, функция выбросит исключение. Вместо этого мы можем заменить сигнатуру на NonZeroInteger -> int или на int -> int option.



ФП подталкивает вас к более строгому и полному описанию сигнатур функций. Если функции не выбрасывают исключений вы можете использовать сигнатуру и систему типов в качестве документации. Вы также можете использовать систему типов для создания предметной модели (Domain Model) и описания бизнес-правил (Business Rules). Таким образом можно гарантировать, что операции не допустимые в реальном мире не будут компилироваться в приложении, что дает более надежную защиту, чем модульные тесты. Подробнее об этом подходе вы можете прочитать в отдельной статье.

Функции в качестве аргументов




Хардкодить данные считается дурным тоном в программирование, вместо этого мы передаем их в качестве параметров (аргументов методов). В ФП мы идем дальше. Почему бы не параметризировать и поведение?



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

let printList anAction aList =
    for i in aList do
        anAction i

Пойдем дальше. Рассмотрим императивный пример на C#. Очевидно, что в данном коде присутствует дублирование (одинаковые циклы). Для того чтобы устранить дублирование нужно выделить общее и выделить общее в функцию:

public static int Product(int n)
{     
    int product = 1; // инициализация
    for (int i = 1; i <= n; i++) // цикл
    {
        product *= i; // действие
    }

    return product; // возвращаемое значение
} 
 
public static int Sum(int n) 
{
    int sum = 0; // инициализация
    for (int i = 1; i <= n; i++) // цикл
    {
        sum += i;
    }

    return sum; // возвращаемое значение
} 

В F# для работы с последовательностями уже есть функция fold:

let product n =
    let initialValue = 1
    let action productSoFar x = productSoFar * x

[1..n] |> List.fold action initialValue 
 
let sum n =
    let initialValue = 0
    let action sumSoFar x = sumSoFar+x

[1..n] |> List.fold action initialValue

Но, позвольте, в C# есть Aggregate, который делает тоже самое! Поздравляю, LINQ написан в функциональном стиле :)
Рекомендую цикл статей Эрика Липперта о монадах в C#. С десятой части начинается объяснение «монадической» природы SelectMany

Функции в качестве интерфейсов


Допустим у нас есть интерфейс.

interface IBunchOfStuff
{
    int DoSomething(int x);
    string DoSomethingElse(int x); // один интерфейс - одно дело
    void DoAThirdThing(string x); // нужно разделить
} 

Если взять SRP и ISP и возвести их в абсолют все интерфейсы будут содержать только одну функцию.

interface IBunchOfStuff
{
    int DoSomething(int x);
} 

Тогда это просто функция int -> int. В F# не нужно объявлять интерфейс, чтобы сделать функции взаимозаменяемыми, они взаимозаменяемы «из коробки» просто по своей сигнатуре. Таким образом паттерн «стратегия» реализуется простой передачей функции в качестве аргумента другой функции:

let DoSomethingWithStuff strategy x =
    strategy x

Паттерн «декоратор» реализуется с помощью композиции функций

let isEvenWithLogging = log >> isEven >> log  // int -> bool

Здесь автор для простоты изложения опускает вопросы семантики. При моделировании реальных предметных моделей одной сигнатуры функции не всегда достаточно.

Каррирование и частичное применение


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



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

Такие преобразования возможны не только для компилируемых функций в программировании, но и для математических функций. Возможность такого преобразования впервые отмечена в трудах Готтлоба Фреге, систематически изучена Моисеем Шейнфинкелем в 1920-е годы, а наименование получило по имени Хаскелла Карри — разработчика комбинаторной логики, в которой сведение к функциям одного аргумента носит основополагающий характер.

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

let three = 1 + 2 
let three = (+) 1 2 
let three = ((+) 1) 2 
let add1 = (+) 1  
let three = add1 2 

Это называется частичным применением. В функциональных ЯП частичное применение заменяет принцип инъекции зависимостей (Dependency Injection)

// эта функция требует зависимость
let getCustomerFromDatabase connection (customerId:CustomerId) =
    from connection
    select customer
    where customerId = customerId
 
// а эта уже нет
let getCustomer1 = getCustomerFromDatabase myConnection 

Продолжения (continuations)


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

int Divide(int top, int bottom) 
{
    if (bottom == 0)
    {
        // кто решил, что нужно выбросить исключение?
        throw new InvalidOperationException("div by 0");
    }
    else 
    {
        return top/bottom;
    }
}

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

void Divide(int top, int bottom, Action ifZero, Action<int> ifSuccess) 
{
    if (bottom == 0)
    {
        ifZero();
    }
    else
    {
        ifSuccess( top/bottom );
     }
}
 

Если вы когда-нибудь писали асинхронный код, то наверняка знакомы с «пирамидой погибели» (Pyramid Of Doom)



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

let ifSomeDo f opt =
    if opt.IsSome then
        f opt.Value
    else
        None

И переписать код, используя продолжения

let example input =
    doSomething input
    |> ifSomeDo doSomethingElse
    |> ifSomeDo doAThirdThing
    |> ifSomeDo (fun z -> Some z)

Монады


Монады – это одно из «страшных» слов ФП. В первую очередь, из-за того, что обычно объяснения начинаются с теории категорий. Во вторую — из-за того что «монада» — это очень абстрактное понятие, не имеющее прямой аналогии с объектами реального мира. Я большой сторонник подхода «от частного к общему». Поняв практическую пользу на конкретном примере проще двигаться дальше к более полному и абстрактному определению.



Зная о «продолжениях», вернемся к аналогии с рельсами и тоннелем. Функцию, в которую передаются аргумент и два «продолжения» можно представить как развилку.

Но такие функции не компонуются :(




На помощь приходит функция bind



let bind nextFunction optionInput =
    match optionInput with
    // передаем результат выполнения предыдущей функции в случае успеха
    | Some s -> nextFunction s
    // или просто пробрасываем значение None дальше
    | None -> None

Код пирамиды погибели может быть переписан с помощью bind

// было
let example input =
    let x = doSomething input
    if x.IsSome then
        let y = doSomethingElse (x.Value)
        if y.IsSome then
            let z = doAThirdThing (y.Value)
            if z.IsSome then
                let result = z.Value
                Some result
            else
               None
        else 
            None 
    else
        None 

// стало
let bind f opt =
    match opt with
        | Some v -> f v
        | None -> None

let example input =
    doSomething input
        |> bind doSomethingElse
        |> bind doAThirdThing
        |> bind (fun z -> Some z)

Кстати, это называется «monadic bind». Скажите своим друзьям, любителям хаскеля, что вы знаете, что такое «monadic bind» и вас примут в тайное общество:)

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



Bind для обработки ошибок


Если у вас появилось смутное ощущение, что дальше идет описание монады Either, так оно и есть

Рассмотрим код на C#. Он выглядит достаточно хорошо: все кратко и понятно. Однако в нем отсутствует обработка ошибок. Действительно, что может пойти не так?

string UpdateCustomerWithErrorHandling() 
{
    var request = receiveRequest();
    validateRequest(request);
    canonicalizeEmail(request);
    db.updateDbFromRequest(request);
    smtpServer.sendEmail(request.Email) 
    return "OK";
} 

Мы все знаем, что обрабатывать ошибки нужно. Добавим обработку.

string UpdateCustomerWithErrorHandling() 
{
    var request = receiveRequest();
    var isValidated = validateRequest(request);
    if (!isValidated) 
    {
        return "Request is not valid"
    }
    
    canonicalizeEmail(request);
    try 
    {
         var result = db.updateDbFromRequest(request);
         if (!result) 
        {
           return "Customer record not found"
        }
    }
    catch
    {
        return "DB error: Customer record not updated"
    } 
 
    if (!smtpServer.sendEmail(request.Email))
    {
        log.Error "Customer email not sent"
    } 
 
    return "OK";
} 

Вместо шести понятных теперь 18 не понятных строчек. Это 200% дополнительных строчек кода. Кроме того, линейная логика метода теперь зашумлена ветвлениями и ранними выходами.

С помощью bind можно абстрагировать логику обработки ошибок. Вот так будет выглядеть метод без обработки ошибок, если его переписать на F#:

А вот этот код но уже с обработкой ошибок:


Более подробно эта тема раскрыта в отдельном докладе.

Функторы


Мне не очень понравилось описание функторов у Скотта. Прочитайте лучше статью «Функторы, аппликативные функторы и монады в картинках»

Моноиды


К сожалению, для объяснения моноидов не подходят простые аналогии. Приготовьтесь к математике.

Я предупредил, итак, математика


  • 1 + 2 = 3
  • 1 + (2 + 3) = (1 + 2) + 3
  • 1 + 0 = 1
    0 + 1 = 1

И еще немного


  • 2 * 3 = 6
  • 2 * (3 * 4) = (2 * 3) * 4
  • 1 * 2 = 2
    2 * 1 = 2

Что общего между этими примерами?


  1. Есть некоторые объекты, в данном случае числа, и способ их взаимодействия. Причем результат взаимодействия — это тоже число (замкнутость).
  2. Порядок взаимодействия не важен (ассоциативность).
  3. Кроме того, есть некоторый специальный элемент, взаимодействие с которым не меняет исходный объект (нейтральный элемент).

За более строгим определением обратитесь к википедии. В рамках статьи обсуждается лишь несколько примеров применения моноидов на практике.

Замкнутость


Дает возможность перейти от попарных операций к операциям на списках

1 * 2 * 3 * 4
[ 1; 2; 3; 4 ] |> List.reduce (*)

Ассоциативность


Применение принципа «разделяй и властвуй», «халявная» параллелизация. Если у нашего процессора 2 ядра и нам нужно рассчитать значение 1 + 2 + 3 + 4. Мы можем вычислить 1 + 2 на первом ядре, а 3 + 4 — на втором, а результат сложить. Больше последовательных вычислений — больше ядер.

Нейтральный элемент


С reduce есть несколько проблем: что делать с пустыми списками? Что делать, если у нас нечетное количество элементов? Правильно, добавить в список нейтральный элемент.
Кстати, в математике часто встречается определение моноида как полугруппы с нейтральным элементом. Если нейтральный элемент отсутствует, то можно попробовать его доопределить, чтобы воспользоваться преимуществами моноида.

Map / Reduce


Если ваши объекты — не моноиды, попробуйте преобразовать их. Знаменитая модель распределенных вычислений Google — не более чем эксплуатация моноидов.



Эндоморфизмы


Функции с одинаковым типом входного и выходного значения являются моноидами и имеют специальное название — «эндоморфизмы» (название заимствовано из теории категорий). Что более важно, функции, содержащие эндоморфизмы могут быть преобразованы к эндоморфизмам с помощью частичного применения.
Грег Янг открыто заявляет, что Event Sourcing — это просто функциональный код. Flux и unidirectional data flow, кстати тоже.


Монады VS моноиды


Монады являются моноидами, ведь как известно, монада — это всего лишь моноид в категории эндофункторов, а монадические законы — не более чем определение моноида в контексте продолжений.
Кстати, бастион ООП — GOF тоже содержит монады. Паттерн «интерпретатор» — это так называемая свободная монада.

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


  1. mSnus
    18.09.2017 02:25
    +16

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


    Когда-то изобрели процедурный подход. Жить стало круче, стали плодить процедуры из всего, чего можно, где можно было обойтись прямым кодом: вместо a = a + 2 стали писать Add(a, 2). Что улучшает читаемость и поддерживаемость кода.


    Потом стали работать с ООП, и все завертелось ещё круче — все должно быть объектами! Теперь мы можем писать a.Add(2), что ещё лучше улучшает читаемость и поддерживаемость!


    Но это мало, далее пошла мода на интерфейсы. Стоит ведь предусмотреть, что 2 — это не совсем два, а добавить — это не всегда сложить. Реализуем интерфейс IAdd! Что улучшит читаемость и поддерживаемость кода, само собой, а также сделает его ну ОЧЕНЬ гибким.


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


    Но это мало. Это, знаете ли, не Bleeding Edge! Заверните, пожалуйста, в функциональное программирование, нарежьте монадами по 100гр кусочек и подайте под мелко нашинкованными лямбдами. Выглядит вкусно? Что, добавить 2 к переменной a? Это прошлый век! Как вы можете оскорблять высокую кухню самой постановкой такой задачи!


    Идите к этим, как их, низкоуровневым! Пусть выдадут вам
    add ax, 2! А у нас — искусство!


    1. symbix
      18.09.2017 06:59
      +3

      ООП не о том, что мы прибавляем 2, а о том, зачем мы это делаем. (Поэтому никакого .add там не будет).


      1. musuk
        18.09.2017 20:58
        -1

        Там будет CQRS и Add Action


      1. vlreshet
        19.09.2017 09:32
        +5

        Там будет какой-нибудь MathOperationsFactory, который вернёт AddingService, который примет два числа, а вернёт объект типа MathOperationResult. Или же вообще, MathOperationsBuilder, которому надо будет на вход передать два числа, класс отвечающий за сложение, а потом класс-конфиг, который укажет в каком виде это обработать и отдать. Как-то так ?\_(?)_/?


        1. symbix
          19.09.2017 10:03
          +3

          Ну вот вы шутите, а многие за чистую монету такое принимают. И даже так пишут :-)


          Для математики ООП вообще не нужно. ООП удобно для моделирования предметной области на языке этой самой предметной области. ФП удобно для реализации алгоритмов и всяких цепочек взаимодействий-преобразований. Никто не запрещает совмещать :-)


          Или еще можно сказать, что инструментами ООП удобно описывать "что надо сделать", а инструментами ФП — "как мы это делаем".


          1. mayorovp
            19.09.2017 10:29
            +1

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


            А вот "как" описывается, в основном, структурным программированием, которое не относится ни к ООП, ни к ФП.


            1. mayorovp
              19.09.2017 10:45

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


              Императивный подход:


                  IEnumerable<int> SomeMethod(IEnumerable<Foo> foos)
                  {
                      foreach (var foo in foos)
                      {
                          if (foo.x > 42)
                          {
                              foreach (var bar in foo.Bars)
                              {
                                  yield return foo.y + bar.z;
                              }
                          }
                      }
                  }

              Декларативный подход:


                  IEnumerable<int> SomeMethod(IEnumerable<Foo> foos) =>
                      from foo in foos
                      where foo.x > 42
                      from bar in foo.Bars
                      select foo.y + bar.z;

              Эквивалентный код:


                  IEnumerable<int> SomeMethod(IEnumerable<Foo> foos) => foos
                      .Where(foo => foo.x > 42)
                      .SelectMany(foo => foo.Bars.Select(bar => foo.y + bar.z));

              В первом случае используется структурное программирование, во втором случае угадывается монада List. Но оба подхода активно используют ООП и его паттерны (первый код неявно использует паттерн "итератор", второй к нему добавляет "цепочку ответственности").


              1. symbix
                19.09.2017 15:50

                А, ну это смотря что считать ООП.


                Я вот не считаю, что если я написал class ImmutableCollection<T> с методами map и filter, то это ООП. :-)


                1. mayorovp
                  19.09.2017 15:54

                  Если это простой класс не привязанный ни к какой иерархии — то тут конечно же от ООП будет только обертка. А вот если ваш ImmutableCollection реализует хотя бы интерфейс IEnumerable (C#) или Iterable (Java) — то это уже типичный механизм ООП.


                  1. Druu
                    19.09.2017 16:04

                    С чего бы это наличие интерфейса свидетельствовало об ООП? Интерфейс — это просто описание набора операций, доступных для данного типа.


                    1. mayorovp
                      19.09.2017 16:13
                      +3

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


                      Кроме того, тут используется паттерн из мира ООП ("итератор"), который запрещен в мире функционального программирования, поскольку построен вокруг изменяемого состояния — так что как только вы его реализовали, ваш код перестает быть чистым функциональным.


                      1. Druu
                        19.09.2017 16:57

                        > Кроме того, тут используется паттерн из мира ООП («итератор»)

                        Это как вы определили, что там используется итератор? Из кода это никак не следует.

                        > С того, что тут задействуются два кита ООП — наследование и полиморфизм

                        Интерфейсы не наследуются, а реализуются. И если интерфейс в стиле тайпклассов хаскеля, не предполагает полиморфизма подтипов (по крайней мере вне костылей из existential types)? Будет уже не ООП?


                        1. mayorovp
                          19.09.2017 17:13
                          +1

                          Это как вы определили, что там используется итератор? Из кода это никак не следует.

                          Следует. Интерфейсы IEnumerable и Iterable — это стандартные реализации паттерна "итератор".


                          Интерфейсы не наследуются, а реализуются.

                          Это всего лишь особенность конкретного языка.


                          И если интерфейс в стиле тайпклассов хаскеля, не предполагает полиморфизма подтипов (по крайней мере вне костылей из existential types)? Будет уже не ООП?

                          Мы все еще говорим о C# и Java или обсуждаем какой-то неопределенный язык программирования?


                          Разумеется, в том же Хаскеле классы не являются признаком ООП по построению. Но Хаскель — чистый функциональный язык, а я говорю про мультипарадигменный C#.


                          1. Druu
                            19.09.2017 18:34

                            > Следует. Интерфейсы IEnumerable и Iterable — это стандартные реализации паттерна «итератор».

                            То есть если я поменяю IEnumerable на List, код чудесным образом перестанет быть ООП?

                            > Это всего лишь особенность конкретного языка.

                            Это особенность интерфейсов. Интерфейсы нельзя наследовать, потому что интерфейс — это контракт. Что значит «наследовать контракт»?

                            > Мы все еще говорим о C# и Java или обсуждаем какой-то неопределенный язык программирования?

                            Какая разница, на каком? У вас что, _один и тот же_ код то функциональный, то ООП в зависимости от того, какой язык?


                            1. 0xd34df00d
                              19.09.2017 19:48

                              Какая разница, на каком? У вас что, _один и тот же_ код то функциональный, то ООП в зависимости от того, какой язык?

                              Классы в хаскеле и классы в сишарпе — это две большие разницы.


                              1. Druu
                                19.09.2017 20:02

                                > Классы в хаскеле и классы в сишарпе — это две большие разницы.

                                В хаскеле нету классов


                                1. 0xd34df00d
                                  19.09.2017 20:03

                                  Есть. Просто типов :)


                                  1. Druu
                                    19.09.2017 20:15

                                    То, что там в названии одинаковое слово, не означает, что между ними есть что-то общее. Это совершенно другая сущность.


                                1. mayorovp
                                  19.09.2017 20:07

                                  Таки есть, хотя к классам из ООП они имеют довольно слабое отношение.


                                  A Gentle Introduction to Haskell: Classes


                            1. mayorovp
                              19.09.2017 19:48

                              Это особенность интерфейсов. Интерфейсы нельзя наследовать, потому что интерфейс — это контракт. Что значит «наследовать контракт»?

                              Когда придумывались классические "три кита" ООП, интерфейсов/контрактов не было, отсюда и некоторое несоответствие в определениях. Технически интерфейс мало отличается от абстрактного класса без полей.


                              Применительно к интерфейсам в C#, при реализации интерфейса в некотором смысле наследуются все методы-расширения, определенные для этого интерфейса. Например, любой класс, реализующий IEnumerable<T>, автоматически получает методы-расширения Where, Select, SelectMany, Join и прочие.




                              То есть если я поменяю IEnumerable на List, код чудесным образом перестанет быть ООП?

                              Мои ответы надо рассматривать в контексте тех комментариев, которые я писал ранее и тех вопросов, на которые я отвечал.


                              Напомню, началась эта ветка с моего ответа на вот этот комментарий:


                              Я вот не считаю, что если я написал class ImmutableCollection<T> с методами map и filter, то это ООП.

                              Если вы считаете что слово class автоматически означает ООП — то лучше расскажите об этом symbix, а не мне. Если вы так не считаете — то я не понимаю какой тезис вы пытаетесь отстоять в этом споре.


                              1. Druu
                                19.09.2017 20:09

                                > Применительно к интерфейсам в C#, при реализации интерфейса в некотором смысле наследуются все методы-расширения, определенные для этого интерфейса. Например, любой класс, реализующий IEnumerable, автоматически получает методы-расширения Where, Select, SelectMany, Join и прочие.

                                И? Это вы к чему? Как это меняет тот факт, что интерфейс — это контракт?

                                > Если вы так не считаете — то я не понимаю какой тезис вы пытаетесь отстоять в этом споре.

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


                                1. mayorovp
                                  19.09.2017 20:14

                                  Вы опять путаете общее понятие и элемент языка.


                                  Интерфейс как элемент языка очень похож на абстрактный класс без полей.


                                  1. Druu
                                    19.09.2017 20:20

                                    > Интерфейс как элемент языка очень похож на абстрактный класс без полей.

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


                                    1. mayorovp
                                      19.09.2017 20:24

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


                                      1. Druu
                                        19.09.2017 20:27

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


                                        1. mayorovp
                                          19.09.2017 20:32

                                          «Подобной логики» — это какой?

                                          Я видел программы на C#, в которых все методы были статические и находились в классе Program, а остальные классы были без конструкторов и с публичными полями. Это было в чистом виде структурное программирование на C#.

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


                                          1. Druu
                                            19.09.2017 20:38

                                            > Я видел программы на C#, в которых все методы были статические и находились в классе Program

                                            Но ведь _класс_, да еще и статический — это со всей очевидностью ООП-шная конструкция. Как и методы. Ровно в той же степени, как интерфейсы или абстрактные классы. Разве из этого не следует сразу сделать вывод, что это код в ООП-стиле? И не-ООП код, получается, на c# вообще не написать?


                                            1. mayorovp
                                              19.09.2017 20:39

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


                                  1. DistortNeo
                                    19.09.2017 21:52

                                    Не понимаю, зачем тут спорить. Всё зависит от использования интерфейса.

                                    Когда мы приводим объект к типу `IEnumerable`, например, `void Foo(IEnumerable obj)`, то интерфейс неотличим от абстрактного класса.

                                    Когда же мы накладываем ограничение на тип, например `void Foo(T obj) where T: IEnumerable`, интерфейс является контрактом.

                                    Всё остальное типа особенности множественного наследования здесь не имеет значения.


                              1. ApeCoder
                                19.09.2017 22:28

                                Что значит «наследовать контракт»?

                                Можно сделать более специфичный контракт который унаследует поддерживаемые операции у другого контракта и добавит свои.


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


                        1. 0xd34df00d
                          19.09.2017 19:47

                          И если интерфейс в стиле тайпклассов хаскеля, не предполагает полиморфизма подтипов (по крайней мере вне костылей из existential types)?

                          Что вы называете подтипами?
                          И почему existentials сразу костыли? У них есть куча своих весьма изящных применений.


                          1. Druu
                            19.09.2017 20:32

                            > И почему existentials сразу костыли?

                            Костыли — в качестве эмуляции подтипирования. Потому что подтипирование в хаскеле считается ненужным.

                            > Что вы называете подтипами?

                            Не совсем понимаю вопроса. То же, что и все? Есть системы типов с подтипированием (например, lambda<:), у них есть семантика.


                            1. 0xd34df00d
                              19.09.2017 20:43

                              Отлично, тогда мы на одном языке говорим на тему подтипов.


                              Но как на хаскеле existentials помогают эмулировать подтипы? Я то ли ни разу с таким не сталкивался, то ли не распознал это, когда таки сталкивался.


                              1. Druu
                                20.09.2017 09:32

                                > Но как на хаскеле existentials помогают эмулировать подтипы?

                                data IShow = forall a. (Show a) => IShow a

                                тогда любой инстанс IShow ведет себя так же, как в ООП ведет себя класс, реализующий интерфейс IShow. Отличие только в наличии обертки, но на самом деле чисто с формальной точки зрения запись data IShow = forall a. (Show a) => a тоже валидна, просто в хаскеле так нельзя (вроде бы нельзя, по крайней мере, без каких-то хитрых расширений).


                                1. Druu
                                  20.09.2017 09:38

                                  > тогда любой инстанс IShow

                                  Любой инстанс Show, конечно же.


                                1. 0xd34df00d
                                  20.09.2017 18:44

                                  Я бы не назвал это сабтайпингом. Обычный type erasure — есть у вас некоторая коробка, в которой лежит объект, про который вы только знаете, что вы его можете отображать, и всё.


                                  1. Druu
                                    20.09.2017 20:03

                                    > объект, про который вы только знаете, что вы его можете отображать, и всё.

                                    Но ведь сабтайпинг именно так и работает: «перед нами объект, про который мы можем сказать, что мы с ним можем делать все, что и с объектом, подтипом которого он является». В данном случае любой инстанс Show ведет себя как подтип IShow (если без боксинга).


                                    1. 0xd34df00d
                                      21.09.2017 01:01

                                      Но вы с ним не можете делать ничего другого.


                                      Да и чем тогда это отличается от a в функции foo :: Show a => a -> Smth?


                                      1. Druu
                                        21.09.2017 03:09

                                        > Да и чем тогда это отличается от a в функции foo :: Show a => a -> Smth?

                                        Тем, что foo можно применять только к одному конкретному типу a, а foo :: IShow => smth к любому «подтипу» IShow (т.к. а там нет).

                                        > Но вы с ним не можете делать ничего другого.

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


                      1. 0xd34df00d
                        19.09.2017 19:46

                        Кроме того, тут используется паттерн из мира ООП («итератор»), который запрещен в мире функционального программирования

                        Итератор в ФП — это скорее обычный список (по крайней мере, в ленивых хаскелеподобных языках). Собственно, если относиться к списку не как к структуре данных, а к методу управления потоком исполнения, то всё становится сильно проще и понятнее.


                        1. 0xd34df00d
                          19.09.2017 19:53
                          +1

                          Это, кстати, к разговору о паттернах в ФП. Использование ленивого списка как итератора и управляющей конструкции — вполне себе паттерн.


                        1. mayorovp
                          19.09.2017 19:55
                          +1

                          Ну уж нет. Итератор в ООП — это общий паттерн для организации обхода произвольных коллекций, а head/tail-список — это конкретная реализация.


                          Аналогом паттерна "Итератор" в Хаскеле можно назвать классы Traversable и Foldable, но никак не список.


                          1. 0xd34df00d
                            19.09.2017 20:03

                            У Foldable-то не зря есть метод toList — то бишь, любой Foldable не зря имеет морфизм в списки (чаще всего забывающий, но для целей обхода это неважно).


                            1. mayorovp
                              19.09.2017 20:05
                              +1

                              Но первична-то именно операция foldr, а toList выражается через нее.


                              1. 0xd34df00d
                                19.09.2017 20:13

                                Ну, естественно, это ж не прямой аналог.


                                Думать в терминах foldMap, на мой взгляд, удобнее, кстати.


                    1. lair
                      19.09.2017 16:50
                      +1

                      … вот мы и пришли к вопросу "что же такое ООП".


                      1. TheShock
                        19.09.2017 16:51
                        -2

                        «ООП — это миф, или как на обманывают тыжпрограмисты», смотрите на РЕНТВ сегодня в 26.00.


                        1. lair
                          19.09.2017 16:52
                          +3

                          Я считаю, что опечатку "как на обманывают" надо расшифровывать "как, нах, обманывают".


                  1. symbix
                    20.09.2017 01:13

                    Ну, от того, что реализован интерфейс, тоже пока еще ничего не случится. ООП появится там, где будет someMethod(Iterable foo). :-)


            1. 0xd34df00d
              19.09.2017 19:44

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

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


              1. mayorovp
                19.09.2017 20:03

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


                Функциональное выражение алгоритма на деревьях — это скорее схема такого алгоритма чем сам алгоритм.


                1. 0xd34df00d
                  19.09.2017 20:12

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

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


                  что для функциональных языков едва ли не запретное слово :-)

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


                  Функциональное выражение алгоритма на деревьях — это скорее схема такого алгоритма чем сам алгоритм.

                  Фиг с ними с деревьями. Чем take 5 . sortBy (comparing fst) хуже такого кода?


                  std::sort(vec.begin(), vec.end(),
                          [](const auto& left, const auto& right)
                              { return left.first < right.first; });
                  vec.erase(std::min(vec.end(), vec.begin() + 5), vec.end());


                  1. mayorovp
                    19.09.2017 20:18

                    Ничем не хуже. Просто take 5 . sortBy (comparing fst) — декларативное выражение того что мы хотим получить в результате, а приведенный вами фрагмент кода на C++ — императивный алгоритм получения.


                    1. 0xd34df00d
                      20.09.2017 18:46

                      Но есть очевидный и в известном смысле гладкий морфизм. На плюсах я тоже могу написать немного библиотечного кода, чтобы потом делать take(5) | sortBy(comparing(fst)), но это ж не сделает плюсы функциональным языком? Да и в какой момент код перестанет быть императивным и станет функциональным?


                      1. mayorovp
                        20.09.2017 19:38
                        +2

                        Код перестает быть императивным когда из него пропадают явные указания на промежуточные состояния процесса решения задачи.


                        Так, в вашем примере после исполнения std::sort контейнер vec оказывается в состоянии "отсортирован, но может содержать более 5 элементов". Это промежуточное состояние явно требуется кодом — но оно не требуется постановкой задачи и никоим образом из нее не следует!


                        В то же время в варианте take 5 . sortBy (comparing fst) состояние "отсортирован, но может содержать более 5 элементов" имеет не входной список и не выходной — а некоторый промежуточный, не имеющий даже имени. Он полностью скрыт реализацией.


                        Тем не менее, если сделать библиотеку на плюсах и написать что-то типа x = take(5) | sortBy(comparing(fst)) | x или x.transform(take(5) | sortBy(comparing(fst))) — то эта команда, хоть и написана в декларативном стиле, скорее всего будет являться частью какого-то императивного алгоритма.


                        "Функциональным" же код делает применение паттернов и подходов функционального программирования, сам по себе функциональный код может быть как декларативным, так и императивным.


                        1. 0xd34df00d
                          21.09.2017 01:04

                          Код перестает быть императивным когда из него пропадают явные указания на промежуточные состояния процесса решения задачи.

                          То есть, если я напишу


                          sortAndTake xs = take 5 xs'
                              where xs' = sortBy (comparing fst) xs

                          или, одним выражением и с сохранением типа всего терма,


                          \xs -> let xs' = sortBy (comparing fst) xs in take 5 xs'

                          то это станет императивным кодом?


                          Так, в вашем примере после исполнения std::sort контейнер vec оказывается в состоянии «отсортирован, но может содержать более 5 элементов». Это промежуточное состояние явно требуется кодом — но оно не требуется постановкой задачи и никоим образом из нее не следует!

                          Если это спрятано внутри отдельной функции, то это точно так же является деталью реализации.


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


                          1. Druu
                            21.09.2017 03:21

                            > то это станет императивным кодом?

                            Не станет, потому что у вас никакого промежуточного состояния нету. f x where x = y — это просто (x => f(x))(y)


                  1. mayorovp
                    20.09.2017 09:09
                    +1

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

                    А параллельный алгоритм обычно можно рассматривать как несколько таких последовательностей.


                    1. myrslok
                      20.09.2017 13:32
                      +1

                      Вы, наверное, имеете в виду линейный порядок.


                      1. mayorovp
                        20.09.2017 14:05

                        Да, точно. Наверное, 0xd34df00d тоже имел в виду именно его.


                        1. 0xd34df00d
                          20.09.2017 18:44

                          Да, его.


                1. symbix
                  20.09.2017 01:18

                  Ну, да, наверное, скорее "выражение алгоритма". Я несколько неудачно выразился, а 0xd34df00d правильно понял, что я имел ввиду.


        1. DistortNeo
          19.09.2017 14:00
          +2

          Я, кстати, так уже даже делал для задачи генерации кода обработки изображений в рантайме.


    1. nevoroman
      18.09.2017 08:13
      +9

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


      1. mayorovp
        18.09.2017 11:22

        Ага, особенно "практичен" вот этот кусок кода:


        let printList anAction aList =
           for i in aList do
               anAction i

        Был обычный алгоритм, который делал вполне конкретную вещь. Из него вынесли наружу всю конкретику, оставив тривиальный код. Но ведь [1..10] и printLn на самом деле никуда не делись! Они просто перешли к вызывающему коду, теперь каждый кто вызывает printList должен указывать еще и эти [1..10] и printLn.


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


        Напомнило https://xkcd.com/1790/


        Что самое веселое, даже после такого преобразования код все еще не абстрагирован от всего. К примеру, для асинхронных операций код будет работать некорректно — запустит их параллельно вместо последовательного выполнения. О ужас! Нам срочно нужна операция foldM!


        PS в целом пост мне понравился, но конкретно этот пример вызвал возмущение своей бессмысленностью.


        1. DistortNeo
          18.09.2017 12:39
          +2

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

          Кстати, называть функцию `printList` после такого преобразования уже некорректно, правильнее `visitList`. А `printList` — результат частичного применения `visitList` с параметром `printLn`.


        1. ApeCoder
          18.09.2017 12:56
          -1

          Они просто перешли к вызывающему коду, теперь каждый кто вызывает printList должен указывать еще и эти [1..10] и printLn.

          Вероятно, потому, что функция теперь называется неправильно. Она не печаетает а применяет действие к списку поэтому ее надо назвать по другому


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

          Функция скрывает, как именно происходит перебор — например значение i уже наружи не видно и не надо о нем заботиться


          К примеру, для асинхронных операций код будет работать некорректно

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


          1. mayorovp
            18.09.2017 13:01

            Функция скрывает, как именно происходит перебор — например значение i уже наружи не видно и не надо о нем заботиться

            Но раньше-то она скрывала список и действия!


            Асинхронные операции это синхронные операции по созданию тасков. Так что для них код скорее всего даже не скомпилируется

            Хорошо, что ошибочный код не скомпилируется. Но плохо что он при этом не будет работать.


            1. ApeCoder
              18.09.2017 13:11

              Но раньше-то она скрывала список и действия!

              Это уже не она. Никто не мешает оставить старую, просто абстрагировать перебор оттуда


              let printList = List.iter printItem
              

              Хорошо, что ошибочный код не скомпилируется. Но плохо что он при этом не будет работать.

              Если надо последовательно объединить асинхронные функции просто воспользуйтесь другой функцией


              1. mayorovp
                18.09.2017 13:26

                Никто не мешает оставить старую, просто абстрагировать перебор оттуда

                Автору статьи тоже не помешало бы так сделать :-)


                Если надо последовательно объединить асинхронные функции просто воспользуйтесь другой функцией

                Но зачем в таком случае абстракция над перебором списка, если она не позволяет нам повторно использовать код?


                1. ApeCoder
                  18.09.2017 14:24
                  +1

                  Но зачем в таком случае абстракция над перебором списка, если она не позволяет нам повторно использовать код?

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


                  1. 0xd34df00d
                    18.09.2017 18:42

                    Она нам позволяет повторно использовать код. Но не весь. Какого поведения вы хотите от нее? Чтобы выполняла параллельно?

                    Кстати, для этого достаточно произвести композицию функции map с соответствующим примитивом, написав вместо


                    map f list

                    такое:


                    withStrategy (parList strat) $ map f list

                    Ну или просто


                    parMap f list


    1. 0xd34df00d
      18.09.2017 18:43

      Заверните, пожалуйста, в функциональное программирование, нарежьте монадами по 100гр кусочек и подайте под мелко нашинкованными лямбдами.

      Чтобы прибавить к одному числу другое, не нужны никакие монады. И даже jquery не нужно.


  1. TheShock
    18.09.2017 03:04
    +26

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

    Точно такой же бред можно сочинить и в обратную сторону:

    — Функции высшего порядка / Класс
    — Каррирование / Класс
    — Композиция функций / Класс
    — Предикат / Класс
    — Функтор / Класс
    — Лифт / Класс
    — Моноид / Класс
    — Монада / Класс

    И так далее из списка "жаргона ФП"

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

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

    Ну вот даже это что — «просто функция»? Или, все-таки, «паттерн»?
    что делать с пустыми списками? Что делать, если у нас нечетное количество элементов? Правильно, добавить в список нейтральный элемент

    И да, на счет КДПВ. Я, конечно, могу ошибаться, но, насколько я знаю, в ФП, Лисп — это просто баловство вроде JS в сравнении с Haskell, потому мне непонятно, почему он улетел так высоко.


    1. spmbt
      18.09.2017 04:43
      +1

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


    1. marshinov Автор
      18.09.2017 08:16

      Этот доклад раздражает не только вас. Подобным образом отреагировал и Дядя Боб. Кстати, его пост-ответ тоже заслуживает перевода.


    1. AndreyRubankov
      18.09.2017 10:17
      +5

      Добавлю еще про манипуляции в статье:

      А вот этот код но уже с обработкой ошибок:image


      На самом деле, данный код совсем не соответствует семантике приведенного в пример «ужасного ООП с проверкой ошибок».

      В ООП, каждая ошибка выдает какой-то свой результат, в ФП варианте – все ошибки просто игнорируются. Если добавить проверку ошибок как в ООП, то будет не на много лучше чем в ООП, на который автор жаловался.

      ФП вариант с проверкой ошибок соответствует ООП варианту без проверки, где каждый метод может кинуть RuntimeException в случае ошибки.


      1. marshinov Автор
        18.09.2017 10:43
        -1

        ФП вариант с проверкой ошибок соответствует ООП варианту без проверки, где каждый метод может кинуть RuntimeException в случае ошибки.

        Здесь я с вами согласен, но только для однопоточного кода. С TPL вариант с exception'ами уже не такой хороший. Кроме этого, нужно гарантировать, что в программе только один catch, иначе высок риск «проглатывания» ошибок.


        1. AndreyRubankov
          18.09.2017 11:24
          -1

          TPL
          Это уже смесь фп и ооп подходов и является нетрадиционным. Потому этот кейс не стоит брать во внимание – там другие правила.

          Кроме этого, нужно гарантировать, что в программе только один catch, иначе высок риск «проглатывания» ошибок.
          Не обязательно.

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

          Это все тот же ".catch()" в промисах, но в промисах он менее гибкий.


          1. marshinov Автор
            18.09.2017 11:32
            +1

            Потому этот кейс не стоит брать во внимание – там другие правила

            Если вас кейс на данный момент не слишком волнует, это не значит, что его не стоит брать во внимание. Вот был у вас однопоточный код, стал он выполняться двое суток. Надо параллелить, а у вас внутри везде throw. Весь код на помойку, переписываем на TPL с нуля? Весело, но дорого.


            1. AndreyRubankov
              18.09.2017 12:10
              -1

              1. Мои предыдущие комменты в этом треде относятся конкретно к данной статье, а не к всевозможным особенностям реализации каких-то фреймворков.

              2. Теперь конкретно ваш пример:

              Вот был у вас однопоточный код, стал он выполняться двое суток. Надо параллелить, а у вас внутри везде throw.
              Это самое плохое решение в данном случае.

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

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


              1. marshinov Автор
                18.09.2017 12:24
                +1

                Такие проблемы не появляются на ровном месте, а если вы с нею столкнулись, то у вас проблемы гораздо большего масштаба, чем «у нас в коде везде throw».
                Случай из жизни. Жили были программисты. Совместили они логику валидации и прасинга документов. И выбрасывали они исключения для валидации. И нормально все было, пока не стали присылать сводные xls-файлы на несколько десятков миллионов строк. И, нет-нет, нельзя клиенту объяснить, что обмен xls-файлами такого размера не самое оптимальное решение.

                Ждать сутки для разбора этого файла последовательно — не вариант, тем более, что железо на проде позволяет и не все строки файлов связаны. Map / Reduce — прям то, что нужно. В итоге не контролируемые побочные эффекты (функции валидации, дергающие БД и выбрасывающие исключения) значительно затруднили мне тот самый Map / Reduce.

                У Either есть сильные и слабые стороны. Вы продолжаете настаивать на том, что есть один православный способ на все случаи жизни, а все остальное к дело не относится и вообще другой случай. У автора пример тоже максимально простой, чтобы не пугать монадами и прочими эндофункторами. В ASP.NET MVC с обработкой ошибок действительно хорошо — можно обработать все декларативно. Но вы точно уверены, что у вас все исключения должны возвращать код 500? 401, 412, 422, не? Посмотрите доклад про rop целиком, прежде чем судить. Там есть много здравых мыслей.


                1. raveclassic
                  18.09.2017 12:30
                  +3

                  А еще вместо Either можно взять аналог Validation


                  1. marshinov Автор
                    18.09.2017 12:31
                    -2

                    Интересно, а без хаскеля примера нет?



                  1. 0xd34df00d
                    18.09.2017 18:53

                    Эм, а разве AccValidation обеспечивает семантику early return?


                    1. raveclassic
                      18.09.2017 20:52

                      Ммм, а должна? Это ж аппликатив, аккумулирующий все ошибки в полугруппе "слева"?


                      1. 0xd34df00d
                        18.09.2017 20:54

                        А зачем тогда оно вообще надо? Завернуть в MonadWriter, и всё.


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


                        1. raveclassic
                          18.09.2017 23:38

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


                          Validation же — своего рода Either, но со сбором всех ошибок в полугруппу (причем удобно в NonEmptyArray) без доступа к значению при провале (так как его, значения, нет). Поэтому, например, в scala cats Validated описывается как способ параллельной валидации, а Either — последовательной.


                          Если вы про early return для map функтора, то там да, все ок — failure-ветка "закорачивается".


                          PS. Вы можете меня поправить, я пока разбираюсь со всем этим добром :)


                          1. 0xd34df00d
                            19.09.2017 00:01

                            Без примеров кода сложно.


                            Validation же — своего рода Either, но со сбором всех ошибок в полугруппу (причем удобно в NonEmptyArray) без доступа к значению при провале (так как его, значения, нет).

                            А это как?


                            Вот у меня есть foo :: a -> AccValidation err1 b и bar :: b -> AccValidation err2 c. Как я могу вызвать bar, если foo мне вернула ошибку, и никакого b нет?


                            А, ну, собственно, AccValidation не является монадой, её там в списке инстансов нет. Ожидаемо.


                            1. raveclassic
                              19.09.2017 00:34

                              Без примеров кода сложно.

                              Примеры там на гитхабе есть


                              Как я могу вызвать bar, если foo мне вернула ошибку, и никакого b нет?

                              Никак, так как при попытке определить bind/flatMap ломается ap и перестает аггрегировать ошибки. Так что только через ap и с одинаковым типом значения. Но, вроде, можно считерить и ненадолго перегнать в Either. В cats даже есть хелпер withEither


                1. mayorovp
                  18.09.2017 12:44
                  +1

                  В итоге не контролируемые побочные эффекты (функции валидации, дергающие БД и выбрасывающие исключения) значительно затруднили мне тот самый Map / Reduce.

                  Что-то мне кажется, что дерганье БД было намного большей проблемой чем исключения.


                  1. nikolayv81
                    20.09.2017 08:36

                    ИМХО так и было, из личного опыта буквально на днях, с учётом ограничений xls на 1е6 строк в листе, не оптимизированный код грузит полный лист в БД как есть примерно за минуту(а вот если построчно вставлять теже самые пол миллиона, то выходит только на вставку в одной транзакции около получаса), дальнейшая обработка инструментами Oracle это никак не часы, видать просто при разработке использовались тестовые наборы по 100 строк на файл, а потом поставили на prod и внедрили.


                1. AndreyRubankov
                  18.09.2017 12:48
                  +2

                  Да, знакомая ситуация. Но тут ведь другая проблема:
                  Спроектированная система была не предназначена для такого – это архитектурная проблема.

                  Решение архитектурных проблем – это всегда больно. Даже если бы у Вас не было бы throw, но была бы логика хождения в базу и изменения какого-то общего состояния. Вы бы мучались не меньше. Но в этом случае вы бы проклинали «Stateful». Но вы упускаете тот момент, что Вы переделывали Архитектуру уже работающего приложения.

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


                  1. marshinov Автор
                    18.09.2017 13:40
                    +1

                    В данном случае проблема вообще в нарушении SRP изначально как мне кажется. Исключения и доступ к БД уже сверху наложились.

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

                    В данном случае есть «правильный» вариант (болты). Нужно было их сразу ставить. Была бы дополнительная сложность, кривая обучения, но я заметил, что по примеру джуниоры копипастят код разного качества примерно с одной эффективностью. Лучше учить копипастить сразу хороший:)

                    Я строго на стороне дяди Боба: ФП и ООП — инструменты, не заменяющие, а дополняющие друг-друга. Полезно знать и применять при необходимости приемы и той и другой.

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

                    Практическая применимость и tool-support таких концепций — отдельный вопрос. Я не считаю, что IO — супер-элегантное решение проблемы ввода-вывода в хаскеле: «посмотрите это выглядит как императивный код, но это не он»:)


        1. mayorovp
          18.09.2017 12:41
          +1

          А что не так в TPL с исключениями?


          1. marshinov Автор
            18.09.2017 12:52

            Вы правы, что сам по себе TPL не при чем. Просто в многопоточном / асинхронном коде проблемы обработки исключений (exception handling) становятся более очевидными, особенно то, что касается отслеживания exit points.

            Вообще монады vs исключения vs коды возврата тема, мягко говоря, спорная:)


            1. mayorovp
              18.09.2017 12:54
              +2

              Хорошо, а в чем особенность отслеживания exit points в многопоточном коде?


      1. Druu
        18.09.2017 12:58
        +2

        > В ООП, каждая ошибка выдает какой-то свой результат, в ФП варианте – все ошибки просто игнорируются.

        Это в maybe. Если в either, то не будут игнорироваться.


        1. AndreyRubankov
          18.09.2017 13:09

          Там конкретный код взятый из статьи, который игнорирует ошибки. Никаких «если».

          ps: можете привести пример правильного кода, который будет обрабатывать ошибки точно так же, как это делается в ООП варианте? Будет интересно посмотреть, на сколько он будет лучше смотреться.


          1. Druu
            18.09.2017 13:14
            +2

            > там конкретный код взятый из статьи

            Там ведь код на картинке будет тот же самый. Просто в другой монаде (она там, вообще говоря, не указана, просто мы из контекста знаем, что автор говорил о Maybe).


            1. AndreyRubankov
              18.09.2017 13:27
              +2

              Отлично, я в целом про это и говорил. Автор спекулирует кодом.
              В ООП версии но ставил некрасивые обработчики, чтобы показать всю уродливость, хотя так никто не делает.
              А в ФП версии кода, он даже не добавил никаких обработчиков, мол «вот смотрите как красиво!».

              Если бы автор привел пример, где каждая ветка, которая может пойти не так выдавала бы свое сообщение об ошибке, это выглядело бы уже не так красочно, как он показывал ;-)


              1. marshinov Автор
                18.09.2017 13:50

                Ошибки там не игнорируются. Возвращается первая ошибка, дальше выполнение не идет. Druu верно отметил: есть разница между Maybe и Either. Кстати, Скотт не кисло глумится над тем, как обычно объясняют монады апологеты ФП и какое впечатление это производит.


                1. AndreyRubankov
                  18.09.2017 14:04
                  +2

                  К сожалению в репозитории я не нашел указанного метода.

                  Под «игнорируются» я имел ввиду, что в данном коде нету явных обработчиков ошибок. Они будут где-то снаружи (или внутри).

                  После ошибки в первой функции мы «перейдем на красные пути» и выйдем из функции с результатом, который говорит про ошибку – я прав?

                  И тут вопрос, который я изначально подымал: Кто обрабатывает эти ошибки?
                  Судя по коду есть 2 варианта:
                  1. ошибки обрабатываются внутри каждой функции, которую вызывает updateCustomerWithErrorHandling;
                  2. ошибка обрабатывается снаружи updateCustomerWithErrorHandling.

                  В обоих случаях – мы получаем несоответствие между двумя примерами кода.


                  1. 0xd34df00d
                    18.09.2017 18:55

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


                    В случае Either это всё красиво и аккуратно спрятано под капотом bind для Either.


              1. marshinov Автор
                18.09.2017 14:20
                -1

                Вы по ссылке пройдите все-таки. Там ещё один полуторачасовой доклад, посвященны этой теме. С примерами и доскональным разбором как же эта магия работает. Продолжительность доклада «паттерны фп» — ещё полтора часа. Если тема будет востребована, я могу попробовать собраться с силами и перевести и его:)


                1. vbif
                  18.09.2017 16:31

                  Всё очень просто, вот вам полуторочасовой про то, как просто этим пользоваться, а потом ещё один доклад— ещё полтора часа.


                  1. 0xd34df00d
                    18.09.2017 18:55
                    +1

                    А вы на ООП-языках за 10 минут все паттерны познали?


                    1. vbif
                      18.09.2017 18:57
                      -1

                      Зачем?


                      1. 0xd34df00d
                        18.09.2017 19:13
                        +1

                        Чтобы не иронизировать над паттернами ООП, конечно же.


              1. Druu
                18.09.2017 15:54
                +2

                > Если бы автор привел пример, где каждая ветка, которая может пойти не так выдавала бы свое сообщение об ошибке

                Так эти ветки внутри функций за |>
                Функции выдают ошибку. В коде с монадами нам не надо при этом создавать ветки в вызывающем коде, а в коде с ООП — нужно было бы.


                1. mayorovp
                  18.09.2017 16:00

                  Так, может быть, тогда и [кажущаяся] разница в красоте ООП- и ФП-решений связана исключительно с тем, в каком месте размещается проверка и возврат сообщения об ошибке?


                  1. Druu
                    18.09.2017 16:05
                    +1

                    Ну да, исключения по control-flow изоморфны maybe/either/etc или что там еще. Однако, вариант с монадами лучше исключений тем, что, ну, там нет исключений :)
                    Плюс — всегда можно упороться трансформерами и засовывать туда на call site дополнительную логику, не меняя сам код. Но я лично не ярый сторонник подобного.


                    1. mayorovp
                      18.09.2017 16:17
                      +4

                      Эдак можно и про вариант с исключениями сказать что он лучше монад тем что в нем нет монад :-)


                      1. Druu
                        18.09.2017 16:31

                        Дык монады это не сущность, это просто то, как мы называем определенную конструкцию. А так там обычные ф-и — к которым монады полностью сводятся. И именно в том, что у монад под капотом обычные функции, которые ведут себя как обычные функции — и есть преимущество «монадического» решения. Не требуются специальные костыли рантайма, информация о наличии ошибок — содержится в типе, при наличии статической типизации — компилятор гарантирует что ошибка будет обработана, ну и поскольку это все — first-class citizen, то оно может быть допилено под конкретные нужды. С исключениями такое уже не прокатит.


                        1. mayorovp
                          18.09.2017 16:48

                          Монада — это тип данных, то есть нечто существующее в рантайме. Чем же в таком случае отличаются монады и костыли рантайма? Почему раскрутка стека — костыль рантайма, а целый лишний тип данных — нет?


                          По поводу типизации — вон в Java есть checked exceptions. Все как вы написали — информация содержится в типе, компилятор гарантирует что исключение будет поймано. И даже под свои нужны исключения прекрасно допиливаются.


                          1. Druu
                            18.09.2017 16:52
                            +3

                            > Монада — это тип данных, то есть нечто существующее в рантайме. Чем же в таком случае отличаются монады и костыли рантайма?

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

                            > По поводу типизации — вон в Java есть checked exceptions.

                            И они по определенным причинам не взлетели.


                            1. mayorovp
                              18.09.2017 16:58
                              +3

                              И они по определенным причинам не взлетели.

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


                              1. Druu
                                18.09.2017 17:06

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

                                Я могу написать ф-ю, которая принимает лямбду с заданными checked exceptions? Нет.

                                Проблема у checked exceptions не с тем, что они, с-но, checked exceptions, а с тем, что их хреново реализовали в джаве. Можно реализовать по-человечески — но штука в том, что в этом случае оно будет только названием отличаться от встроенной в язык монады Either. Точнее — это _и будет_ монада Either!


                                1. mayorovp
                                  18.09.2017 17:14
                                  +2

                                  Я могу написать ф-ю, которая принимает лямбду с заданными checked exceptions? Нет.

                                  Да ладно?


                                  1. raveclassic
                                    18.09.2017 17:16
                                    -2

                                    Так это ж не лямбда, а чит чистой воды


                                    1. TheShock
                                      18.09.2017 17:18
                                      -1

                                      Почему?


                                      1. raveclassic
                                        18.09.2017 17:26
                                        -1

                                        Может, потому-что это не лямбда, а объект с методом call?


                                        1. TheShock
                                          18.09.2017 17:30
                                          -1

                                          Просто детали реализации?


                                        1. mayorovp
                                          18.09.2017 17:31

                                          И в чем же принципиальная разница?


                                          Java довольно многословна, это факт. Но чем кроме 4 букв и 1 точки этот объект отличается от лямбды?


                                          1. raveclassic
                                            18.09.2017 17:38
                                            -1

                                            Странно, что вы тогда не apply использовали. Судя по докам, так оно и реализуется.


                                            1. mayorovp
                                              18.09.2017 17:39

                                              Э, вы вообще о чем?


                                              1. raveclassic
                                                18.09.2017 17:43

                                                Ну, запись x -> x + 2 это же тоже самое, что


                                                new Function<Number, Number>() {
                                                  @Override
                                                  public Number apply(x: Number) {
                                                    return x + 2;
                                                  }
                                                }

                                                Не? Я не пишу на джаве


                                                1. mayorovp
                                                  18.09.2017 18:54

                                                  Это зависит от требуемого интерфейса. У Function метод называется apply, а у Callable — call. В своем интерфейсе можно объявить любой метод с любой сигнатурой.


                                  1. Druu
                                    18.09.2017 17:31
                                    +1

                                    Какой смысл в лямбдах, если надо для каждой велосипедить интерфейс? :)
                                    О том речь и шла — в джаве checked exceptions реализованы крайне неудобно. Потому не взлетели. А если сделать так, чтобы взлетели, то оно будет как монада :)


                                    1. TheShock
                                      18.09.2017 17:35

                                      Следуя вашей логике, Хаскель не взлетел, потому что неудобно, а если сделать так, чтобы взлетел, то будет как Джава.


                                      1. raveclassic
                                        18.09.2017 17:38

                                        Ну, ADT есть не только в хаскеле


                                        1. TheShock
                                          18.09.2017 17:43
                                          -1

                                          Та не, я более абстрактно шутил. Хаскель менее популярный, чем Джава, а следуя той логике, чтобы сделать его популярным — нужно превратить в Джаву.


                                      1. Druu
                                        18.09.2017 17:45
                                        +1

                                        > Следуя вашей логике, Хаскель не взлетел, потому что неудобно

                                        Нет, это не по моей логике. По чьей-то чужой, возможно.


                                    1. mayorovp
                                      18.09.2017 17:38
                                      +1

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


                                      Да, "классическое" ООП многословно, это его недостаток, который никто не скрывает — и именно потому в такие классические ООП-языки как C++, Java и C# активно добавляют элементы ФП (те же лямбды). Но к исключениям против монад это отношения не имеет.


                                      1. Druu
                                        18.09.2017 17:46
                                        -1

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

                                        Да это просто пример. У checked exceptions много разных проблем из-за врожденной кривизны.


                                        1. mayorovp
                                          18.09.2017 18:57
                                          -1

                                          Да это просто пример. У checked exceptions много разных проблем из-за врожденной кривизны.

                                          Аргументируйте, пожалуйста.


                                          1. 0xd34df00d
                                            18.09.2017 19:14

                                            Я могу написать функцию, которая принимает функцию, бросающую произвольный список исключений, и возвращает функцию, которая бросает тот список исключений плюс (или минус) ещё пара моих типов?


                                            1. mayorovp
                                              18.09.2017 20:51

                                              Я не спорю, в рамках ФП все является либо функцией, либо значением, либо и тем и другим — и исключения тут никаким боком не нужны.


                                              Но я спрашивал про недостатки checked exceptions в рамках подхода ООП. Где нет никаких функций, возвращающих функции.


                                              1. Druu
                                                19.09.2017 04:36
                                                -1

                                                Проблема состоит во взаимодействии двух вещей:
                                                1. В случае ООП мы работаем со стейтом, вместо того, чтобы прокидывать аргументы явно
                                                2. Проверяемые исключения не являются first class citizen

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


                                                1. 0xd34df00d
                                                  19.09.2017 06:28

                                                  Люто не хватает анонимных типов-сумм, кстати. По крайней мере, мне сходу неочевидно, как взять две функции с возвращаемым типом Either a' a и Either b' b и построить их монадическую композицию в тип Either (a'|b') b.


                  1. 0xd34df00d
                    18.09.2017 18:57

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


                    1. mayorovp
                      18.09.2017 18:58

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


                      1. 0xd34df00d
                        18.09.2017 19:15

                        Я запутался. Можете для оголтелых ФПистов ссылкой ткнуть?


                        1. mayorovp
                          18.09.2017 20:53

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


                          Дальше начался холивар "исключения — это плохо, но мы никому не расскажем почему именно".


                          1. 0xd34df00d
                            18.09.2017 20:57

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


                            И таки да, мне самому интересно, почему checked exceptions в джаве не взлетели.


                            1. mayorovp
                              18.09.2017 21:02

                              Ну, я упростил пересказ. Полная ветка выглядит пока что именно так: исключения — плохо, потому что их нет в сигнатуре, а checked exceptions — еще хуже, но почему мы не расскажем.


                            1. TheShock
                              18.09.2017 21:10
                              -1

                              по взгляду на функцию нельзя понять, что она возвращает


                              Вон ниже про ФП такой диалог:
                              — Вот только эксепшены не объявлены в сигнатуре функции, и о наличии «красных путей» можно только догадываться.
                              — Это было бы аргументом если сторонники ФП брали за правило всегда объявлять сигнатуры своих функций. Вот только…
                              — В статически-типизированных все хорошо с сигнатурами, просто их можно опускать, потому что вывод типов работает лучше

                              Так в чем разница? В том же C# точно так же на этапе компиляции можно узнать обо всех потенциальных ошибках.


                              1. lair
                                18.09.2017 21:44
                                +3

                                В том же C# точно так же на этапе компиляции можно узнать обо всех потенциальных ошибках.

                                Только для известных реализаций, не для интерфейсов. К сожалению. И не для переданных делегатов.


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


                              1. 0xd34df00d
                                19.09.2017 00:03

                                Так в чем разница?

                                Разница между отсутствием и присутствием при необязательном объявлении?


                                В том же C# точно так же на этапе компиляции можно узнать обо всех потенциальных ошибках.

                                Я последний раз C# тыкал очень давно. Как там это можно узнать?


                                Кроме того, есть разница между «можно узнать» и «тайпчекер заставит по умолчанию».


                1. AndreyRubankov
                  18.09.2017 16:36
                  +1

                  В коде с монадами нам не надо при этом создавать ветки в вызывающем коде, а в коде с ООП — нужно было бы.
                  Вообще-то нет, не обязательно.

                  Метод, который выполняет какое-то действие кидает эксепшн (переходит на «красные пути»), который прерывает дальнейшее выполнение кода.

                  Функция validateRequest явно не занимается формированием ответа на, допустим, http запрос, значит она отдает ответ, который просто переводит исполнение на «красные пути», и где-то тааам, в конце кто-то примет этот ответ и сделает из него ответ на запрос.

                  в случае с эксепшеном:… и кто-то перехватит эксепшн и сделает из него ответ на запрос.

                  – суть та же, но способ реализации другой.

                  Вопрос: почему автор не использовал эксепшены и намеренно изуродовал ООП код? =)


                  1. raveclassic
                    18.09.2017 16:48

                    Метод, который выполняет какое-то действие кидает эксепшн (переходит на «красные пути»), который прерывает дальнейшее выполнение кода.

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


                    1. mayorovp
                      18.09.2017 16:57

                      Это было бы аргументом если сторонники ФП брали за правило всегда объявлять сигнатуры своих функций. Вот только ...


                      … ни одна функция из определенных в обсуждаемом тексте в примерах на F# не имеет явной сигнатуры! Тут не то что о наличии "красных путей", тут в принципе о типах аргументов и возвращаемых значений надо догадываться!


                      И самое интересное — это таки работает. А значит, "нужно догадываться" еще не означает "трудно разобраться". И с исключениями можно поступить так же.


                      Тем более, для сложных случаев есть документация.


                      А еще в Java есть checked exceptions.


                      1. marshinov Автор
                        18.09.2017 16:59

                        … ни одна функция из определенных в обсуждаемом тексте не имеет явной сигнатуры! Тут не то что о наличии «красных путей», тут в принципе о типах аргументов и возвращаемых значений надо догадываться!

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


                        1. mayorovp
                          18.09.2017 17:01
                          +1

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


                          1. raveclassic
                            18.09.2017 17:05
                            +1

                            А как вы (окей, без checked exceptions) поймете, свалится ли у вас функция в рантайме, если в ее типе нет на это указания?


                            1. mayorovp
                              18.09.2017 17:18

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


                              Но если мне нужно узнать про конкретную функцию — а посмотрю ее исходный код (если она моя) или документацию (если она сторонняя).


                              1. raveclassic
                                18.09.2017 17:24
                                +1

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

                                Сложно, несложно, это уже человеческий фактор.


                                Но если мне нужно узнать про конкретную функцию — а посмотрю ее исходный код (если она моя) или документацию (если она сторонняя).

                                Ну а можно этого не делать и положиться на компилятор.


                                1. mayorovp
                                  18.09.2017 21:09

                                  Идея исключений — в удешевлении общего случая жертвуя частным.


                                  Так вот: в общем случае мне не нужно смотреть какие именно исключения кидает код.


                              1. 0xd34df00d
                                18.09.2017 19:11
                                +1

                                Но если мне нужно узнать про конкретную функцию — а посмотрю ее исходный код (если она моя) или документацию (если она сторонняя).

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


                      1. Druu
                        18.09.2017 17:00

                        > ни одна функция из определенных в обсуждаемом тексте в примерах на F# не имеет явной сигнатуры!

                        Явной не имеет, но по факту сигнатура есть. Вы увидите ее, если наведете мышкой на ф-ю в ИДЕ, ну и компилятор вам в любом случае напомнит, если вы вдруг не обработаете ошибку.

                        А явная сигнатура кому нужна-то, по факту?


                        1. 0xd34df00d
                          18.09.2017 19:13

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


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


                          1. Druu
                            18.09.2017 19:21
                            +1

                            > Ну и, кроме того, не всегда компилятор может вывести тип

                            В этом случае и проблем с отсутствием сигнатуры не будет, очевидно :)


                      1. raveclassic
                        18.09.2017 17:04
                        +1

                        А сигнатура потому-что выводится. И если у вас у значения вывелся тип Option<number>, вам компилятор не даст работать с ним как с number.


                        Ну и не все на джаве пишут.


    1. 0xd34df00d
      18.09.2017 18:46
      +2

      Про лисп у меня было ровно то же замечание, да.


      Да и вообще, безусловно, что монады, что CPS какой-нибудь, что вообще вся иерархия тайпклассов из Control.* (arrow, arrowplus, monad, monadplus, monadzero, alternative, вот это всё), это всё тоже вполне себе паттерны. Просто математически чуть более обоснованные, чуть более гранулярные и потому чуть более композабельные.


  1. AlexPublic
    18.09.2017 05:07
    +5

    Было бы действительно интересно почитать статью на тему паттернов проектирование в мире функционального программирования. Но к сожалению в данной статье ничего подобного не видно. Здесь имеется только обзор всем известных основ ФП и всё. Вот например какие паттерны мне надо применить при проектирование GUI библиотеки? Человек из ООП мира ответит сразу (ну или может пойдёт в начале полистает всем известную книжку и ответит потом). А что скажет сторонник ФП? «Использовать монады и функции»? Ну так это аналогично ответу «использовать классы» и ничего не говорит об архитектуре. В отличие от таких понятий как например «паттерн Посетитель» и т.п.

    В общем я сильно порадовался названию статьи и полностью разочарован её содержимым. Буду ждать следующую попытку от адептов ФП…


    1. Druu
      18.09.2017 07:04
      -2

      > А что скажет сторонник ФП?

      В ФП нет паттернов, так что вопрос смысла не имеет.


      1. alkozko
        18.09.2017 08:05
        +4

        Паттерны — это не только то, что написано в книжке GoF, а просто набор общепринятых подходов к решению некоторого класса задач.
        Так почему паттернов ФП нет? там все задачи уникальные? Или может в ФП не принято решать одну и туже задачу как все и надо обязательно сделать свой велосипед (сомневаюсь, но вдруг)? Или какие-то другие причины?
        Мне правда интересно


        1. BlessMaster
          18.09.2017 08:22
          -1

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


        1. Druu
          18.09.2017 08:47

          > Так почему паттернов ФП нет? там все задачи уникальные?

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


          1. AndreyRubankov
            18.09.2017 11:44
            +3

            Паттерны, как уже говорилось, это не про GoF. Паттерны – это про подходы к написание и структурированию кода.

            Если какой-то подход имеет строго выраженную идею, структуру и применяется для достижения конкретных целей – это паттерн.

            Каррирование, Промисы, Коллбеки – это все паттерны.


            1. Druu
              18.09.2017 13:04
              -4

              > Если какой-то подход имеет строго выраженную идею, структуру и применяется для достижения конкретных целей – это паттерн.

              Я и говорю, в этом понимании в ФП нету паттернов.

              > Каррирование, Промисы, Коллбеки – это все паттерны.

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


              1. TheShock
                18.09.2017 15:20
                -3

                Я и говорю, в этом понимании в ФП нету паттернов.

                О! А вот и целевая аудитория статьи, я выше о них смеялся.

                Если в какой-нибудь парадигме нету паттернов, то это значит что она настолько ненужна в практической разработке, что под нее даже не пишут никакую теорию. Вы считаете, что это об ФП?


                1. Druu
                  18.09.2017 15:58
                  +1

                  > то это значит что она настолько ненужна в практической разработке, что под нее даже не пишут никакую теорию

                  Под ФП есть огромное количество теории. Просто паттернов в этой теории нету. Там есть монадные трансформеры, стрелки, а также зигохистоморфные препроморфизмы с пределами и копределами. А паттернов — вот нет, не завезли :)
                  И, да, эту теорию можно применять на практике. Например, благодаря этой теории я знаю, что могу использовать синтаксис генераторов в js с любой монадой, в которой fmap применяет свой аргумент не более раза :)


                  1. TheShock
                    18.09.2017 16:04
                    -1

                    То есть паттерны есть, но их нету? И вот пример паттерна, но это не паттерн, потому что это ФП, а не богомерзкое ООП, а если хипстеры узнают, что я признал это паттерном — засмеют на афтерпати.


                    1. Druu
                      18.09.2017 16:07

                      > То есть паттерны есть, но их нету?

                      Давайте определимся с терминологией, чтобы друг друга понимать. Я под «паттерном» понимаю нечто, что, в частности, имеет четкое назначение. Это соответствует общепринятому смыслу, вкладываемому в термин «паттерн». В этом смысле паттернов в ФП нету, потому как все конструкции слишком универсальны. Если же под «паттерном» подразумеваются какие-то «классические» приемы, которые туда-сюда ходят и применяется в том или ином контексте — то такое, безусловно, есть.


                      1. TheShock
                        18.09.2017 16:30
                        +1

                        Давайте определимся с терминологией

                        Давайте. По вашему, паттерны ФП внезапно перестают быть паттернами в какой-то момент универсальности? А насколько именно они должны быть «универсальными», чтобы перестать быть «паттерном»? «зигохистоморфный препроморфизм» — уже не такой и универсальный, он уже стал паттерном? А паттерн «Интерфейс» внезапно перестает быть паттерном? И все просто потому вы хотите, чтобы в священном ФП не было того же, что есть в отврательном ООП?


                        1. Druu
                          18.09.2017 16:38
                          -1

                          > По вашему, паттерны ФП внезапно перестают быть паттернами в какой-то момент универсальности?

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

                          > А паттерн «Интерфейс» внезапно перестает быть паттерном?

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

                          > И все просто потому вы хотите

                          Я ничего не хочу. Мне лично без разницы, как и что вы будете называть. Кажется, я достаточно ясно раскрыл свою точку зрения, а какими конкретно словами она выражается — для меня совершенно несущественно. Если фп-конструкции удовлетворяют вашему пониманию термина «паттерн» — ну так пусть удовлетворяют, я не против. Сущность объекта не зависит от того, как этот объект называют, пусть хоть «тирьямпампация» :)


                          1. vbif
                            18.09.2017 16:42
                            +1

                            То есть у монады нет назначения? У объекта первого класса нет назначения?


                            1. Druu
                              18.09.2017 16:44
                              -2

                              > То есть у монады нет назначения?

                              Именно так. У _конкретной_ монады назначение есть. У монады в общем (какой-то, неизвестно какой) — назначения нет. Это слишком общий объект, сказать про что-то «оно монада» — практически, ничего полезного не сказать. Что с этим чем-то будут делать — вывести уж точно нельзя.

                              > У объекта первого класса

                              Монада как бы не особо объект первого класса да и вообще не программный объект. Ее программным объектом, конечно, можно сделать (запаковать нужные ф-и в класс, например), но этого не требуется. Слово «конструкция» на мой взгляд наиболее точно отражает положение вещей. Есть некая конструкция (которая может быть реализована как угодно), и если она обладает определенными особенностями, то это — монада.


                              1. vbif
                                18.09.2017 16:51

                                Организация последовательных вычислений — чем не назначение?


                                1. Druu
                                  18.09.2017 16:54
                                  +2

                                  Тем, что этим занимается только некоторый класс монад, но далеко не все. Условно говоря, у нас есть несколько широких классов монад: монады контейнерные (типа List), монады, протаскивающие контекст (State), монады, управляющие потоком управления (вроде Maybe) ну и эзотерические, имя им легион (Cont, например)


                                  1. TheShock
                                    18.09.2017 16:59
                                    +2

                                    Ну ладно. Тогда давайте попробуем так.
                                    Монада Maybe, монада Option — это паттерны? Они четко соответствуют вашему определению: «нечто, что, в частности, имеет четкое назначение»


                                    1. Druu
                                      18.09.2017 17:01
                                      +1

                                      > Монада Maybe, монада Option — это паттерны?

                                      Да, конкретные монады (из классических, то есть у которых есть какая-то общепринятая семантика, как у той же Maybe), наверное, можно считать паттернами.

                                      Однако, почему тогда не считать паттерном в ООП какой-то _конкретный_ класс? Так ведь не поступают, по каким-то причинам.


                                      1. TheShock
                                        18.09.2017 17:20
                                        -1

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


                                        1. Ingas
                                          18.09.2017 19:10
                                          +1

                                          Джентльмены, откройте глаза!


                                          Эта серия лекция популяризировала паттерн Railway [Oriented] Programming.


                                          Гугль выдает: Результатов: примерно 15 600 000 (0,53 сек.)


                                          1. myrslok
                                            19.09.2017 21:51

                                            Не все 15 миллионов по делу. Большая часть результатов относится к каким-то железнодорожным делам. Если искать то же в кавычках, то результатов становится примерно в тысячу раз меньше. Думаю, вы преувеличиваете известность этого словосочетания.


        1. marshinov Автор
          18.09.2017 11:26

          Думаю, дядя Боб частично ответил на ваш вопрос.


    1. marshinov Автор
      18.09.2017 08:17

      Вот например какие паттерны мне надо применить при проектирование GUI библиотеки?
      Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.


      1. Druu
        18.09.2017 08:41
        +3

        «react+redux» не имеет никакого отношения к ФП.


        1. marshinov Автор
          18.09.2017 08:50
          -3

          Да? Абрамов несколько иного мнения 29:00.


          1. Druu
            18.09.2017 09:12
            +3

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


            1. marshinov Автор
              18.09.2017 10:41
              +3

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


              1. RidgeA
                18.09.2017 11:09
                +3

                Разве чистые функции — это единственная концепция ФП?


                1. raveclassic
                  18.09.2017 12:31
                  +2

                  Зачем же тогда писать "никакого"?


                  «react+redux» не имеет никакого отношения к ФП.


              1. Druu
                18.09.2017 13:08

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

                Это были бы принципы функционального программирования, если бы на этом подходе строился поток управления в целом. Но 90% логики остается в грязных функциях с сайд-эффектами, а в редьюсерах — мелочь. Редакс ведь не про редьюсеры, редьюсеры можно исопльзовать и без редакса. Он про стор (который работает грязно) и про action creators и их middleware (которые работают грязно). В редаксе используются чистые функции? Используются, с этим не поспоришь. Но они в той же мере абсолютно везде используются. Важно — как используются. Если подход основан на использовании чистых ф-й — можно говорить об ФП. Если подход основан на использовании грязных функций (как в случае редакса) и там где-то применяются чистые функции — то тут об ФП говорить рановато.


          1. TheShock
            18.09.2017 15:37
            +1

            Абрамов несколько иного мнения 29:00.

            о! Группи Абрамова! Если Дан что-то сказал — надо отключить свой мозг и принять за истину! Думать не нужно, нужно помнить заветы Абрамова.

            image

            У Редакса есть элементы ФП. Код там процедурный. Диспатч — процедурный. В языке С sqrt — тоже чистая функция, которая не изменяет свой аргумент, но от этого весь язык не становится функциональным


            1. marshinov Автор
              18.09.2017 16:27

              Вы вырываете из контекста. Я сослался на мейнтейнера. У меня к redux много вопросов, особенно к процедурному стилю. Приходится писать очень много boilerplate или не идиоматичный redux'у код (проблема с обучением новых сотрудников).

              «react+redux» не имеет никакого отношения к ФП.

              Вы и другие участники ветки уже признали, что отношение есть: react / redux используют элементы ФП. Очевидно, что в них много побочных эффектов, потому что в web-ориентированном UI по-другому быть вообще не может.

              В JS идеи начали просачиваться в том числе из-за проблем с data-flow и callback hell. Оказалось, что есть продолжения и с ними все попроще. А вот монады в JS пока никому не нужны, потому что никто целей чистоты, как в хаскеле не ставит.

              Есть проблема — асинхронный код, который есть в 90% приложений и необходимость его как-то поддерживать и развивать. Есть решение — промисы. Копнули чуть дальше, оказывается еще и чистые функции есть. Подход прагматичный и инструментальный.


              1. TheShock
                18.09.2017 16:32

                Ну а вы признаете, что были неправы тут?

                Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.

                Это не «функциональное программирование», пусть и с элементами.
                Я сослался на мейнтейнера

                Который отличный маркетолог, эвангелист, но плохой источник истины.


                1. marshinov Автор
                  18.09.2017 17:01
                  -2

                  По-моему, вы просто переходите на личности. Ну не нравится он вам и все. Остальное уже не важно, главное похейтить. Вот вам не нравится его pr. Вы делаете pr на хейте Абрамова. У него хотя-бы не вторично.


                  1. TheShock
                    18.09.2017 17:10

                    Погодите, а где я перешел на личность Абрамова? Тем, что я не считаю кого-то источником истины — это «переход на личности»? Что-то вроде «Ты сказал, что бог не всемогущ. Ты — богохульник и еретик!». Вы прям истинный религиозный фанатик.

                    Я никогда не высмеивал Абрамова. Я высмеиваю только его фанаток-программистов, которые заменяют свой мозг его речами.

                    И, кстати, так проигнорировали главный вопрос. Потому повторюсь:

                    вы признаете, что были неправы?


                    1. marshinov Автор
                      18.09.2017 18:51
                      -1

                      «react+redux» не имеет никакого отношения к ФП
                      и
                      У Редакса есть элементы ФП. Код там процедурный. Диспатч — процедурный. В языке С sqrt — тоже чистая функция, которая не изменяет свой аргумент, но от этого весь язык не становится функциональным

                      Спорите со мной уже вы про Абрамова. А оригинальный вопрос был:
                      Вот например какие паттерны мне надо применить при проектирование GUI библиотеки?

                      React и Redux — это примеры применения элементов ФП в UI-библиотеках. Это максимально точный ответ. В комментариях мне пишут, что «react и redux не имеют отношения» а потом «содержат элементы».

                      Содержать элементы уже != иметь отношение?

                      Очевидно, что react и redux — не хаскель. Это никто утверждать не будет. Кстати, если перейти на ссылку с видео то там он утверждает ровно тоже самое «redux использует идеи функционального программирования. Они не новые».

                      Суть вопроса «какие есть примеры ФП в UI» уже утратила актуальность. В ветке мы обсуждаем недобросовестный маркетинг и девочек.

                      вы признаете, что были неправы?

                      В чем конкретно я не прав? Может и признаю, пока не понял.


                      1. TheShock
                        18.09.2017 19:03

                        Процитирую вас:

                        Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.


                        Но это, как мы уже выяснили, не функциональное, а так, моментами использует функциональное.

                        Вопрос был про «паттерны создания UI на ФП», а вы в ответ привели процедурную либу с некоторыми функциональными элементами и еще ошибочно назвали ее функциональной.

                        Другой момент — был задан вопрос «какие есть практики создания UI на ФП». Все, что прилетело в ответ — «вот используйте библиотеку» (это просто фейспалм). Данное грустное обстоятельство указывает на слабую теоретическую базу.


                  1. TheShock
                    18.09.2017 17:18

                    Вы делаете pr на хейте Абрамова

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


      1. AlexPublic
        18.09.2017 20:04

        А можно увидеть именно сами паттерны (словесное описание и т.п.), а не какую-то библиотечку из дикого JS мира? )


    1. KoCMoHaBT61
      18.09.2017 11:18
      -2

      Да не то, что паттерны!
      Как вообще можно проектировать на ФП, вот в чём вопрос.


      1. 0xd34df00d
        18.09.2017 19:36
        -1

        Пишете сигнатуры функций, добиваетесь, чтобы тайпчекалось — всё, спроектировали.


        1. TheShock
          18.09.2017 19:38
          -2

          А через три месяца выбрасываете неподдерживаемый лапшекод и возвращаетесь в Джаву.


          1. 0xd34df00d
            18.09.2017 19:44
            -2

            Где через три месяца выбрасываете неподдерживаемый императивный лапшекод и возвращаетесь в хаскель.


            1. TheShock
              18.09.2017 19:50

              Где через три месяца снова выбрасываете весь код и идете работать грузчиком.

              Тут спрашивают: «как писать поддерживаемые программы на фп», на что получают от вас ответ из разряда: «ну пишите как-то, чтобы оно скомпилировалось».

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

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


              1. 0xd34df00d
                18.09.2017 20:04
                +2

                Спрашивали в контексте паттернов. Никогда не приходилось поддерживать неподдерживаемый код, где синглтон на визиторе и фабрикой с инверсией зависимостей погоняет?


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


                <зануда>Ну и да, вопрос был не о том, как писать поддерживаемый код, а «как вообще можно проектировать на ФП».


                1. TheShock
                  18.09.2017 20:15

                  «как вообще можно проектировать на ФП».

                  Ну смотрите — на вопрос «как вообще можно управлять самолетом?» можно просто ответить: «берете штурвал и двигаете им, пока не прилетите на место», а можно дать ссылку на пару талмудов об управлении самолетом.

                  Никогда не приходилось поддерживать неподдерживаемый код, где синглтон на визиторе и фабрикой с инверсией зависимостей погоняет?

                  Конечно стыкался с таким. Со всяким стыкался. Но все-равно, у тебя есть куча хорошей теории о проектировании крупных систем, откуда можно почерпнуть множество разномастного опыта. Да, некоторые люди будут иметь культ карго, некоторые вообще все неправильно поймут. Да и без какого-либо опыта сложно понять всю эту теорию. Но с ФП — тебя просто садят в горящий самолет и кричат: «лети».

                  пишете сначала модуль ...

                  Вы же понимаете, что скорее описываете «как писать», а не «как проектировать»?


                  1. 0xd34df00d
                    18.09.2017 23:56

                    Ну смотрите — на вопрос «как вообще можно управлять самолетом?» можно просто ответить: «берете штурвал и двигаете им, пока не прилетите на место», а можно дать ссылку на пару талмудов об управлении самолетом.

                    Учитывая коннотации, вопрос скорее был «вот в автомобиле штурвал есть, а в этих ваших самолётах как управлять ими?».


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

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


                    Но с ФП — тебя просто садят в горящий самолет и кричат: «лети».

                    Почему? На крупном масштабе тот же подход, хоть ООП, хоть ФП.


                    Вы же понимаете, что скорее описываете «как писать», а не «как проектировать»?

                    Нет. Бьёте на модули, как обычно, а на уровне модулей уже вот как я описываю.


                1. KoCMoHaBT61
                  19.09.2017 06:24
                  -4

                  пишете сначала модуль, у него продумываете...

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


                  1. 0xd34df00d
                    19.09.2017 06:30
                    +3

                    Вы, конечно, извините, но это не имеет смысла вообще.


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


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


                    1. KoCMoHaBT61
                      19.09.2017 09:23
                      -5

                      То есть — у тебя есть анализ в виде модульности, но нет синтеза вообще. И эту, не знаю, «хэшмапу» (которая, на самом деле, не хэшмапа, а «контейнер очень важных данных») ты зовёшь не напрямую, а через макаронины идиотских экспортов.
                      Собственно-говоря — так оно и выглядит в реале, но хотелось верить, что есть кто-то, кто знает как проектировать функционально.


                      1. 0xd34df00d
                        19.09.2017 19:52

                        То есть — у тебя есть анализ в виде модульности, но нет синтеза вообще.

                        Синтеза чего?


                        И эту, не знаю, «хэшмапу» (которая, на самом деле, не хэшмапа, а «контейнер очень важных данных») ты зовёшь не напрямую, а через макаронины идиотских экспортов.

                        Эта фраза тоже не имеет особого смысла. Когда мне нужна хешмапа, я делаю import qualified Data.HashMap.Strict as M (ну или Lazy, зависит от ситуации). Когда мне нужно её создать, я делаю M.fromList, или M.singleton, или mempty, или вроде того. Ну и так далее.


                        Собственно-говоря — так оно и выглядит в реале, но хотелось верить, что есть кто-то, кто знает как проектировать функционально.

                        У меня ощущение, что у вас уже есть ответ, а всё, что вам говорится, вы под этот ответ подгоняете.


                        1. KoCMoHaBT61
                          20.09.2017 11:26
                          -1

                          Анализ и Синтез это термины логики. Анализ — это разложение чего-либо на составные элементы, Синтез это сборка чего-то из элементов, которые получены в результате Анализа.
                          Что-же касается Хэшмапы.
                          Тебе не нужна Хэшмапа, но ты этого не понимаешь!
                          Тебе нужен контейнер для неких данных.
                          Возвращаясь к вопросу проектирования — для каких данных тебе нужен контейнер, как эти данные связаны с другими данными, какое место занимают эти данные во множестве других данных. На эти вопросы ответа нет, потому, что данные ты не формализовал и не обобщил. А из этого ответа, через долгое (или недолгое, как повезёт) исходит решение, какой конкретно контейнер ты должен применить в данном случае.
                          Модули — это не ответ. Модульность даёт некоторое смысловое разбиение предметной области на группы. Ещё большее и удачное разбиение (Анализ) даёт ОО. Но мы говорим о ФП, причём в теоретическом смысле, не вдаваясь в подробности.


                          1. lair
                            20.09.2017 11:51
                            +1

                            Ещё большее и удачное разбиение (Анализ) даёт ОО.

                            Обосновать можете?


                          1. 0xd34df00d
                            20.09.2017 18:49
                            +1

                            Анализ — это разложение чего-либо на составные элементы, Синтез это сборка чего-то из элементов, которые получены в результате Анализа.

                            Анализ в ФП в ваших терминах тоже есть.


                            Тебе не нужна Хэшмапа, но ты этого не понимаешь! Тебе нужен контейнер для неких данных.

                            На каком-то уровне мне и контейнер не нужен, а нужно решить бизнес-задачу.


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

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


                            Модули — это не ответ. Модульность даёт некоторое смысловое разбиение предметной области на группы. Ещё большее и удачное разбиение (Анализ) даёт ОО.

                            Почему?


                  1. mayorovp
                    19.09.2017 06:34
                    +2

                    Модуль — это прежде всего способ организации кода. Модули не имеют отношения к используемой парадигме.


                  1. lair
                    19.09.2017 11:31
                    +1

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

                    Ээээ, что? Сокрытие информации никто не отменял, оно не только к состоянию относится.


                    1. KoCMoHaBT61
                      19.09.2017 14:04
                      -1

                      А нету информации, нечего скрывать.
                      Есть функция, которая принимает нечто и нечто другое выдаёт.


                      1. lair
                        19.09.2017 14:57
                        +1

                        Функция — это тоже информация.


                        1. KoCMoHaBT61
                          19.09.2017 18:47

                          Не может человеческий язык состоять из одних глаголов.


                          1. lair
                            19.09.2017 18:54
                            +2

                            Не может, и что? К тому, что функция — это информация, и иногда нуждается в сокрытии, это отношения не имеет.


                          1. Yuuri
                            20.09.2017 11:53

                            1. KoCMoHaBT61
                              20.09.2017 12:43
                              -1

                              Отличная статья, кстати, если-бы там кругом не упоминалась богомерзкая Java.
                              Более забавная статья есть на хабре:
                              habrahabr.ru/post/161885
                              Только она больше вопросов порождает, чем отвечает на вопрос «как проектировать на ФЯ».


                              1. Yuuri
                                20.09.2017 13:59

                                Лично я не понимаю этот вопрос, он слишком абстрактный. А как проектировать на не-ФЯ?


                                1. KoCMoHaBT61
                                  20.09.2017 14:24

                                  Ну, Гради Буч, Румбаух, Шлаер с Меллором, Якобсон много про ОО писали.


                      1. 0xd34df00d
                        19.09.2017 19:56
                        +1

                        Когда я делаю M.fromList [...], я получаю M.HashMap K V. И вот кишки этой хешмапы имеет смысл скрывать, например. Равно как и имеет смысл скрывать вспомогательные функции, если вы пишете библиотеку и гарантировать сохранность этих функций с точки зрения внешнего пользовательского API вы не хотите.


                        1. KoCMoHaBT61
                          20.09.2017 11:32
                          -2

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

                          Реальные задачи немного другие.
                          Вот например, постановка задачи про «Грабить Корованы» — до хэшмапы там как до эльфов лесом.

                          Если проектировать «Корованы» с Функциональным подходом, то мы быстренько наталкиваемся на такую проблему, что нам тяжело описать «Корован» и очень легко описать операцию «Грабить».


                          1. mayorovp
                            20.09.2017 11:40
                            +1

                            Хешмапа — это был ваш пример.


                            1. KoCMoHaBT61
                              20.09.2017 12:25
                              -1

                              Хэшмапа это не мой пример, смотри сообщение выше.


                          1. Druu
                            20.09.2017 12:56

                            > что нам тяжело описать «Корован»

                            Почему же тяжело? Прикидываем данные, которые должны описывать караван, и пишем соответствующий тип.


                            1. KoCMoHaBT61
                              20.09.2017 13:03
                              -3

                              В ФП у тебя минимум два «Корована»!
                              Первый это Неограбленный_Корован, а второй это Ограбленный_Корован.
                              И нет никакого жёстко заданного «типа».


                              1. lair
                                20.09.2017 13:12
                                +1

                                Э, правда? Из какого же определения ФП это следует?


                              1. Druu
                                20.09.2017 13:19

                                > И нет никакого жёстко заданного «типа».

                                Как это нет? Есть, конечно. Как же компилятор типы проверяет, если их нет? :)

                                > В ФП у тебя минимум два «Корована»!

                                Берете Clean (это такой чисто функциональный ЯП), объявляется тип вашего каравана uniqueness и будет только один караван! Чудо! :)


                                1. 0xd34df00d
                                  20.09.2017 18:52

                                  Скоро можно даже будет не брать Clean, линейные типы почти завезли в хаскель.


                              1. raveclassic
                                20.09.2017 13:19
                                +3

                                type Caravan = {
                                  gold: number
                                };
                                const rob = (caravan: Caravan): Caravan => ({
                                  gold: 0
                                });

                                Парам парам пам. Caravan — жестко заданный тип.


                              1. 0xd34df00d
                                20.09.2017 18:52
                                +1

                                В ФП у тебя минимум два «Корована»!
                                Первый это Неограбленный_Корован, а второй это Ограбленный_Корован.

                                Если старый корован вам не нужен, его GC подберёт. Если нужен — ну, так он вам и так нужен. Персистентность — это зачастую преимущество.


                                И нет никакого жёстко заданного «типа».

                                В дополнение к коду на другом языке рядом:


                                data Caravan = Caravan {
                                        name :: String,
                                        camelsCount :: Int,
                                        gold :: Double
                                    }
                                
                                robCaravan :: Caravan -> Caravan
                                robCaravan c = c { gold = 0 }


                                1. KoCMoHaBT61
                                  20.09.2017 19:00

                                  Персистентность ведёт к потере стейтлесс преимущества ФП.


                                  1. 0xd34df00d
                                    20.09.2017 19:17

                                    Но как? Если что.


                                    1. KoCMoHaBT61
                                      20.09.2017 22:39

                                      Хоть коммент удаляй — масло-масляное.
                                      Ладно, напишу свой вариант:

                                      rob({caravan,_Name,_CamelsCount,_Gold}) ->
                                          {caravan};
                                      rob(_) ->
                                          ok.
                                      


                                      1. 0xd34df00d
                                        21.09.2017 01:05

                                        Простите моё невежество, это какой язык и что оно делает?


                                        1. KoCMoHaBT61
                                          21.09.2017 06:17

                                          Эрланг. В котором, кстати, есть паттерны в виде ОТП.


                          1. raveclassic
                            20.09.2017 13:04

                            del


      1. Druu
        19.09.2017 04:24

        Точно так же, как и не на ФП. Все те же способы прекрасно работают.


  1. Druu
    18.09.2017 07:07
    +4

    > Обратите внимание, сигнатура int -> int -> int не содержит скобок не случайно.

    Она не содержит скобок из-за того, что скобки там ничего не значат, в силу правоассоциативности "->".


    1. myrslok
      19.09.2017 11:56

      Скобки не "ничего не значат", а подразумеваются, причем определенным образом. В (int -> int) -> int их нельзя опустить.


      1. raveclassic
        19.09.2017 12:07

        скобки там ничего не значат


  1. Alex_T666
    18.09.2017 07:08
    +5

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

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


  1. znsoft
    18.09.2017 08:18
    +2

    мне, незнающему с какой стороны посмотреть на этот новомодный ФП статья дала понимание того, что коллбэки со времен asm x86 переросли в отдельную парадигму программирования


    1. BlessMaster
      18.09.2017 08:25
      -2

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


  1. aslepov78
    18.09.2017 08:22
    +2

    Почему на рисунке «Lisp programmers» выше «Haskell programmers»?


  1. DmitryKoterov
    18.09.2017 08:33

    Cherry — это на практике черешня, а не вишня. (Так же как dinner — это на практике ужин, а не обед.)


    1. marshinov Автор
      18.09.2017 08:53

      А черешня — не вишня? Это самая важная деталь, из-за которой в переводе весь смысл метафоры потерян?:)


      1. DmitryKoterov
        18.09.2017 12:55

        Ну это все троллинг, конечно — извините. Мне просто подумалось, что если cherry — это вишня, то композиция функций — это фрактал, почему бы и нет. (Да, я понимаю, что это перевод. Но.)

        Если говорить про метафоры, то вот еще проскальзывает в статье избитая тема: «функция в ФП- это объект первого порядка». Ну на практике (такой же, как с вишней-черешней) это не совсем тот «первый порядок», который имелся в виду, когда Чёрч придумывал лямбда-исчисление. Вот что, например, имеется в виду под «настоящим первым порядком» с практической точки зрения: habrahabr.ru/post/322052

        True = t => f => t
        False = t => f => f

        И дальше все остальное через это выводится.

        А то, что коллбэки можно передавать в параметрах или что функции комбинировать, так это как черешня-вишня: вроде бы и то же, а вроде бы и вообще нет.


        1. marshinov Автор
          18.09.2017 13:53

          Я целенаправленно скачал лекции по лябда-исчислению и даже посмотрел первые три. Кому как, конечно, но мне с вишенками и яблочками «заходит» лучше. Для математики такие аналогии смехотворны. Но мы же программы пишем, а не теоремы доказываем.


  1. injecto
    18.09.2017 08:53
    +4

    Вместо этого популизма лучше почитать что-то более адекватное.


    1. marshinov Автор
      18.09.2017 10:41

      Кому лучше и почему?


    1. mibori
      18.09.2017 18:41
      +1

      Забавно, что сам Евгений, по прошествию стольких лет, сейчас кодит гораздо больше на Java, чем на Haskell )


  1. KoCMoHaBT61
    18.09.2017 10:52
    -3

    С самого начала отличный пример:
    (apple -> banana) (banana -> cherry)

    apple, banana, cherry — это разные сущности. У них абсолютно разные аттрибуты, параметры и инварианты, но — функциональщики с лёгкостью необыкновенной преобразовывают одно в другое. При этом они даже не могут объяснить (или не удосуживаются), что это за преобразование такое (apple->banana), что за этим стоит из предметной области.
    getCustomerFromDatabase — в функциональщине вообще невалидная функция (по названию). Потому, что нету ни Кастомера ни Датабазы.


    1. Throwable
      18.09.2017 12:38
      +4

      Потому что основа функционального программирования — это stateless-операции над аргументами функции. Когда мы добавляем внешний изменяемый state в качестве, например, базы данных, то теряется идемпотентность функций, и результат композиции становится непредсказуемым. Чтобы жестко зафиксировать последовательный порядок применений функций, требуется использовать специальную монаду IO, с которой программирование фактически становится императивным. Кроме того, результат вычисления функции — это уже не просто Response, а еще и измененный State. То есть красивая идеальная схема HttpResponse = WebApplication(HttpRequest) уже не работает, и проблема здесь будет не в getCustomerFromDatabase(), а в updateCustomerInDatabase().
      Поэтому для data-driven приложений функциональное программирование не лучший выбор.


      1. marshinov Автор
        18.09.2017 12:42

        Чтобы жестко зафиксировать последовательный порядок применений функций, требуется использовать специальную монаду IO, с которой программирование фактически становится императивным.
        Все-таки точнее сказать: «код выглядит как императивный». IO позволяет отделить вычисления от исполнения.


      1. KoCMoHaBT61
        18.09.2017 14:55

        Про монады я ничего не знаю, писал на Эрланге.
        Так вот, этот самый Эрланг изначально противоречит «основе функционального программирования» — а именно в модульной структуре. Если наши функции такие стейтлесс, то нахрена ограничивать доступ к функциям в модуле с помощью -export([blablafun/1]).
        Ну ладно… Ты не замечаешь, что в ответе ты применил Структуру Данных, которой быть не должно? То есть идеальная схема — это:
        {http_response, _bla, _bla ,_bla, _bla} = handle({http_request,_bla,_bla,_bla,_bla}).
        … и эти оба тупла — это совсем не структуры данных…
        А до кастомеров в датабазах и о последовательности исполнения мы ещё и близко не добрались.


      1. 0xd34df00d
        18.09.2017 19:42
        +1

        Можно смотреть на IO не как на внешний мир, а как на токены, взаимодействующие с внешним миром, а ваша программа — чистая композиция этих (пусть и нечистых) токенов.


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


  1. igrishaev
    18.09.2017 10:53
    +2

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


  1. marshinov Автор
    18.09.2017 11:29
    +1

    Просто используйте неизменяемые коллекции

    Всегда и везде и память ваша закончится:) Кроме шуток, R# перестал работать в VS2015 как раз из-за неизменяемых коллекций в Roslyn, которые выжрали всю оперативку. Анализаторы Roslyn выключить нельзя. Пришлось JetBrains запилить Rider.


    1. lair
      18.09.2017 11:52

      Люди, у которых 2015-ая студия и решарпер, смотрят на вас с удивлением.


      1. marshinov Автор
        18.09.2017 11:57

        У меня студия на i7 с ssd и 12гб оперативки знатно так подтормаживает, особенно при наборе текста. Райдер работает значительно шустрее. Кажется, про проблему открыть проект «Решарпер» в VS2015 со включенным «решарпером» упоминал serjic в одном из интервью про Rider.


  1. igrishaev
    18.09.2017 13:53

    Судя по картинке, Лисп вообще вне конкуренции. Почему же речь идет идет за F#, который двумя этажами ниже? И как лисперы живут без типов, монад?


    1. marshinov Автор
      18.09.2017 15:38
      +1

      Вы не поверите, это юмор:) Лисп — старейший из «выживших» ЯП. На коболе и фортране никто уже нового не пишет, а Clojure — это современный диалект Лиспа.


      1. vbif
        18.09.2017 16:36
        +1

        На фортране всё ещё пишут те, для кого фортран писался изначально.


  1. alexeykuzmin0
    18.09.2017 18:47
    +6

    монада — это всего лишь моноид в категории эндофункторов
    Не один год не мог понять, что такое монада, а после этой фразы резко понял.
    Большое спасибо!


  1. Gebb
    18.09.2017 18:52
    +1

    Функции с одинаковым типом входного и выходного значения являются моноидами

    Странное утверждение. Функция из А в А — это бинарное отношение на множестве А, то есть, подмножество декартова произведения (АхА). Моноид — полугруппа с единицей. Может, имелось в виду, что множетво всех таких фнкций образует моноид относительно операции композиции?


    1. marshinov Автор
      18.09.2017 18:55

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


      1. Ingas
        18.09.2017 19:04
        +1

        Стиль изложения автора — «открывай редактор, набивай код, смотри что получается, думай, изобретай».
        Т.е абсолютно адекватный материалу.
        Рекомендую!

        И далеко не все у него просто. Когда он объясняет различие аппликатива и монад, если не seasoned haskeler/MLer — мозг приходится на повышенную тактовую частоту переключать.


  1. Ingas
    18.09.2017 18:56
    +1

    Господа, я удивлен, что серия лекций, которая популяризировала выражение «Railway oriented programming» кого-то в 2017 году поражает «новизной».
    Кстати, «Railway oriented programming» (который является ни чем иным как паттерном(!)) — имеет реализации (или подражания) почти для каждого значимого языка.
    Я полагал, что если даже первоисточник кто-то не читал, то уже с последствиями его — обязательно должен был столкнуться.

    Ну, а если вам так нравится удивляться тому что уже вошло в повсеместную практику — то может быть вам в новинку будет лекция Норвига 98-го года norvig.com/design-patterns/design-patterns.pdf
    Рекомендую!