«Паттерны» функционального программирования

https://fsharpforfunandprofit.com/fppatterns/
  • Перевод
  • Tutorial

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

До недавнего времени такой взгляд на вещи действительно был недалек от истины: говорим ФП, подразумеваем Хаскель и теорию категорий. В последнее время ситуация изменилась и функциональная парадигма набирает обороты в 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 тоже содержит монады. Паттерн «интерпретатор» — это так называемая свободная монада.
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 361
  • +16

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


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


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


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


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


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


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

    • +3

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

      • –1
        Там будет CQRS и Add Action
        • +5
          Там будет какой-нибудь MathOperationsFactory, который вернёт AddingService, который примет два числа, а вернёт объект типа MathOperationResult. Или же вообще, MathOperationsBuilder, которому надо будет на вход передать два числа, класс отвечающий за сложение, а потом класс-конфиг, который укажет в каком виде это обработать и отдать. Как-то так ¯\_(ツ)_/¯
          • +3

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


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


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

            • +1

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


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

              • 0

                Да, поясню по поводу ООП. ООП может использоваться для написания как императивного кода — в таком случае его скрещивают со структурным программированием, так и для написания декларативного — в таком случае к нему часто добавляют элементы функционального. Проще всего показать это на примере языка 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. Но оба подхода активно используют ООП и его паттерны (первый код неявно использует паттерн "итератор", второй к нему добавляет "цепочку ответственности").

                • 0

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


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

                  • 0

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

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

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


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

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

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

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

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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


                                      A Gentle Introduction to Haskell: Classes

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

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


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




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

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


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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

                                            Всё остальное типа особенности множественного наследования здесь не имеет значения.
                                        • 0
                                          Что значит «наследовать контракт»?

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

                                        • +1

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

                                          • +1

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


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

                                            • 0

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

                                              • +1

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

                                                • 0

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


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

                                        • +1

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

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

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

                                        • 0

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

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

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

                                      • 0

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


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

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

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


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

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


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

                                          Фиг с ними с деревьями. Чем 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());
                                          • 0

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

                                            • 0

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

                                              • +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))) — то эта команда, хоть и написана в декларативном стиле, скорее всего будет являться частью какого-то императивного алгоритма.


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

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

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


                                                  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 элементов». Это промежуточное состояние явно требуется кодом — но оно не требуется постановкой задачи и никоим образом из нее не следует!

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


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

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

                                                    Не станет, потому что у вас никакого промежуточного состояния нету. f x where x = y — это просто (x => f(x))(y)
                                            • +1
                                              PS Последовательность — это не полный порядок. К примеру, на множестве вещественных чисел полный порядок задан — но последовательности они не образуют.

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

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

                                          • 0

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

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

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


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

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


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


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


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


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

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

                                        Кстати, называть функцию `printList` после такого преобразования уже некорректно, правильнее `visitList`. А `printList` — результат частичного применения `visitList` с параметром `printLn`.
                                        • –1
                                          Они просто перешли к вызывающему коду, теперь каждый кто вызывает printList должен указывать еще и эти [1..10] и printLn.

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


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

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


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

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

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

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


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

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

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

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


                                              let printList = List.iter printItem
                                              

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

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

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

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


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

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

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

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

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

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


                                                    map f list

                                                    такое:


                                                    withStrategy (parList strat) $ map f list

                                                    Ну или просто


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

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

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

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

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

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

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

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

                                        И да, на счет КДПВ. Я, конечно, могу ошибаться, но, насколько я знаю, в ФП, Лисп — это просто баловство вроде JS в сравнении с Haskell, потому мне непонятно, почему он улетел так высоко.
                                        • +1
                                          Ношение красивой, но не тёплой шапки в мороз повышает вероятность дополнительного финансирования, на утеплитель. Это знают все теоретики красивых стилей, но не говорят о конечной цели: ).
                                          • 0
                                            Этот доклад раздражает не только вас. Подобным образом отреагировал и Дядя Боб. Кстати, его пост-ответ тоже заслуживает перевода.
                                            • +5
                                              Добавлю еще про манипуляции в статье:
                                              А вот этот код но уже с обработкой ошибок:image


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

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

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

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

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

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

                                                  Это все тот же ".catch()" в промисах, но в промисах он менее гибкий.
                                                  • +1
                                                    Потому этот кейс не стоит брать во внимание – там другие правила

                                                    Если вас кейс на данный момент не слишком волнует, это не значит, что его не стоит брать во внимание. Вот был у вас однопоточный код, стал он выполняться двое суток. Надо параллелить, а у вас внутри везде throw. Весь код на помойку, переписываем на TPL с нуля? Весело, но дорого.
                                                    • –1
                                                      1. Мои предыдущие комменты в этом треде относятся конкретно к данной статье, а не к всевозможным особенностям реализации каких-то фреймворков.

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

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

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

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

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

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

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

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

                                                            • 0

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

                                                              • 0

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


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

                                                                • 0

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


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


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


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

                                                                  • 0

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


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

                                                                    А это как?


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


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

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

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


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

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

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

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

                                                            • 0

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                            • –1
                                                              синхронный код:
                                                              try
                                                              {
                                                                  var a = someMethodThrowingArgumentException(123);
                                                                  var b = someMethodThrowingInvalidOperationException(a);
                                                                  var c =  someMethodThrowingGenericException(b);
                                                                  return c.ToString();
                                                              }
                                                              catch(ArgumentException)
                                                              {
                                                                  retrun "ArgumentException happened";
                                                              }
                                                              catch(InvalidOperationException)
                                                              {
                                                                  retrun "InvalidOperationException happened";
                                                              }
                                                              catch(Exception)
                                                              {
                                                                  retrun "Ooops!";
                                                              }
                                                              

                                                              С TPL:
                                                              someTask.ContinueWith(a => {
                                                                  try
                                                                  {
                                                                      someSyncMethod(a);
                                                                      someAsyncMethod(a).ContunueWith(b => {
                                                                          try
                                                                          {
                                                                              // ну и так далее
                                                                          }
                                                                          catch(InvalidOperaionException)
                                                                          {
                                                                               // только для синхронного метода
                                                                               return "InvalidOperationException happened";
                                                                          }
                                                                      })
                                                                  }
                                                                  catch(ArgumentException)
                                                                  {
                                                                       return "ArgumentException happend"
                                                                  }
                                                              })
                                                              

                                                              Да, есть async/await. Но код с async/await только выглядит императивным, а компилируется в нечто другое, работающее с помощью SyncronizationContext и по духу ничем не отличается от монад. Т.е. в чисто-императивном коде без «примочек» придется писать try/catch в каждом ContinueWith. Код становится менее читаемым, exit points «размазываются» по разным callback'ам и их приходится «собирать». Основная проблема такая.
                                                              • 0
                                                                > Да, есть async/await. Но код с async/await только выглядит императивным, а компилируется в нечто другое, работающее с помощью SyncronizationContext и по духу ничем не отличается от монад.

                                                                Код с async/await ничем не отличается от кода с Task.ContunueWith, а просто явялется синтаксическим сахаром. SynchronizationContext используется и там, и там.
                                                                • +1
                                                                  Т.е. в чисто-императивном коде без «примочек» придется писать try/catch в каждом ContinueWith.

                                                                  А разве в момент получения результата всей цепочки (т.е. var t = a.ContinueWith().ContinueWith().ContinueWith(); t.Wait();) вы не получите AggregateException?

                                                                  • 0
                                                                    Пожалуй, нужно ответить более развернуто. Да, я согласен, что в TPL с обработкой ошибок все здорово. Гораздо лучше, чем при работе с Thread напрямую. Да, сильно сложнее с TPL не становится. Я говорю о том, что с TPL появляются дополнительные нюансы.

                                                                    var t = a.ContinueWith().ContinueWith().ContinueWith(); t.Wait();

                                                                    Этот код не имеет практического применения. Какой смысл использовать асинхронную модель, чтобы потом принудительно ее «синхронизировать» с помощью t.Wait()?

                                                                    Я имел в виду следующие особенности:

                                                                    • Fire & Forget: не все разработчики одинаково полезны ответственно относятся к обработке исключений. Кто-то просто напишет так и ошибка будет потеряна.
                                                                      Task.Run(() => {
                                                                          // ...
                                                                          throw new Exception("please help me")
                                                                      }; // Fire & Forget
                                                                    • Про AggregateException.Flatten для TaskCreationOptions.AttachedToParent нужно знать, иначе можно потерять часть исключений.
                                                                      void Handle(AggregateException ex)
                                                                      {
                                                                          foreach (var exception in ex.Flatten().InnerExceptions)
                                                                          {
                                                                              Console.WriteLine(exception.Message);
                                                                          }
                                                                      }
                                                                    • Возможность обработать исключение с помощью TaskContinuationOptions.OnlyOnFaulted, а не try/catch — еще один exit point.
                                                                    • Можно потерять одно из исключений при использовании Task.WhenAll (да, они ССЗБ).
                                                                    • UnobservedTaskException — настраиваемое поведение
                                                                    • Wrapping / Unwrapping AggregateException при использовании await или t.Wait() — нужно внимательно следить, что мы ловим в try/catch.

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

                                                                    Спасибо за комментарий. Пока отвечал понял, что зря вообще докопался до TPL. Мне больше не понравилась идея строить на исключениях control-flow на самом деле, а TPL как-то под «замес» попал:)
                                                                    • +1
                                                                      Этот код не имеет практического применения.

                                                                      Не имеет. Это просто иллюстрация для того, чтобы было понятно, где именно вылетит AggregateException.


                                                                      Я имел в виду следующие особенности:

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


                                                                      Мне больше не понравилась идея строить на исключениях control-flow на самом деле,

                                                                      А не надо строить control flow на исключениях, они не для этого.

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

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

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

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

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

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

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

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

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

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

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


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

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

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

                                                                        • –1
                                                                          Зачем?
                                                                          • +1

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

                                                                                        Да ладно?

                                                                                        • –2

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

                                                                                          • –1
                                                                                            Почему?
                                                                                            • –1

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

                                                                                              • –1
                                                                                                Просто детали реализации?
                                                                                                • 0

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


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

                                                                                                  • –1

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

                                                                                                    • 0

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

                                                                                                      • 0

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


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

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

                                                                                                        • 0

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

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