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



Хотя люди обычно признают удобства ФП фич, ведь намного приятнее писать:


int Factorial(int n)
{
    Log.Info($"Computing factorial of {n}");
    return Enumerable.Range(1, n).Aggregate((x, y) => x * y);
}

чем ужасные императивные программы вроде


int Factorial(int n)
{
    int result = 1;
    for (int i = 2; i <= n; i++)
    {
        result *= i;
    }
    return result;
}

Так ведь? С одной стороны да. А с другой именно вторая программа в отличие от первой является функциональной.


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


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


Функциональная программа — программа, состоящая из чистых функций.

Ок, это мы знали, но что такое чистая функция? Чистая функция — функция, результат вызова которой является ссылочно прозрачным. Или, если формально:


Функция f является чистой если выражение f(x) является ссылочно прозрачным для всех ссылочно прозрачных x

А вот тут начинаются различия с тем, что люди обычно представляют под "чистой функцией". Разве чистая функция — это не та, которая стейт не мутирует? Или там в глобальные переменные не залезает? Да и что это за "ссылочная прозрачность" такая? На самом деле корреляция с этими вещами действительно есть, но сама суть чистоты не в том, чтобы ничего не мутировать, а именно эта самая прозрачность.


Так что же это такое? А вот что:


Ссылочная прозрачность — свойство, при котором замена выражения на вычисленный результат этого выражения не изменяет желаемых свойств программы

Это значит что если у нас где-то написано var x = foo() то мы всегда можем заменить это на var x = result_of_foo и поведение программы не поменяется. Именно это и является главным требованием чистоты. Никаких других требований (вроде неизменяемости) ФП не накладывает. Единственный момент тут — философский, что считать "поведением программы". Его можно определить интуитивно как свойства, которые нам критично важно соблюдать. Например, если исполнение кода выделяет чуть больше или чуть меньше тепла на CPU — то нам скорее всего это пофиг (хотя если нет, то мы можем с этим работать специальным образом). А вот если у нас программа в базу ходить перестала и закэшировала одно старое значение — то это нас очень даже волнует!


Вернемся к нашим примерам. Давайте проверим, выполняется ли наше правило для первой функции? Оказывается, что нет, потому что если мы заменим где-нибудь Factorial(5) на 120 то у нас поменяется поведение программы — в логи перестанет писаться информация которая раньше записывалась (хотя если мы подойдем с позиции "да и хрен ними, с логами" и не будем считать это желаемым поведением, то программу можно будет считать чистой. Но, наверное мы не просто так ту строчку в функции написали, и логи в кибане все же хотели бы увидеть, поэтому сочтем такую точку зрения маловероятной).


А что насчет второго варианта? Во втором случае всё остается как было: можно все вхождения заменить на результат функции и ничего не изменится.


Важно отметить, что это свойство должно работать и в обратную сторону, то есть мы должны иметь возможность поменять все var x = result_of_foo на var x = foo() без изменения поведения программы. Это называется "Equational reasoning", то есть "Рассуждения в терминах эквивалентности". В рамках этой парадигмы что функции, что значения — суть одно и то же, и можно менять одно на другое совершенно безболезненно.


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


Зачем это нужно


Наверное — самый главный вопрос. Зачем так мучиться? Копировать данные вместо того чтобы изменить напрямую, оборачивать объекты в эти ваши ST чтобы изменения (если они есть) не утекали наружу, и вот это всё… Ответ — для лучшей композиции. В своё время goto очень невзлюбили именно потому, что с ним очень трудно понять как на самом деле программа себя ведет и какой на самом деле поток данных и управления, и переиспользовать функцию написанную с goto было сложно, ведь тогда он умел даже в середину тела функции прыгнуть без каких-либо проблем.


С Equational reasoning всегда просто понять, что происходит: вы можете заменить результат на функцию и всё. Вам не нужно думать, в каком порядке функции вычисляются, не надо переживать насчёт того как оно поведет если поменять пару строчек местами, программа просто передает результаты одних функций в другие.


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


var something = function();
DoStuff(this.Field, something);

И понадобилось мне во время выполнения задачи их немного отрефакторить, что я и сделал:


DoStuff(this.Field, function());

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


... что-то считаем
this.Field = GetUpdatedVersion(this.Field, localData) // ой! 
... продолжаем считать и возвращаем результат

Соответственно если раньше с точки зрения компилятора оно выглядело так:


var something = function();
var arg1 = this.Field;      // после вызова function - новое значение!
var arg2 = something;
DoStuff(arg1, arg2);

То после рефакторинга получилось следующее:


var arg1 = this.Field;      // до вызова function - остаётся старое значение!
var arg2 = function();
DoStuff(arg1, arg2);

Соответственно если раньше функция DoStuff вызывалась с обновленной версией поля, то после рефакторинга начала вызываться со старой.


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


В общем и целом, ФП направлено на то, чтобы можно было судить о поведении функции наблюдая только её одну. Если вы, как и я, пишете на каком-нибудь C# в обычном императивном стиле, вам кроме этого нужно понимать, как у вас DI работает, что конкретно делает функция function или DoStuff, можно ли эту функцию безопасно из разных потоков вызывать или нет. В ФП вы смотрите на одну функцию, смотрите на её данные, и этой информации вам достаточно чтобы полностью понимать как она работает.


То есть этот стиль направлен на более удобное разделение частей программы друг от друга. Это сильно упрощает понимание кода для людей, которые его не писали. По традиции отмечу, что этим кем-то можете быть вы сами через полгода. Чем больше проект, тем сильнее эффект. Насколько я видел, в достаточно крупных проектах на сотни тысяч строк люди сами в итоге переизобретают все те же принципы, несмотря на то что и язык и платформа обычно достаточно сильно упираются. Потому что просто невозможно отлаживать большие программы, когда всё взаимодействует со всем. Чистота функции, когда её вызов просто возвращает результат, а не пишет вам нескучные рассказы в кибану и не посылает емэйлы на почту, очень в этом помогает. Любой разработчик большого проекта вам скажет, что чётко очерченные контракты и небольшие стейтлесс модули — самые простые и удобные в работе с ними. Функциональный подход всего лишь развивает эту идею до логической точки — все функции должны быть чистыми, и не зависеть от какого-либо состояния.


Как эти принципы отражаются в коде


В качестве сравнения могу предложить вам такой пример, который я взял из Красной книги Scala (совершенно шикарная книга, очень доходчиво и интересно рассказывает о ФП, c крутыми задачками). Правда, для большей понятности я адаптировал текст и код к C#.


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


ООП вариант

Окей, как нам сказали, так и пишем:


public class Cafe
{
    public Coffee BuyCoffee(CreditCard card)
    {
        var cup = new Coffee()
        card.Charge(cup.Price)
        return cup
    }
}

Строка card.Charge(cup.Price) является примером побочного эффекта. Оплата кредитной картой предполагает некоторое взаимодействие с внешним миром — например, для этого может потребоваться связаться с компанией-эмитентом кредитной карты через какой-либо веб-сервис, авторизовать транзакцию и всё такое. Побочным эффектом оно называется потому, что все эти действия не имеют отношения к созданию экземпляра Coffee, то есть они как бы находятся "сбоку" от основного результата функции "вернуть стаканчик кофе".


В результате из-за побочного эффекта код трудно тестировать. Любой опытный ООП разработчик скажет "Да сделай ты интерфейс для того чтобы списывать деньги!". Разумное требование, так и поступим:


public class Cafe
{
    public Coffee BuyCoffee(CreditCard card, IPaymentProvider paymentProvider)
    {
        var cup = new Coffee()
        paymentProvider.Charge(card, cup.Price)
        return cup
    }
}

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


  • Во-первых нам пришлось ввести IPaymentProvider, хотя если бы не тесты одна конкретная реализация нас бы вполне устроила.
  • Во-вторых моком реализующим нужный функционал может быть неудобно пользоваться. Типичный пример — InMemory DB, где мы мокаем Insert/Save/… методы, а потом достаем внутренний стейт (как правило в виде списков) и смотрим, что всё сохранилось куда надо. Надо ли говорить, что инспектировать внутреннее состояние объектов — это нехорошо? И да, можно конечно использовать какой-нибудь фреймворк который сделает за нас большую часть работы, но не всю, да и тащить целый фреймворк просто чтобы протестировать что мы можем купить чашечку кофе выглядит оверкиллом.
  • Ну а в-третьих есть проблемы с переиспользованием этой функции. Допустим мы хотим купить N чашечек кофе. В текущих интефрейсах у нас нет простого способа это сделать кроме как написать полностью новую функцию (если мы конечно не хотим заддосить наш платёжный шлюз однотипными запросами):

public class Cafe
{
    public Coffee BuyCoffee(CreditCard card, IPaymentProvider paymentProvider)
    {
        var cup = new Coffee()
        paymentProvider.Charge(card, cup.Price)
        return cup
    }

    public Coffee[] BuyCoffees(int count, CreditCard card, IPaymentProvider paymentProvider)
    {
        // нам теперь еще и случай 0 чашек надо обработать, 
        // чтобы не выставить случайно чек на 0 рублей
        if (count == 0) return Array.Empty<Coffee>(); 
        var cups = Enumerable.Range(0, count).Select(_ => new Coffee()).ToArray();
        paymentProvider.Charge(card, cups[0].Price * count)
        return cups
    }
}

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


ФП вариант

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


public class Cafe
{
    public (Coffee, Charge) BuyCoffee(CreditCard card)
    {
        var cup = new Coffee()
        return (cup, new Charge(card, cup.Price))
    }
}

Да, вот так просто. Теперь вызывающий код, если это реальное приложение, может произвести транзакцию и списать деньги. А вот если это тест, то он просто может проверить возвращенный объект Charge на все интересующие его свойства. Никаких моков больше не надо: мы разделили события выставления счёта и интерпретацию этого счёта. Charge это простая DTO которая хранит с какой карты сколько надо списать. Легко видеть, что наша функция стала чистой. Она просто возвращает кортеж из двух объектов, которые являются простым описанием данных. Мы можем заменить вызов этой функции на результат, и смысл программы не поменяется. И нам на этом уровне больше не нужен никакой провайдер платежей, ура!


Что насчёт покупки N стаканчиков кофе? Благодаря тому что мы избавились от эффектов, нам не нужно бояться что N вызовов BuyCoffee заспамят наш платежный шлюз, поэтому просто переиспользуем её.


public class Cafe
{
    public (Coffee, Charge) BuyCoffee(CreditCard card)
    {
        var cup = new Coffee()
        return (cup, new Charge(card, cup.Price))
    }

    public (Coffee[], Charge) BuyCoffees(int count, CreditCard card)
    {
        var (coffees, charges) = Enumerable.Range(0, count)
                                           .Select(_ => BuyCoffee(card))
                                           .Unzip();
        return (coffees, charges.Aggregate((c1, c2) => c1.Сombine(c2))
    }
}

Ну и дописываем хэлпер-функцию Combine:


public class Charge
{
    public CreditCard Card { get; set; }
    public double Amount { get; set; }

    public Charge(CreditCard card, double amount)
    {
        Card = card;
        Amount = amount;
    }

    public Charge Combine(Charge other)
    {
        if (Card != other.Card) 
            throw new ArgumentException("Can't combine charges to different cards");
        return new Charge(Card, Amount + other.Amount);
    }
}

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


IEnumerable<Charge> Coalesce(IEnumerable<Charge> charges) => 
    charges.GroupBy(x => x.Card).Select(g => g.Aggregate((c1, c2) => c1.Combine(c2))

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


Предвижу, что мне могут возразить, что дескать-то проблема не решена, и теперь код уровнем выше должен делать это списание, только теперь логика немного размазана, и мы просто чуть-чуть упростили тесты конкретно нашего класса Cafe. На самом деле, это не так, потому что код выше тоже может передать решение что делать дальше, а тот код еще дальше, и так до сервиса, который уже реально что-то сделает с этими данными (но и там его можно сделать тестируемым без моков, подробнее об этом в другой статье).


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


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


И это всё?


С точки зрения самой сути ФП — да, это всё. Отсутствие эффектов это единственное требование, которое нужно соблюдать, чтобы программа была функциональной. Но исторически сложилось, что ФП языки обладают более обширным количеством ограничений, а ограничения обычно придумывают не просто так, а чтобы получить от этого преимущества. Ограничение на типы переменных (то что в int переменную нельзя засунуть строку) позволяет писать более надежные программы, ограничения на изменение потока управления (например, запрет goto) ведет к упрощению понимания программ, ограничение на шаблонизацию (Templates vs Generics) позволяет проще писать обобщенный код и иметь более хорошие сообщения об ошибках, и так далее.


Одним из самых крутых преимуществ распространенных ФП языков, на мой взгляд, является ценность сигнатур функций и типов. Дело в том, что в отличие от "грязных" функций, сигнатура чистой обычно дает столько информации, что количество возможных вариантов её реализации снижается до жалких единиц, а в экстремальных случаях компилятор может сгенерировать тело функции по её сигнатуре. Почему это не работает в императивных программах? Потому что там void UpdateOrders() и void UpdateUsers() имеют одну и ту же сигнатуру () -> (), но совсем разное значение. В ФП они будут иметь тип навроде () -> OrdersUpdate и () -> UsersUpdate. Именно потому, что функции разрешено только вычислять значение (а не делать произвольную дичь) мы и можем с уверенностью судить о многих её свойствах, просто глядя на сигнатуру.


Что же нам это дает? Ну, например предположим у нас есть такая функция (пример на Rust)


// принимаем массив объектов, еще какой-то объект, и возвращаем значение того же типа
fn foo<T>(a: &[T], b: T) -> T { ...какое-то тело... }

Я не знаю что внутри этой функции, но по сигнатуре я вижу, что результатом будет один из элементов массива, либо в случае пустого массива — элемент b который я передал. Откуда я это знаю? Оттуда, что функция не делает никаких предположений о типе T. Поэтому она никак не может создать экземпляр самостоятельно. Следовательно, единственный способ получить значение того же типа — взять один из объектов которые мы ей передали.


Соответственно я могу написать такой тест


let a = [1,2,3,4,5];
let b = foo(a, 10);
assert!(b == 10 || a.iter().any(|x| x == b))

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


А теперь давайте уберем второй параметр и посмотрим что произойдет:


fn foo<T>(a: &[T]) -> T { ...какое-то тело... }

Обратите внимание, что для пустого массива эта функция кинет исключение, панику, войдет в вечный цикл или сделает еще что-то нехорошее. Или, если говорить формально, вернёт Bottom-тип ?. Откуда я это знаю? А потому что функция обязалась вернуть значение T, а мы ей ни одного не передали. То есть её контракт невозможно соблюсти для любого значения аргумента a. Таким образом функция является частично-рекурсивной, и следовательно не определена для пустых массивов. А на неопределенных аргументах функции обычно паникуют.


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


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


T Foo<T>(List<T> list, T value) => default(T);

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


А знаете как будет выглядеть в расте функция, которая если массив пустой вернет дефолтное значение T? Вот так:


fn foo<T: Default>(a: &[T]) -> T { ...какое-то тело... }

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


В функциональной парадигме вам в 99% случаев достаточно просто посмотреть на сигнатуру функций чтобы понять, как она работает. Это может показаться неправдоподобным хвастовством, но это так. Haskell коммьюнити довело эту мысль до абсолюта и создало поисковик Hoogle который позволяет искать функции в библиотеках по её сигнатуре. И он отлично работает.


Например (a -> Bool) -> [a] -> [a] (функция, принимающая два аргумента: предикат и список, в качестве результата возвращает список таких же элементов) ожидаемым образом находит функции filter и takeWhile.


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


fn bar<T>(s: String) -> T { ... } // раст-вариант
bar :: String -> a                // хаскель-вариант

Ответ

Если подумать, то у нас нет никакого способа сделать объект, про тип которого мы ничего не знаем. Потому единственное, что может сделать эта функция — никогда не вернуть результат. То есть вернуть вечный цикл или панику, известный нам ?. Но вспомним, что функция принимает еще и строковую переменную. Для цикла большого смысла её передавать нет, поэтому можно быть практически уверенным в том, что это функция занимается бросанием паники:


fn bar<T>(s: String) -> T { 
    panic!(s);
}

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


Навык додумать что делает функция по сигнатуре очень выручает, потому что вам не нужно лезть в тело функций чтобы понять, что она может сделать а что нет. Даже если функция foo из примера выше занимает 1000 строк, она всё равно обязана вернуть либо один из элементов переданного массива, либо второй аргумент. Других вариантов нет. И вам не нужно читать 1000 строк чтобы это понять. Вы просто знаете это глядя на сигнатуру функции.


Разве чисто функциональный язык может сделать что-то полезное?


Этот вопрос меня волновал с тех пор, как я у знал о функциональных языках. "Чёрт", думал я, "Но ведь мне надо в базу сходить, HTTP запрос сделать, в консоль написать в конце концов. Но чистый язык этого не разрешает. Наверное он подходит только чтобы факториалы считать".


Как оказалось, сам ФП язык всё это делать действительно не может, Но тут умные ребята взяли и придумали как это обойти. Они сказали "Окей, программа не может делать грязных действий. Но, а что если мы разделим создание описателя вычисления и его интерпретацию (прямо как в нашем примере с кафе)? А тогда получится, что вся программа чистая, а нечистым является рантайм который выполняет всю грязную работу!".


Как это выглядит? Ну возьмем для примера тип IO, отвечающий за взаимодействие с внешним миром. Это такой же тип, как наш Charge из примера выше, только вместо списания по карте он описывает ввод/вывод. Сам по себе IO ничего не делает, если мы напишем print "Hello world" в хаскелле ничего не произойдет. Но если мы напишем main = print "Hello world" то магическим образом текст попадет на экран. Как же это происходит?


А всё дело в том, что рантайм хаскелля занимается интерпретацией этого IO. То есть все описанные действия происходят за пределами функции main. То есть из всей нашей программы мы собираем гигантскую стейт машину, которую затем рантайм начинает интерпретировать. И этому рантайму разрешено делать "грязные" вещи — ходить в базу, печатать на экран, и делать всё, что угодно. Но с точки зрения кода мы ничего никогда не совершаем.


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


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


Наверное, я вас только запутал этой аналогией, поэтому давайте покажу на примере. Вот программа на Rust:


fn main() {
    let _ = print!("Hello ");
    println!("world!");
}

И она выводит "Hello world!". А теперь попробуем написать аналогичную программу на Haskell:


main :: IO ()
main = do
  let _ = print "Hello "
  print "world!"

И она выводит "world!". По сути разница между поведением этих программ и является квинтэссенцией различия чистой и нечистой программы. В случае хаскелля мы создали описатель "выведи Hello", но никак им не воспользовались. Этот описатель не был проинтерпретирован и надписи на экране не появилось. В качестве результата main мы вернули единственный описатель с world!, который и был выполнен. С другой стороны в случае программы на Rust сам вызов print! уже сам по себе является действием, и мы не можем его никак отменить или преобразовать как-то еще.


Именно возможность работать с эффектами как значениями (выкинуть сам факт того, что мы хотели что-то вывести на экран) очень упрощает жизнь, и делает невозможными баги вроде того что я показал в первом разделе. И когда говорят про "Контроль эффектов в ФП" имеют ввиду именно это. Забегая вперед, можно описывать эффекты функций в стиле "эта функция пишет в базу (причем только вот в ту таблицу), ходит по HTTP (но только через этот прокси, и на вот этот сайт), умеет писать логи и читать конфиги. И всё это будет проверяться во время сборки, и при попытке сходить не на тот сайт или прочитать конфиг не аннотировав такую возможность в сигнатуре будет приводить к ошибке времени компиляции.


Заключение


Как видите, всё противопоставление ООП и ФП совершенно искусственно. Можно писать и в том, и в другом стиле на одном и том же языке, и в принципе даже совмещать. Весь вопрос в том, поощряет ли язык написание в таком стиле или наоборот. Например писать объектно-ориентированно на ANSI C можно, но очень больно. А на джаве просто. С другой стороны писать на джаве в чисто функциональном стиле тяжело, а на Scala или Haskell — просто. Поэтому вопрос скорее заключается в том, что есть два инструмента, один распространен и поддерживается многими языками, другой более интересен по целому спектру свойств, но поддерживается не везде. Ну и дальше ваш выбор как разработчика, какой инструмент вам больше подходит по ситуации.


Лично я для себя вижу очень много преимуществ в функциональной парадигме в плане поддерживаемости кода. Я очень устал от того, что перестановка двух несвязных строчек местами может что-то поломать в совершенно третьем месте. Мне надоело конфигурировать моки и DI. Я не хочу ловить в рантайме ошибки "Метод не был замокан"/"Тип не был зарегистрирован"/"...", в конце концов я не для того выбирал статически типизированный язык.


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

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


  1. kefirr
    09.12.2019 10:45
    +2

    Доступно всё рассказано, и ни одного упоминания монад — спасибо!


    Но, определения в начале статьи всё-таки отличаются от общепринятых (см википедию):


    • Функциональная программа — программа, состоящая из чистых функций
    • Функция f является чистой если выражение f(x) является ссылочно прозрачным для всех ссылочно прозрачных x

    Есть ссылки на первоисточники этих определений?


    1. PsyHaSTe Автор
      09.12.2019 11:06

      Доступно всё рассказано, и ни одного упоминания монад — спасибо!

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


      Есть ссылки на первоисточники этих определений?

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


      1. gearbox
        09.12.2019 13:35

        >А на самом деле монада — это просто интерфейс с парой методов.
        Монада это переопределенная функция композиции функций. Все остальное от лукавого.


        1. 0xd34df00d
          09.12.2019 18:19
          +1

          А ещё функтор и аппликативный функтор. И ещё законы там всякие должны выполняться.


          1. gearbox
            09.12.2019 20:01
            -1

            да но как это отменяет суть процесса? Происходит композиция функций. Просто передача параметров не напрямую а с упаковкой/распаковкой.


          1. Druu
            10.12.2019 10:10

            А ещё функтор и аппликативный функтор. И ещё законы там всякие должны выполняться.

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


            Иными словами, если вы говорите "монада" в контексте математическом (теоркат и вот это вот все) — то, конечно, тут вполне строго и четкое определение. Если же это контекст прикладного программирования — этими факторами можно пренебречь (и постоянно на практике пренебрегается).


            1. koldyr
              10.12.2019 11:06
              +1

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


              1. Druu
                10.12.2019 12:21

                Ну, вот я например недавно использовал монадический интерфейс для сетевого взаимодействия, чтобы работать с запросами в виде аналогичном x <- query1, y <- query2, return x + y (ну для примера), надо это было в силу не совсем тривиального процесса отправки и получения ответа. Если у нас query — это промисы (ну как при обычных хттп запросах), то мы получаем, с-но, промис-монаду (которая, к слову, в том же js не является монадой из-за некоторых особенностей семантики, нарушающих монадические законы. но всем пофиг, кто вообще знает, что promise в js — не монада?). В моем же случае у меня типы x и query1 были типами произвольных сообщений (x тип приходящих, query — тип исходящих), с-но, не было никакого соответствующего функтора и даже нельзя было написать сколько-нибудь осмысленные return и fmap. Но, несмотря на то, что полученная конструкция была прям уж совсем не монада, взаимодействие с ней выглядит вполне монадически ну и концептуально оно тоже вполне монада.


                1. mayorovp
                  10.12.2019 12:24

                  Как же тогда у вас в коде return x+y используется, когда нельзя осмысленный return написать?


                  1. Druu
                    10.12.2019 13:31

                    У меня в коде и не используется, очевидно же :)
                    Это просто пример был, для того чтобы понятно, о чем речь в общем. В реальном коде вместо этого везде что-то вроде someFunction(x+y) где someFunction исполняет какой-то нужный сайд-эффект (setState реактовский, например).
                    Но это достаточно вырожденный пример, когда от монады, действительно, мало что осталось и такое не так уж распространено. А вот неисполнение монадических законов — как в промис-монаде жс — штука обыденная. Try в скале, насколько я помню, еще из распространенных примеров.
                    В Linq, кстати, тоже return не используется (если даже написать аналог return'а, то шарп не сможет вывести полиморфный аргумент). И вообще монадическая нотация там рассахаривается совсем не как в хаскеле, т.к. SelectMany — не бинд.


                    Смысл в том, что есть математические абстракции, и есть реальные объекты реального мира. И одно не в точности соответствует другому — абстракции описывают реальные объекты лишь с точностью до. Всегда есть какая-то погрешность. Даже какой-нибудь канонический Maybe в хаскеле — это не идеальная сферическая монада в вакууме (просто потому, что ваш пека — это не идеальная сферическая в вакууме машина Тьюринга, а просто обычный конечный автомат) и можно построить специфические кейзы, когда оно не будет себя вести как должно. Но мы разумно пренебрегаем подобными кейзами.
                    Ключевой момент тут "разумно", коненчо — т.е. надо понимать какие есть погрешности и где они важны.
                    Например, альтернативное рассахаривание шарпа работает т.к. fmap f x = x >>= return. f (т.к. from x' in x select f(x') как раз непосредственно в Select aka fmap и преходит), и вы, будучи неосторожным, вполне можете встретиться с неожиданным поведением монады, которая этому закону не удовлетворяет


                    1. PsyHaSTe Автор
                      10.12.2019 13:41

                      Because of these difficulties, Haskell developers tend to think in some subset of Haskell where types do not have bottom values. This means that it only includes functions that terminate, and typically only finite values. The corresponding category has the expected initial and terminal objects, sums and products, and instances of Functor and Monad really are endofunctors and monads.


                    1. mayorovp
                      10.12.2019 13:45
                      +1

                      В Linq, кстати, тоже return не используется (если даже написать аналог return'а, то шарп не сможет вывести полиморфный аргумент).

                      static IEnumerable<T> Return<T>(T value) {
                          yield return value;
                      }

                      Который из аргументов не получится тут вывести?


                      SelectMany — не бинд

                      а что это такое тогда?


                      1. Druu
                        10.12.2019 14:31

                        а что это такое тогда?

                        Просто функция, никакой теоретической конструкции, которой бы она соответствовала, мне неизвестно (хотя может она и есть). Есть много функций, которые "не бинд". Не понятно, что вас удивило тут. У SelectMany, собственно, три аргумента. А у бинда — два.


                        Который из аргументов не получится тут вывести?

                        Причем тут определение Return, я про применение. Если вы напишите Return(1) то за счет какой магии шарп узнает, что там должен быть IEnumerable? Хотя, сейчас вот я вспомнил, что недавно в определенных контекстах шарп научился по возвращаемому типу выводить аргументы, может, сейчас и выведет (хотя не уверен). Раньше точно не мог.


                        1. PsyHaSTe Автор
                          10.12.2019 14:40

                          Просто функция, никакой теоретической конструкции, которой бы она соответствовала, мне неизвестно (хотя может она и есть). Есть много функций, которые "не бинд". Не понятно, что вас удивило тут. У SelectMany, собственно, три аргумента. А у бинда — два.

                          Потому что это liftM2, из которого бинд можно вывести.


                        1. mayorovp
                          10.12.2019 14:46
                          +1

                          У SelectMany, собственно, три аргумента. А у бинда — два.

                          А это тогда что? SelectMany<TSource,TResult>(IEnumerable<TSource>, Func<TSource,IEnumerable<TResult>>)


                          Если вы напишите Return(1) то за счет какой магии шарп узнает, что там должен быть IEnumerable?

                          Понял. Но направление вывода типов в C# никак не мешает IEnumerable<> быть монадой.


                          1. Druu
                            10.12.2019 15:33

                            А это тогда что? SelectMany<TSource,TResult>(IEnumerable, Func<TSource,IEnumerable>)

                            Это перегрузка НЕ используется при десугаринге Linq, в том-то и дело. В Linq используется вот эта перегрузка:


                                this IEnumerable<TSource> source,
                                Func<TSource, IEnumerable<TCollection>> collectionSelector,
                                Func<TSource, TCollection, TResult> resultSelector
                            )```
                            она позволяет десугарить Linq не во вложенные замыкания, а просто в последовательность вызовов something.SelectMany(...).SelectMany(...).SelectMany().
                            и если вы хотите что-то сделать используемым в Linq, то вам надо будет именно эту перегрузку писать (ну только IEnumerable на свой генерик замените), иначе ошибка типов. А обычный бинд, напротив, не требуется. Понятно, конечно, что одно можно написать через другое.
                            
                            >Понял. Но направление вывода типов в C# никак не мешает IEnumerable<> быть монадой.
                            
                            А кто говорил, что ей что-то мешает быть монадой?


                            1. mayorovp
                              10.12.2019 15:39

                              Ну, начинали-то вы ветку не с десугаринга Linq, а с неисполнения монадических законов...


                              1. Druu
                                10.12.2019 16:52

                                Ну, начинали-то вы ветку не с десугаринга Linq, а с неисполнения монадических законов...

                                Так а где я говорил, что конкретно для IEnumerable что-то не исполняется (ну, если пренебречь тем, что "настоящих" монад в программировании не бывает в принципе)?
                                Linq был приведен как пример того, что чтобы что-то было "linq-монадой" этому чему-то не нужен ни стандартный bind, ни return.


                                1. koldyr
                                  11.12.2019 19:49

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

                                  Спойлер: return в хаскеле НЕ ключевое слово.


                                  1. 0xd34df00d
                                    11.12.2019 19:57
                                    +1

                                    Спойлер: return в хаскеле НЕ ключевое слово.

                                    И вообще все Ъ уже давно пишут pure.


                                    1. PsyHaSTe Автор
                                      11.12.2019 20:09

                                      В чатике помню обсуждалось, что в некоторых случаях с pure можно заклиться, а с return — нет. Вроде речь шла про магию fix, MonadWrap и всякое такое.


                                      Для полноты картины.


                                      Но в своих простых случаях я с проблемами взаимозаменяемости не столкнулся.


                                  1. Druu
                                    12.12.2019 07:59
                                    -2

                                    ФП без ТК это конечно хорошо, но — плохо.

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


                                    как они связаны с join и причем тут какие-то «законы»

                                    join вам в ФП не нужен, а законы не при чем вообще. На практике хватит одного бинда. Даже кривого.


              1. graninas
                10.12.2019 16:10

                1. mayorovp
                  10.12.2019 16:23

                  (комментарий был удалён)


                1. koldyr
                  10.12.2019 18:31

                  Я не настолько силён в английском. Итак, там нарушена ассоциативность, о чем внизу написано. Как это использовать, не взорвав себе голову.


    1. somebody4
      09.12.2019 18:21

      ни одного упоминания монад — спасибо
      Есть, если наведёте мышкой на ST, то увидите, она там прячется.


      1. PsyHaSTe Автор
        09.12.2019 18:24

        Ну вот, а я надеялся я хорошо её спрятал...


        1. SlimShaggy
          09.12.2019 22:47
          +4

          Для тех, кто читает с телефона — очень хорошо)


  1. AlienJust
    09.12.2019 11:01
    +2

    Грязный рантайм! Спасибо за статью.


  1. igrishaev
    09.12.2019 11:23

    Вам просто нужны неизменяемые коллекции и репл. Когда вы пишете класс в половину экрана ради элементарных действий, это никакое не ФП, а имитация.


    1. PsyHaSTe Автор
      09.12.2019 11:28

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


      Можете как-то подробнее написать о чем речь? При чем тут класс в половину экрана?


  1. S-e-n
    09.12.2019 11:43

    Из этой статьи я узнал, что писал в функциональном стиле на PHP4 14 лет назад.


    1. S-e-n
      09.12.2019 11:55
      +4

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


      1. PsyHaSTe Автор
        09.12.2019 12:24
        +1

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


        Кто развел всё это шаманство и мистицизм вокруг ФП не берусь судить. Берусь только немного развеять и показать, что всё очень приземленно, практично и полезно для живых разработчиков, а не только для высоколобых математиков.


    1. vesper-bot
      09.12.2019 16:58

      И я — на Паскале лет тридцать назад. ;)


  1. vintage
    09.12.2019 11:49
    +2

    единственное разумное использование такого аргумента — вернуть дефолтное значение когда массив пустой

    Найти первое недефолтное значение массива.


    1. PsyHaSTe Автор
      09.12.2019 12:31
      +1

      Нет, так не получится — у вас не определена операция равенства для T.


      1. ov7a
        09.12.2019 12:59

        Зависит от языка? В той же scala у всех есть equals(). Может имеется ввиду не равный, а идентичный (та же ссылка)?


        1. PsyHaSTe Автор
          09.12.2019 14:28

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



          Чтобы это сделать в нормальном языке вам нужно будет явно затребовать возможность сравнивать:


          bool Eq<T>(T a, T b) where T : IEquatable<T> => a.Equals(b);

          Хотя с языках с недостаточно сильной типизацией можно нахачить, но я как раз и говорю о том, что лучше их не использовать. В том же сишарпе можно обойти это через object.Equals/object.ReferenceEquals/..., но это скорее всего только выйдет боком (потом бегать выяснять, почему класс поменяли на структуру и всё сломалось).


          1. iskateli
            09.12.2019 23:30

            А что конкретно должен поддерживать язык, чтобы иметь «возможность работать с эффектами как значениями»?


            1. 0xd34df00d
              09.12.2019 23:39

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


  1. vintage
    09.12.2019 11:50

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

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


    1. some_x
      09.12.2019 15:59
      +2

      В таком случае код каждой функции (код которой вы не помните наизусть) перед использованием нужно обязательно тщательно изучить. Даже если это стандартная библиотека и функция выглядит как identity(v: T) => T, нужно обязательно проверить не эксплуатирует ли она баг в рантайме и не запускает ли ракету на Марс?

      Вряд ли вы так делаете для всех функций которые явно не подозреваете во вредительских намерениях?

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


      1. vintage
        09.12.2019 16:16

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


        1. PsyHaSTe Автор
          09.12.2019 16:19
          +1

          Если бы каждый раз когда функция с именем вроде GetItem обновляет записи в БД или глобальном стейте мне давали бы рубль я бы уже давно стал миллионером.


          1. vintage
            09.12.2019 16:26
            -1

            Где же ваша дедукция, которой вы так кичитесь? Префикс Get означает, что Item будет возвращён из базы в любом случае. А если его там ещё нет, то очевидно он должен быть тут же создан.


            1. PsyHaSTe Автор
              09.12.2019 16:28
              +11

              Префикс Get означает, что Item будет возвращён из базы в любом случае. А если его там ещё нет, то очевидно он должен быть тут же создан.

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


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


              1. vintage
                09.12.2019 16:55
                -3

                Это называется "абстракция". Клиента не волнует, что система делает там у себя под капотом, её задача выдать элемент по идентификатору. Если допускается его несуществование, то метод называется findItem, если не допускается — getItem. Яркий пример такой абстракции: https://ru.wikipedia.org/wiki/Touch


                1. PsyHaSTe Автор
                  09.12.2019 17:26
                  +2

                  Если допускается несуществование записи — метод обычно должен называться GetOrCreate или как-то схоже по смыслу. А еще лучше, когда по типам видно, что возвращается Option<T>, и понятно что происходит, если объект не нашелся.


                  1. vintage
                    09.12.2019 18:28
                    -3

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


                    1. vedenin1980
                      09.12.2019 18:40
                      +2

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

                      Во-вторых, getItem — должен вернуть либо Item, либо пустой элемент/null/ошибку, если item'а не существует. Создавать что-либо он не должен, для этого должны быть методы getItemOrCreate или getItemOrDefault.


                      1. vintage
                        09.12.2019 18:58
                        -3

                        getItem должен делать ровно то, что указано в названии. Не больше и не меньше. Не null возвращать ибо тогда он был бы getItemOrNull. Не ошибку кидать, иначе он был бы getItemOrThrows. А вот создавать ли новый, дефолтный или вообще прокси — скрыто за абстракцией.


                        1. vedenin1980
                          09.12.2019 19:14
                          +4

                          Угу, если вы спрашиваете кого-то «какая у тебя машина?», то вы разрешаете ему купить себе любую машину за вас счет, если у него ещё машины нет.
                          Боюсь, с таким чинильщиком абстракций, никакого ломальщика абстракций уже не нужно.


                          1. vintage
                            09.12.2019 19:20
                            -1

                            Если у него машины нет, но он подписал со мной контракт, что по моему запросу предоставит мне машину, то он должен её предоставить. Где он её достанет — его проблемы, а не мои. Может угнать, может купить за свой счёт, может собрать в гараже из мусора.


                            1. vedenin1980
                              09.12.2019 19:40
                              +6

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

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


                              1. vintage
                                09.12.2019 19:49
                                -6

                                Если контракт нарушен — паника и экстренное закрытие матрицы.


                                С каким "таким" подходом? Что за слова надо отвечать? Так я и не называют метод getItem, если не могу гарантировать его возврат.


                                1. vvzvlad
                                  09.12.2019 20:24
                                  +6

                                  Наркоман.


                                1. playermet
                                  10.12.2019 15:07
                                  +2

                                  А функция sendMessage (ну или getResponse) тоже должна либо стопроцентно гарантировать доставку по сети либо крашить приложение?


                                  1. vintage
                                    10.12.2019 15:18
                                    -5

                                    Если назовёте её deliveryMessage, то да, должна гарантировать.


                                    1. playermet
                                      10.12.2019 16:08
                                      +5

                                      Нужно срочно бежать переписывать API всех библиотек мира добавив им try в начало почти всех методов. Ну или крашить все приложения работающие с сетью при потере пакета.

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


                                      1. vintage
                                        10.12.2019 16:52
                                        -3

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


                            1. hydrates
                              10.12.2019 15:09
                              +8

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

                              И это при том, что у вас полный холодильник жратвы, есть и уха, и икра с маслом, есть всё кроме борща.

                              Но жена ваша проста — сказал «ещё борща» — запускаю протокол «борщ», еду по ночной Москве искать свёклу.

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

                              Такой пример, без заумных слов DDOS и RPS, покажется вам более полезным.


                              1. vintage
                                10.12.2019 15:25
                                -7

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


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


                                1. PsyHaSTe Автор
                                  10.12.2019 15:27
                                  +3

                                  А если борща вдруг не хватило, то это жена неправильная, ага


                                1. jayb19
                                  10.12.2019 16:51
                                  +1

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

                                  Правильно реализованная жена
                                  вообще не должа заниматься добычей свеклы, это должен делать «правильно реализованный» муж.
                                  P. S: Получается анти-паттерн «Отказ от ответственности».


                                  1. vintage
                                    10.12.2019 16:58
                                    -4

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


                                    1. jayb19
                                      10.12.2019 19:03
                                      +2

                                      Интересная картина мира получается. Интересная, но абсурдная. Правильный мир с правильными людьми долго не протянет. Если все будут майнить — пропадет смысл майнить. Proof-of-work без физического смысла в work. Неплохой сюжет для анти-утопии, не находите?


                                      1. vintage
                                        10.12.2019 19:26
                                        -1

                                        Надо продать этот сюжет Голивуду с интригующим названием "Borsch on Demand".


                                1. hydrates
                                  10.12.2019 23:53

                                  Собственно, такая жена это идеальный вариант, при условии:

                                  1) Ресурсы бесконечны
                                  2) Несвежий борщ вкусен

                                  Когда речь идёт о производительности, выраженной в RPS / Core, то все эти варианты с «запасом» уже не могут быть использованы.

                                  А в программировании я вообще ожидаю что Get это идемпотентная операция, которая не изменяет состояние системы.

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

                                  Если вы имеете ввиду, что в некоторых случаях нужно иметь возможность получить существующий «борщ» или сварить новый — используйте FindOrCreate, например. И любой другой разработчик поймёт что именно произойдёт при вызове метода, не глядя на сигнатуру.


                                  1. vintage
                                    11.12.2019 10:26

                                    в программировании я вообще ожидаю что Get это идемпотентная операция, которая не изменяет состояние системы

                                    Она не изменяет видимого состояния системы. Внутреннее состояние меняется всегда (банально пишутся логи).


                                    1. PsyHaSTe Автор
                                      11.12.2019 11:35
                                      +2

                                      То есть вы ожидаете что Get какой-нибудь сущности типа "дай мне текущее время" упадет с HttpException потому что он полез на сервер часы синхронизировать?


                                      1. vintage
                                        11.12.2019 13:32
                                        -1

                                        Как раз таки не упадёт и выдаст мне его в любом случае.


                                        1. PsyHaSTe Автор
                                          11.12.2019 13:47
                                          +2

                                          Что он выдаст? У него же нет ничего. Он пошел за борщём, а его не оказалось. Теперь поехал за ним на рынок, через пару часов может вернется (если таймаут раньше не упадет).


                                          Или может не стоило ему так делать?


                                          1. vintage
                                            11.12.2019 13:59
                                            -3

                                            Ага, в оффлайне все часы останавливаются.


                        1. Vilaine
                          12.12.2019 07:31

                          Похоже на спор конвенций. В вашей конвенции, по-видимому, getItem() используется редко, а вместо него разработчики чаще всего используют getItemOrNull() или hasItem(), т.к. это самый типичный юзкейс. А вот случаи, когда создание запрашиваемого инстанса лежит в зоне ответственности класса/модуля, по-моему, редки. Если не лепить сквозные зависимости типа getItem(itemFactory).
                          Но чаще всё же превалирует конвенция getItem() без видимых сайд-эффектов и getOrCreateItem(тут обычно полные данные), поэтому новичкам в вашем проекте нужно больше времени для адаптации (дороже).


                          1. vintage
                            12.12.2019 11:22

                            Если уж говорить про мою конвенцию, то я пишу просто Item безо всяких get. Выглядит это так:


                            @ $mol_mem_key
                            User( id : string ) {
                                return new this.$.$my_user( id )
                            }


                            1. PsyHaSTe Автор
                              12.12.2019 11:53

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


                              1. vintage
                                12.12.2019 12:33
                                -2

                                А есть другой гайдлайн — называть каналы существительными, а экшены — глаголами. Даже если и те и другие — методы объекта.


                              1. Aldrog
                                12.12.2019 13:24

                                Если в языке нет отдельного понятия property, совершенно нормально называть методы-геттеры существительными.


                                1. mayorovp
                                  12.12.2019 13:37

                                  А если есть?


                                  1. vintage
                                    12.12.2019 15:53

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


                    1. fori
                      09.12.2019 19:11

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

                      Именно в ФП клиента не волнует что внутри функции если тип возвращаемого значения T. А вот в императивных языках нет такого четкого контракта, приходится везде проверять на null либо потом ловить NRE в runtime, вместо compile time проверки.


                      1. vintage
                        09.12.2019 19:14
                        +1

                        В нормальных императивных языках есть чёткие контракты и non-nullable типы.


                        1. PsyHaSTe Автор
                          09.12.2019 19:18

                          А можно пример этих нормальных языков? А то на вскидку только Swift на ум приходит.


                          1. vintage
                            09.12.2019 19:32

                            TypeScript, например. Ну и в принципе легко добавляется в любой язык с дженериками.


                            1. bingo347
                              09.12.2019 23:45

                              TypeScript, например.
                              Вот написали Вы либу с надеждой на TypeScript, а я стал ее использовать в проекте без strictNullChecks или вообще с чистым JavaScript… а потом буду разбираться со стректрейсом, который полностью в node_modules из-за того, что какой-то умник не проверил на null понадеявшийся на «строгий» (нет!) компилятор…
                              И таких библиотек на npm в последнее время стало очень много…

                              Ну и в принципе легко добавляется в любой язык с дженериками.
                              А можно пример такого добавления?



                              1. vintage
                                10.12.2019 11:46
                                -2

                                я стал ее использовать в проекте без strictNullChecks или вообще с чистым JavaScript

                                Ну и ССЗБ.


                              1. jayb19
                                10.12.2019 17:03
                                -1

                                Как по мне, то правильная документация намного удобнее чем строить замудренные конструкции только ради исключения возможности выстрелить в ногу. В реальном мире возможно использовать предметы не по назначению. Но мы же не стреляем себе в голову, так как последствия известны — «задокументированы».
                                С серьезным кодом я не работал, так что могу быть неправ.


                                1. PsyHaSTe Автор
                                  10.12.2019 17:08
                                  +2

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


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


                                  1. jayb19
                                    10.12.2019 19:45

                                    Согласен. Так сложилось, что в крупных проектах без ограничений не обойтись, но ведь проектов(особенно некомерческих), разрабатываемых небольшой группой куда больше. Именно про этот сегмент я писал.
                                    Начитавшись всяких «best practices» многие с умным видом пишут избыточный код, попросту забыв про KISS. Универсального подхода попросту не может быть, все зависит от ситуации.

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


                                    1. PsyHaSTe Автор
                                      10.12.2019 19:57
                                      +1

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


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


                                      1. jayb19
                                        10.12.2019 21:31

                                        Не могу понять как это связано с системой типов. Можно вернуть ожидаемый результат и отослать емэйл. От кривой реализации панацеи нет.
                                        Видимо неправильно сформулировал вопрос:
                                        Для кого введены жесткие ограничения? Машина выполняет только заложеные человеком комманды. Следовательно и типы и приватные поля существуют для человека. Опишу на примере JS: простых приватных полей там нет, и многие изгибаются реализуя их на замыканиях пытаясь спрятать — вопрос: от кого? От себя? Ведь давным-давно условились что поля с префиксом "_" являются приватными, и это всем известно. Но нет, кто как может пишет мудреный код, теряя читабельность и свое время. Зачем нужна эта защита от дурака?
                                        P. S: Возможно я задаю себе(и Вам) слишком глубокие вопросы уходящие корнями в психологию и философию, на которые невозможно ответить сразу.


                                        1. vintage
                                          10.12.2019 21:35

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


                                        1. 0xd34df00d
                                          10.12.2019 22:11
                                          +1

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

                                          Только если у вас функция живёт в IO. Если у вас функция имеет тип MonadDatabase m => Int -> m (Maybe Item), то она не может сделать ничего кроме того, что даёт MonadDatabase.


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

                                          Не понял, в чём мудрёность приватных полей (или полей, не экспортируемых из модуля)?


                                          Зачем нужна эта защита от дурака?

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


                                          Но это в случае приватных полей. А вообще именно ограничения позволяют не допускать ошибок даже самому себе. Опыт показывает, что, да, приходится чуть больше думать над архитектурой, как тут бы прокинуть логгер/БД/етц, но в итоге архитектура получается куда более чистой, low coupling/high cohesion, реюзабельной, поддерживаемой, все дела.


                                        1. PsyHaSTe Автор
                                          10.12.2019 23:06
                                          +1

                                          Не могу понять как это связано с системой типов. Можно вернуть ожидаемый результат и отослать емэйл. От кривой реализации панацеи нет.

                                          Ну вот вам система типов не позволяет вместо строк передавать числа и наоборот? Точно так же система типов может запретить отсылать емейлы, когда вы этого не хотите. Товарищ 0xd34df00d выше расписал как раз этот сценарий.


                          1. ikovalyov
                            10.12.2019 13:43

                            Kotlin


                    1. mayorovp
                      09.12.2019 19:25

                      Увы, но эта самая протечка обусловлена не архитектурой приложения, а характеристиками СУБД и нефункциональными требованиями к ПО. Клиенту, может быть, и правда не важно появилась запись в БД или нет (при сохранении прочих инвариантов) — но вот просадку производительности так просто не заабстрагировать.


                      1. vintage
                        09.12.2019 19:34

                        Подписал контракт — изволь исполнять. Хочешь сэкономить — подписывай иной контракт. Или вы предлагаете делать не то, на что подписались?


                        1. mayorovp
                          09.12.2019 19:54
                          +4

                          Я предлагаю включать в контракт действительно важную информацию, а не отмахиваться от неё на основании "ну, то же абстракция!"


                          В частности, функция GetItem не должна ничего писать в БД. Это и есть её контракт. Который, увы, иногда нарушается, потому что никто не следит.


                          1. vintage
                            09.12.2019 20:07
                            -3

                            Такой контракт ничего не говорит о возможности или невозможности писать в СУБД. Ок, вот вам простой пример. Вам нужно получить сессию, вы пишете что-нибудь типа SessionManager.get(). Если сессия автоматически создаётся заранее, то вам будет просто возвращён её объект. Если же SessionManager решили сделать ленивым, то метод get будет создавать сессию под капотом в момент первого обращения. В какой момент будет запись в бд — спрятано за абстракцией SessionManager. Это не ваше дело в какой момент ему ходить в свою же базу.


                            1. mayorovp
                              09.12.2019 20:13
                              +2

                              Во-первых, я бы метод с описываемой вами функциональностью назвал start, а не get.


                              Во-вторых, в чём смысл хранить в базе пустые сессии?


                              1. vintage
                                09.12.2019 20:18
                                -1

                                start предполагает начало какого-то процесса. Тут же сессия уже может уже быть, может ещё не быть — в контракте это никак не ограничено. Present Simple — она просто есть, как безвременная абстракция.


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


                                1. mayorovp
                                  09.12.2019 21:51
                                  -1

                                  А что будет после "киляния"? Автоматически начнётся новая? Так всё-таки, в чём смысл пустой сессии?


                                  1. KanuTaH
                                    09.12.2019 22:09

                                    Так сессии не обязательно будут пустые. Нопремер, в TLS с его session id cache, для последующих соединений с того же клиента могут заранее генериться сессионные ключи, которые потом будут по очереди выдаваться.


                                    1. mayorovp
                                      09.12.2019 22:13

                                      Так это вариант существующей сессии же, я говорю про другой случай.


                                      1. KanuTaH
                                        09.12.2019 22:16

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


                            1. Cerberuser
                              10.12.2019 18:09
                              +1

                              Вам нужно получить сессию, вы пишете что-нибудь типа SessionManager.get()

                              Я пишу SessionManager.getOrCreate() (как это сделано в Spark, например). И заранее знаю, что он именно что её гарантированно выдаст — или уже существующую, или новую, — именно потому что и в названии функции это чётко сказано, и сигнатура не подразумевает неудачного исхода (хотя и допускает выброс исключения, да, это всё-таки JVM).


                              1. vintage
                                10.12.2019 18:36
                                -2

                                Если сессия безусловно создаётся мидлварой ещё на подлёте к вашему обработчику, то имя getOrCreate врёт, так как при её вызове собственно create не происходит никогда.


                                1. Cerberuser
                                  10.12.2019 19:05
                                  +2

                                  Нет, если она к моменту вызова get гарантированно существует — тогда всё логично, не спорю. Но Вы же выше описывали случай, когда она может не существовать и создаваться по запросу, или я что-то не так понял?


                                  1. vintage
                                    10.12.2019 19:32
                                    -2

                                    Может создаваться, а может и не создаваться. Абстракцией это никак не ограничивается, что позволяет реализации самой решать, когда создавать. Inversion of Control, все дела.


            1. AlexeyKi
              10.12.2019 15:09
              +1

              Не обязательно так


              1. vintage
                10.12.2019 15:30
                -3

                Конечно, но мы же тут играем в детективов, не мешайте.


  1. qw1
    09.12.2019 11:51
    +1

    А как записать алгоритм, по которому надо покупать кофе, пока не кончатся деньги?

    for (;;) {
        cup = new Coffee();
        if (card.Balance < cup.Price) break;
        resultList.Add(cup);
        card.Charge(cup.Price);
    }

    Получается, что чистая функция должна вернуть не просто список изменений, который нужно применить к Card, а алгоритм, изменяющий Card определённым образом и строящий resultList? Но тогда мы входим в рекурсию — наш алгоритм возвращает алгоритм, который нужно интерпретировать.


    1. PsyHaSTe Автор
      09.12.2019 12:06

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


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


      var currentCharge = Charge.Empty(card);
      for (;;) {
          var (cup, charge) = Cafe.BuyCoffee();
          var newCharge = charge.Combine(currentCharge);
          if (card.Balance < newCharge.Amount) break;
          resultList.Add(cup);
          currentCharge = newCharge;
      }


      1. Deosis
        09.12.2019 12:32

        Можно использовать рекурсию:


        Buy(card, coffies, charge){
            var cup = new Coffee();
            return card.Balance < charge.Amount + cup.Price
                ? (coffies, charge)
                : Buy(card, coffies.Push(cup), charge.Combine(new Charge(card, cup.Price));
        }


      1. qw1
        09.12.2019 12:35

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

        То есть, по сути, вместо того, чтобы передавать во все функции мутабельный объект card, нужно передавать начальное состояние originalCard и отдельно все объекты-изменёния его полей (например, за изменение Balance пусть отвечает Charge, за изменение owner пусть отвечает некий Renamer и т.п.)

        Вместо простой сигнатуры BuyCoffee(Card) -> (List), мы делаем BuyCoffee(Card, Charge) -> (Charge, List), которая показывает, что была исходная карта и набор списаний, а получен список покупок и расширенный набор списаний (который можно применить к исходной карте).

        Тут пока только одно действие с картой. А если типов действий десяток, так и таскать между всеми функциями исходное состояние и все списки применённых действий?

        Самое главное. Мы это делаем для упрощения работы программиста. Не скажу за других, но императивный код
        if (card.Balance >= cup.Price) card.Balance -= cup.Price;
        для меня понятнее, чем введение новый сущностей (Charge) и операций с ними (Charge.Combine). И такие сущности надо вводить на каждую мелочь, чтобы сохранить чистоту функций?


        1. PsyHaSTe Автор
          09.12.2019 12:44

          Я так понимаю, currentCharge не должен создаваться в этой функции, а должен передаваться снаружи и изменённый отдаваться обратно.

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


          Поэтому дальнейшие рассуждения немного некорректны.
          Было у вас BuyCoffee(Card, IPaymentProvider) -> (List)
          Стало BuyCoffee(Card) -> (List, Charge).


          Если действий десяток, то у вас на выбор: сделать ADT энум "один из вариантов действия с картой", сделать какой-нибудь аггрегатор с кастомной логикой который что-то подобное сделает или что-то еще. Звучит сложно, наверное, но на самом деле таковым не является.


          Самое главное. Мы это делаем для упрощения работы программиста. Не скажу за других, но императивный код
          if (card.Balance >= cup.Price) card.Balance -= cup.Price;
          для меня понятнее, чем введение новый сущностей (Charge) и операций с ними (Charge.Combine).

          Ну если вам проще, то можно мутировать локальное состояние через ST, я ж говорил :) Главное не нарушить прозрачность. А нарушение прозрачности это плохая штука, я выше вроде показывал какие проблемы оно вызывает. Да и рассуждения в терминах трансформаций данных со временем помогают лучше представлять, что в коде происходит. Не надо думать "так, сейчас пятая итерация, какое состояние у карты там? А какое значение было когда метод был вызван? Забыл..."


          И такие сущности надо вводить на каждую мелочь, чтобы сохранить чистоту функций?

          А вот тут вы и подбираетесь к монадам. Да, формально вам нужно такие функции писать для каждого типа. И именно для того чтобы не заниматься лишней писаниной, монады и изобрели. Ну примерно как придумали виртуальные функции, чтобы не писать if employee.Type == "Manager" { ... }.


          1. qw1
            09.12.2019 12:48

            Да нет, совершенно нормально создать его в функции. Это же просто начальный элемент. Как единица для факториала, вам не надо передавать её снаружи
            Так откуда мы узнаем, сколько денег с карты уже списано?
            Если я вызову 10 раз ф-цию BuyCoffeesForAllMoney(Card), то она 10 раз и купит одно и то же, я же ожидаю от второго и последующих вызовов, что денег на карте больше нет и ничего купить нельзя.


            1. PsyHaSTe Автор
              09.12.2019 13:14

              Так откуда мы узнаем, сколько денег с карты уже списано?

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


              Если же мы хотим всё же "поменять" значение то нам подойдет монада State, тогда мы сможем запомнить что куда списали до того как до этого момента дошли. Но это опять в монады углубляться. Просто монады это как интерфейсы и паттерны в ООП, любой нетривиальный вопрос — и вы в них попадаете.


              Если я вызову 10 раз ф-цию BuyCoffeesForAllMoney(Card), то она 10 раз и купит одно и то же, я же ожидаю от второго и последующих вызовов, что денег на карте больше нет и ничего купить нельзя.

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


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


              1. qw1
                09.12.2019 13:18

                Вот именно то что вызов одной и той же функции может дать разный результат и есть плохой сценарий, от которого стоит избавляться. Функция просто должна возвращать значение, и ничего более
                Вот потому у Вирта есть функции и процедуры.
                Вызывая последовательно процедуры BuyCola и BuyCoffeesForAllMoney я ожидаю, что сначала будет куплена кола, а на остаток денег — кофе. Это просто и естественно. Без новых сущностей, таких как Charge.

                Если же «всё есть функция», надо думать по-другому.


                1. PsyHaSTe Автор
                  09.12.2019 13:22

                  Если же «всё есть функция», надо думать по-другому.

                  Да. надо думать по-другому. Это несет только плюсы. Старые представления стоит время от времени пересматривать. Чистые функции проще тестировать, проще композировать, и обычно проще писать.


                1. gecube
                  09.12.2019 22:49
                  +2

                  Вирт и прочие коллеги, которые разрабатывали ЯП тех времён вероятно очень большую дыру в абстракции допустили. Ибо технически как преподают — никакой разницы между функциями и процедурами нет. Только первые возвращают значение. И в результате в головах у учеников нет понимания почему нужно использовать одно или другое. Что ещё хуже — передача параметров по ссылке, а не по значению. Для эффективности — это не плохо, но потом резко выясняется, что аргумент функции или процедуры мутабелен. Жесть. С/с++ этим же страдают в полный рост. Поглядите на какие-нибудь Win32API или стандартную библиотеку языка. Пойди без поллитра разберись что происходит. Поэтому более абстрактные языки новых поколений — это прекрасно. Твои ожидания от того, как будет выполняться код ПОЧТИ совпадают с реальностью...


                  1. 0xd34df00d
                    09.12.2019 22:52
                    +1

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


                    1. gecube
                      09.12.2019 23:25

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


                      А сейчас теория продвинулась. Вычислительные мощности выросли на порядки. И появились предпосылки для написания… Более математических программ


                    1. mayorovp
                      10.12.2019 06:11

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


      1. 0xd34df00d
        09.12.2019 18:21

        На хаскеле я бы сделал GADT для описания операций с картами и потом обмазался бы свободными монадами.


    1. qw1
      09.12.2019 13:12

      Здесь вы выкрутились тем, что можно предсказать успех операции в зависимости от её параметров: суммы покупки.

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

      Как вот такой алгоритм перевести в «чистый»?

      for (;;) {
          cup = new Coffee();
          if (card1.TryPay(cup.Price)) {
              resultList.Add(cup);
          } else if (card2.TryPay(cup.Price)) {
              resultList.Add(cup);
          } else {
              break;
          }
      }


      1. PsyHaSTe Автор
        09.12.2019 13:18
        +1

        Ну тут уже придется расчехлять монады (в сишарпе монадический do-синтаксис спрятан за async/await поэтому использую его):


        for (;;) {
            cup = new Coffee();
            if (await card1.TryPay(cup.Price)) {
                resultList.Add(cup);
            } else if (await card2.TryPay(cup.Price)) {
                resultList.Add(cup);
            } else {
                break;
            }
        }

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


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


        1. qw1
          09.12.2019 13:27

          Это гениально!

          Достаточно любую процедурную лапшу обвешать async/await, и получим чистую функцию, которую легко сопровождать и в которой минимум ошибок, ведь это теперь ФП!


          1. PsyHaSTe Автор
            09.12.2019 14:31

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


            Зато явность авейтов часто помогает недопустить ошибку которую я сделал. Если бы в том коде было вот такое:


            var something = await function();
            await DoStuff(this.Field, something);

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


            1. qw1
              09.12.2019 14:39

              я бы знал, что переставлять эти строчки может быть опасно.
              А может и не опасно. Нет никакой гарантии, что в этом коде function меняет this.Field.

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


              1. PsyHaSTe Автор
                09.12.2019 14:52

                Так тут фишка как с unsafe в расте. Если я вижу явный забор "опасно" я пойду проверять.


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


                1. qw1
                  09.12.2019 19:42
                  +1

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


                  1. PsyHaSTe Автор
                    10.12.2019 16:35
                    +2

                    Скорее авейт говорит, что у кода обязательно есть побочные последствия. В зависимости от монады это может быть разный эффект. В State это будет изменение стейта, в IO асинхронный запрос куда-то, в Writer запись в лог, и так далее.


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


                    1. qw1
                      10.12.2019 21:25

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

                      Сейчас я слабо представляю, как написать State через await-ы, но интуитивно мне кажется неправильным писать в State через await-ы, а читать его напрямую обращением к изменившемуся полю, а не через другую async-функцию. То есть, ваш пример конкретно с this.Field и await-ами мне кажется несколько надуманным.


                      1. vintage
                        10.12.2019 21:41
                        -1

                        Способ перенести управление во вне — это скорее генераторы. А async/await — не более чем частный их случай.


                        1. qw1
                          10.12.2019 22:47

                          Тут ещё вопрос, кто есть частый случай кого.
                          1. Весь генератор должен yield-ить из одной функции, когда await-ы могут вызываться и из вложенных.
                          2. Генератор привязан строго к одному типу значений, когда типизация await-ов намного богаче.
                          3. Внешний код не может передавать данные внутрь генератора по мере выдачи им значений, чтобы влиять на работу генератора.


                          1. Cerberuser
                            11.12.2019 08:19

                            Весь генератор должен yield-ить из одной функции, когда await-ы могут вызываться и из вложенных.

                            Как минимум несколько раз я видел возможность написать что-то вроде yield* anotherGenerator() (пример из JavaScript).


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

                            А если может (см. как минимум всё тот же JavaScript) — это уже не генератор, а корутина получается?


                            1. qw1
                              11.12.2019 08:53

                              это уже не генератор, а корутина получается?
                              Ну так комментатор выше утверждал, что генератор — более общий случай. Значит, должен уметь всё, что и корутины.


                              1. Cerberuser
                                11.12.2019 09:23

                                Я сейчас чисто терминологически пытаюсь понять: граница проходит именно по возможности сказать не просто next(), а next(passed_value)?


                                1. qw1
                                  11.12.2019 09:51
                                  -1

                                  Нет. От await можно ожидать переключения synchronization context (например, продолжения в другом потоке).

                                  А генератор — просто функция. Не чистая, то есть со своим внутренним состоянием, но в целом ничем не отличающаяся от любой другой функции.


                                  1. mayorovp
                                    11.12.2019 10:33

                                    Так-то yield тоже может переключить контекст. Никто же не обязывает вызывать MoveNext в одном и том же контексте...


                          1. mayorovp
                            11.12.2019 08:49

                            Весь генератор должен yield-ить из одной функции, когда await-ы могут вызываться и из вложенных.

                            Нет, не могут. Вы принимаете за возможность await-тов возможности Task-ов.


                            1. qw1
                              11.12.2019 08:54

                              Да, точно…


                          1. vintage
                            11.12.2019 10:39

                            1. yield и await — одно и то же. async function и generator function — тоже. Вся разниа в том, то async/await передаёт между функциями промисы, а генераторы могут передавать что угодно.
                            2. Не привязан он ни к чему. Вы так же можете возвращать типизированный промис, а внешняя функция будет вас вызывать, когда он отрезолвится.
                            3. Может.


                            1. qw1
                              11.12.2019 11:04

                              3. Может
                              Как это выглядит синтаксически?


                              1. vintage
                                11.12.2019 11:33

                                1. qw1
                                  11.12.2019 11:56
                                  +1

                                  Ну это не встроенная в язык конструкция, как yield в c#. Своих велосипедов можно наделать сколько угодно, назвать их как угодно. Например, сделать Singleton.GetInstance, каждый раз возвращающий новый объект и на основе этого утверждать, что Singleton так и работает.


                                  1. mayorovp
                                    11.12.2019 12:30

                                    Это именно что использование встроенной в язык конструкции. Эта возможность есть в С++, Javascript и Python, но отсутствует в C# и в Kotlin


                                    1. qw1
                                      11.12.2019 12:37

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

                                      Если любую функцию next() называть генератором, то зачем вообще вводить новый термин?


                                      1. mayorovp
                                        11.12.2019 12:40

                                        Генератор — это сопрограмма (обычно безстековая), которая императивно пушит "наружу" последовательность значений при помощи yield-подобной конструкции.


                                        Любую функцию next назвать генератором нельзя.


                                        1. qw1
                                          11.12.2019 13:32

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


                                          1. mayorovp
                                            11.12.2019 13:35

                                            По ссылке выше как раз "наружа"


                                            1. qw1
                                              11.12.2019 13:42

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


                                      1. vintage
                                        11.12.2019 13:37

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


                                        Коллекции — это про итераторы. Итераторы можно реализовать через генераторы, а можно и через обычные функции.


                                        1. qw1
                                          11.12.2019 13:44

                                          Снаружи «ret = gen.next(res);»
                                          А внутри, допустим, «yield 5;».
                                          Или там не «yield 5;», а что-то типа «var localRes = yield 5;»?
                                          Как параметр res попадает внутрь генератора?


                                          1. mayorovp
                                            11.12.2019 14:01

                                            Именно так, let x = yield 5


                                            1. qw1
                                              11.12.2019 14:34

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

                                              Что как-то нелогично. Было бы удобно, например, написать генератор случайных чисел и синтаксисом next(N) получать очередное случайное число в интервале (0,N).


                                              1. mayorovp
                                                11.12.2019 14:52

                                                А что мешает?


                                                function rnd() {
                                                    let next = NaN;
                                                    for(;;) {
                                                        let N = yield next;
                                                
                                                         // генерируем случайное число в интервале (0, N)
                                                
                                                        next = ...;
                                                    }
                                                }

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


                                                1. qw1
                                                  11.12.2019 16:55

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

                                                  function myRandom(maxValue) {
                                                      let dummy = rnd(maxValue);
                                                      return rnd(0);
                                                  }


                                                  1. mayorovp
                                                    11.12.2019 16:58

                                                    Зачем 2 раза-то?


                                                    let dummy = rnd();
                                                    dummy.next();
                                                    
                                                    function myRandom(maxValue) {
                                                        return dummy.next(maxValue).value;
                                                    }

                                                    А создавать новый ГСЧ для получения каждого следующего числа ни в одном языке не рекомендуется.


                                                    1. qw1
                                                      11.12.2019 17:35

                                                      Да, это я не заметил, что получать значение надо через next(N).

                                                      Но как это работает?
                                                      Когда клиет вызывает
                                                      dummy.next(maxValue)
                                                      фукция-генератор работает до следующего yield, т.е. до строки
                                                      let N = yield next;
                                                      выражение в yield возвращает клиенту как результат next(maxValue), а параметр maxValue присваивает переменной N.
                                                      При этом, «генерируем случайное число в интервале (0, N)» находится ниже по коду.

                                                      И при первом вызове
                                                      dummy.next();
                                                      функция выполняется до
                                                      let N = yield next;
                                                      откуда будет взято значение N?


                                                      1. mayorovp
                                                        11.12.2019 18:22

                                                        На инструкции yield выполнение сопрограммы останавливается. Пока не будет сделан вызов next — yield не вернет управления.


                                                        А next, в свою очередь, не вернет управления пока не выполнение сопрограммы не дойдет до yield.


                                                        1. qw1
                                                          11.12.2019 19:57

                                                          Ну так из этого следует, что
                                                          return dummy.next(maxValue)
                                                          возвращает значение не для переданного здесь maxValue, а для предыдущего?


                                                          1. qw1
                                                            11.12.2019 21:02

                                                            Поигрался с этим в браузере.
                                                            Получается, что параметры next() сдвинуты ровно на 1 yield. Например, фукция

                                                            function * rnd() {
                                                                console.log("start");  
                                                                let x1 = yield 0;
                                                                console.log("x1="+x1);
                                                                let x2 = yield x1*2;
                                                                console.log("x2="+x2);
                                                                let x3 = yield x2*3;
                                                                console.log("x3="+x3);
                                                            }
                                                            Выполняется так:
                                                            z=rnd();
                                                            z.next(5); // параметр 5 не используется
                                                                                    console.log("start");  
                                                                                    >> "start"
                                                                                    let x1 = yield 0;
                                                                                    >> z.next(5).value = 0
                                                            // хотя x1=yield 0; выполнен,
                                                            // значение x1 пока не задано
                                                            // вот это меня и смущало!
                                                            z.next(6); // параметр 6 передаётся в x1
                                                                                    console.log("x1="+x1);
                                                                                    >> "x1=6"
                                                                                    let x2 = yield x1*2;
                                                                                    >> z.next(6).value = 12
                                                            z.next(7); // параметр 7 передаётся в x2
                                                                                    console.log("x2="+x2);
                                                                                    >> "x2=12"
                                                                                    let x3 = yield x2*3;
                                                                                    >> z.next(7).value = 21
                                                            ...
                                                            </souce>


                                                            1. mayorovp
                                                              11.12.2019 21:28

                                                              Пока не будет сделан вызов next — yield не вернет управления.


                                                              1. qw1
                                                                11.12.2019 22:12

                                                                Это понятно.
                                                                Неочевидно было, что вызов next(5), который вернул 6 из let x = yield 6;
                                                                это самое значение 5 не пишет в переменную x, а только параметр следующего next будет записан в x.


                                                                1. mayorovp
                                                                  11.12.2019 22:31

                                                                  Как это может быть неочевидно? Сопрограмма "висит" на yield 6, и её разбудит только следующий вызов next. Он и определит значение x. Это единственное логичное поведение...


                                                                  1. qw1
                                                                    11.12.2019 22:45

                                                                    let x = yield 6 уже вернул значение, и мне казалось, что логично внутренний instruction pointer передвинуть на следующий оператор, а этот считать выполненным. И переменную x заполнить тем значением, которое было параметром вызова next, который получил 6. А так, получается, yield выполнен наполовину: expression справа от yield вычислено и возвращено, но результат yield не будет известен до следующего next. А параметр первого next вообще вылетает в никуда.

                                                                    Хотя, этот дизайн можно понять: для клиента удобнее, чтобы параметр next повлиял на значение ф-ции next.


                                                                    1. mayorovp
                                                                      11.12.2019 23:06

                                                                      А так, получается, yield выполнен наполовину

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


                                                                      1. qw1
                                                                        11.12.2019 23:30

                                                                        Интерпретатору удобнее поставить instruction pointer после yield и остановить выполнение. В похожем положении находится корутина сразу после создания: первая инструкция — это не yield, однако корутина заморожена.

                                                                        Хотя, конечно нет никакого instruction pointer-а: корутина компилируется в state-машину, где стейты соотносятся с инструкциями весьма условно, поэто наверное нет большой разницы в реализации, поделён yield между стейтами или нет.


                                                                        1. Cerberuser
                                                                          12.12.2019 06:36
                                                                          +1

                                                                          Если я правильно понимаю, загвоздка в том, что let x = yield y; семантически представляет собой не одну, а две инструкции: собственно yield и присваивание. И "после yield" как раз и значит "между yield и присваиванием".


                                                                          1. qw1
                                                                            12.12.2019 09:47

                                                                            Не совсем. Загвоздка в том, что один yield (независимо от присваивания) выполняется за два клиентских next(): в первый next он вычисляет выражение справа и отдаёт его в результат next, затем ждёт второй next и параметр второго next становится результатом yield. Только после этого yield можно считать выполненным.


                                                                            1. mayorovp
                                                                              12.12.2019 10:42

                                                                              Точно так же как и next выполняется "за два yield". Это ожидаемо и симметрично.


                                                                              1. qw1
                                                                                12.12.2019 10:50

                                                                                Если бы не фича с параметром (о которой я вчера узнал)), можно было бы сделать сопоставление один-к-одному next и yield. Именно это и было ожидаемо.


                                                                    1. vintage
                                                                      12.12.2019 07:03
                                                                      +2

                                                                      Вычисление оператора и запись в переменную — разные инструкции.


                                                                      let x = (yield 5) + (yield 6)


                                                                      1. qw1
                                                                        12.12.2019 09:49

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


                                                                        1. Druu
                                                                          12.12.2019 15:08

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

                                                                          Ну, например, если yield отправляет некоторый запрос, а в next(...) мы засовываем ответ на него (аналога async/await).


                                                                          можно было бы сделать сопоставление один-к-одному next и yield. Именно это и было ожидаемо.

                                                                          Они 1 к 1 и сопоставляются, кроме первого .next(), который можно интерпретировать как .start()


                                          1. vintage
                                            11.12.2019 15:57
                                            -1

                                            Вот так и попадает. next помещает переданное значение в стек и вызывает генератор, который в зависимости от счётчика у себя внутри делает goto на следующую после yield инструкцию.


                        1. mayorovp
                          11.12.2019 09:20

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


                          В C++ Coroutines TS, к примеру, именно co_await является основным механизмом.


                      1. PsyHaSTe Автор
                        10.12.2019 22:59

                        Сейчас я слабо представляю, как написать State через await-ы, но интуитивно мне кажется неправильным писать в State через await-ы, а читать его напрямую обращением к изменившемуся полю, а не через другую async-функцию. То есть, ваш пример конкретно с this.Field и await-ами мне кажется несколько надуманным.

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


                        Слава Богу, у LINQ такой проблемы нет, это полноценный do-синтаксис, так что на нем стейт монаду спокойно реализовать можно:


                        State<int, Unit> s =
                            from state in State.Get<int>()
                            let newState = state + 547
                            from _ in State.Set(newState)
                            select Unit.Instance;
                        
                        Console.WriteLine(s.Run(5));

                        Полный код можно найти по ссылке: https://wandbox.org/permlink/Thcf0CPQparKVQiG


                        Да, первоначально выглядит немного чужеродно, "как же так, ведь from .. in это для итераторов!", но на самом деле это не правда. from x in y это полный аналог хаскеллевского x <- y.




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


                        1. qw1
                          11.12.2019 00:09

                          Не очень понял, что с этим делать.
                          Допустим, я хочу написать «чистую» функцию с аргументом int a, которая увеличит state на 1, если a > 0, на 2, если a = 0, и на 3, если a < 0.


                          1. PsyHaSTe Автор
                            11.12.2019 00:14
                            +2

                            Ну вот так например:


                            State<int, Unit> Foo(State<int, Unit> state) =>
                                from a in State.Get<int>()
                                let valueToSet = a > 0 ? 1 : a == 0 ? 2 : 3
                                from _ in State.Set(valueToSet)
                                select Unit.Instance;

                            Запускаем:


                            var initialState = State<int, Unit>.Return(Unit.Instance);
                            Console.WriteLine(Foo(initialState).Run(5));
                            Console.WriteLine(Foo(initialState).Run(0));
                            Console.WriteLine(Foo(initialState).Run(-5));

                            Получаем


                            (ConsoleApp28.Unit, 1)
                            (ConsoleApp28.Unit, 2)
                            (ConsoleApp28.Unit, 3)


                            1. qw1
                              11.12.2019 00:24

                              Но тут параметр 'state' не используется, а параметра 'a' я не вижу в сигнатуре функции.


                              1. PsyHaSTe Автор
                                11.12.2019 00:30

                                Неправильно распарсил. Тогда так.


                                State<int, Unit> Foo(int a) => 
                                    State.Set(a > 0 ? 1 : a == 0 ? 2 : 3);

                                Можно было бы передавать старый стейт, но он все равно затирается, поэтому не нужен.


                                1. qw1
                                  11.12.2019 00:32

                                  Тут он ставит новое состояние, а не увеличивает текущее.
                                  Нужна сигнатура

                                  static State<int, Unit> Foo(State<int, Unit> state, int a) =>


                                  1. PsyHaSTe Автор
                                    11.12.2019 00:34
                                    +1

                                    Простите, что-то я туплю. Вот так конечно же:


                                    static State<int, Unit> Foo(int a, State<int, Unit> state) =>
                                        from stateValue in State.Get<int>()
                                        let increment = a > 0 ? 1 : a == 0 ? 2 : 3
                                        from _ in State.Set(stateValue + increment)
                                        select Unit.Instance;

                                    var initialState = State<int, Unit>.Return(Unit.Instance);
                                    Console.WriteLine(Foo(5, initialState).Run(10));
                                    Console.WriteLine(Foo(0, initialState).Run(20));
                                    Console.WriteLine(Foo(-5, initialState).Run(30));

                                    получаем:


                                    (ConsoleApp28.Unit, 11)
                                    (ConsoleApp28.Unit, 22)
                                    (ConsoleApp28.Unit, 33)


                                    1. qw1
                                      11.12.2019 00:38

                                      Не получается написать тест. Должно быть 105, у меня пишет 103

                                      Скрытый текст
                                      static State<int, Unit> Foo(int a, State<int, Unit> state) =>
                                          from stateValue in State.Get<int>()
                                          let increment = a > 0 ? 1 : a == 0 ? 2 : 3
                                          from _ in State.Set(stateValue + increment)
                                          select Unit.Instance;
                                      
                                      static void Main(string[] args)
                                      {
                                          var initial = State<int, Unit>.Return(Unit.Instance);
                                          var next = Foo(0, initial);
                                          var next2 = Foo(-1, next);
                                      
                                          Console.WriteLine(next2.Run(100));
                                      }
                                      
                                      


                                      1. PsyHaSTe Автор
                                        11.12.2019 00:46
                                        +2

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


                                        static State<int, Unit> Foo(int a, State<int, Unit> state) =>
                                            from __ in state
                                            from stateValue in State.Get<int>()
                                            let increment = a > 0 ? 1 : a == 0 ? 2 : 3
                                            from _ in State.Set(stateValue + increment)
                                            select Unit.Instance;

                                        Я еще начинающий ФПшник, тем более в сишарпе где язык сопротивляется :)


                                        P.S. Когда уже стабилизируют деприкейт _ как имени переменной… Мешает очень.


                                        1. qw1
                                          11.12.2019 00:53
                                          +1

                                          Теперь всё работает )))


                                          1. PsyHaSTe Автор
                                            11.12.2019 00:59

                                            А вот так можно два стейта разных читать и формировать новый:


                                            static State<string, Unit> ConcatStates(State<string, Unit> a, State<string, Unit> b) =>
                                                from __ in a
                                                from aValue in State.Get<string>()
                                                from _ in b
                                                from bValue in State.Get<string>()
                                                from ___ in State.Set<string>(aValue + bValue)
                                                select Unit.Instance;
                                            
                                            static void Main(string[] args)
                                            {
                                                var a = State.Set("Hello ");
                                                var b = State.Set("World!");
                                                Console.WriteLine(ConcatStates(a, b).Run(""));
                                            }

                                            В общем, мощная штука)) И как видите, работает абсолютно так же, как итераторы или async/await.


                                            Правда, сперва непривычно и дико, ведь LINQ по традиции используется только с итераторами. Потом привыкаешь.


                                        1. qw1
                                          11.12.2019 01:02

                                          Но я так понимаю, принципиально нельзя написать ф-цию с сигнатурой

                                          static bool IsZero(State<int, Unit> state)
                                          возвращающую true, если текущее значение state равно 0.

                                          Если бы так было можно, задача покупки кофе «на все деньги» могла быть решена через такой state (покупаем, пока не 0 на карточке).


                                          1. PsyHaSTe Автор
                                            11.12.2019 01:04
                                            +1

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


                                            static State<int, bool> IsZero(State<int, Unit> state)

                                            К стате, в хаскелле такая нечистая функция есть, правда, не для стейта, а для IO. Называется она unsafePerformIO, и как следует из названия, лучше ей не пользоваться. Против неё есть целая прагма {-# LANGUAGE Safe #-} которая запрещает её использование.


                                            Значения не должны покидать контекста монады, в котором созданы. Это так же плохо, как выходить из асинхронного контекста функции через task.Result (который тоже является монадой, и именно поэтому так делать не надо). Чревато дедлоками и всеми остальными плохими вещами.


                                            Поэтому покидать контексты монад — плохо. Для каждой монады последствия такого выхода свои, но всегда — плохие.




                                            Если кто-то будет это читать, вот тут можно посмотреть итоговый вариант кода после всех исправлений:


                                            https://gist.github.com/Pzixel/05d6fc18f389149e64995b148147345e


                                            1. 0xd34df00d
                                              11.12.2019 01:31
                                              +1

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

                                              Теперь я запутался в контексте дискуссии. Что такое State в итоге? Потому что иначе никто не мешает сделать


                                              isZero :: State Int Bool
                                              isZero = do
                                                theInt <- get
                                                pure $ theInt == 0
                                              
                                              isZero' :: State Int a -> Bool
                                              isZero' = (`evalState` isZero)

                                              Поэтому покидать контексты монад — плохо. Для каждой монады последствия такого выхода свои, но всегда — плохие.

                                              Ну это зависит. Собственно, единственная принципиальная разница между IO и STST — escapable-монада, а IO — нет (ну только если не вспоминать упомянутый вами unsafePerformIO и ему подобные).


                                              1. PsyHaSTe Автор
                                                11.12.2019 01:33
                                                +2

                                                Окей, я не прав. Не был в курсе насчет escapable деления. Вот так всегда, объясняя узнаешь что-то новое. Объяснять — полезно. Люди, помогайте друг-другу )


                                        1. mayorovp
                                          11.12.2019 08:53
                                          +1

                                          Что вы тут развели? Всё проще же, самый первый код был правильным, только с лишним параметром:


                                          State<int, Unit> Foo() =>
                                              from a in State.Get<int>()
                                              let valueToSet = a > 0 ? 1 : a == 0 ? 2 : 3
                                              from _ in State.Set(valueToSet)
                                              select Unit.Instance;

                                          //сс qw1


                                          1. PsyHaSTe Автор
                                            11.12.2019 11:39
                                            +1

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


                                            Так что там всё правильно написано.


                                            1. mayorovp
                                              11.12.2019 12:24
                                              +1

                                              Ну тогда так:


                                              State<int, Unit> Foo(int a) =>
                                                  from x in State.Get<int>()
                                                  let valueToAdd = a > 0 ? 1 : a == 0 ? 2 : 3
                                                  from _ in State.Set(x + valueToAdd)
                                                  select Unit.Instance;

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


                                              1. PsyHaSTe Автор
                                                11.12.2019 12:50

                                                Да, вы правы. Тогда использование становится тривиальным:


                                                var next = Foo(0);
                                                var next2 = Foo(-1);
                                                Console.WriteLine(next.Bind(_ => next2).Run(100));

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


                                                1. mayorovp
                                                  11.12.2019 12:55
                                                  +1

                                                  А для двух стейтов надо использовать либо трансформер монад, либо общий стейт и линзы.


                                                  Но в C# ни то, ни другое нормально работать не будет из-за направления вывода типов.


                                                  1. 0xd34df00d
                                                    11.12.2019 19:47

                                                    А для двух стейтов надо использовать либо трансформер монад, либо общий стейт и линзы.

                                                    Могут вылезти проблемы, так как MonadState s m имеет фундеп m -> s. Понятно, что можно писать StateT S1 (StateT S2 m), но это некомпозабельно.


                                                    Поэтому да, линзы (причём, classy lenses) или этот мой Has.


                                                1. qw1
                                                  11.12.2019 13:38

                                                  А если у меня 3 изменения стейта, мне надо писать

                                                  Console.WriteLine(
                                                    next.Bind(_ => next2.Bind(__ => next3)).Run(100));

                                                  А если 100?


                                                  1. PsyHaSTe Автор
                                                    11.12.2019 13:45

                                                    Ну для небольшого количества стейтов можно переписать в linq опять же :)


                                                    var final = from x in next
                                                                from y in next2
                                                                select Unit.Instance;
                                                    
                                                    Console.WriteLine(final.Run(100));

                                                    Ну а для 100 нужно будет их собирать в список и делать траверс или sequence. Примитивный вариант


                                                    public static State<S, A> Sequence<S, A>(this IEnumerable<State<S, A>> states) =>
                                                        states.Aggregate((prev, next) => prev.Bind(_ => next));

                                                    Используем:


                                                    var states = Enumerable.Range(-5, 10).Select(Foo);
                                                    Console.WriteLine(states.Sequence().Run(100)); // 121


                                                    1. qw1
                                                      11.12.2019 14:45

                                                      Тогда я не понимаю, почему это в таком виде называется State. С тем же успехом я могу написать

                                                              static Func<int, int> Foo2(int a) => 
                                                                  a > 0
                                                                      ? x => x + 1
                                                                      : a == 0
                                                                          ? x => x + 2
                                                                          : (Func<int, int>)(x => x + 3);

                                                      А потом вручную создать композицию этих «стейтов», и вызвать полученную композицию с начальным значением. Но это же не будет State в терминах монад? А в чём разница?
                                                      var next = Foo2(0);
                                                      var next2 = Foo2(-1);
                                                      var next3 = Foo2(1);
                                                      Console.WriteLine(next3(next2(next(100))));


                                                      1. mayorovp
                                                        11.12.2019 15:01

                                                        Вы верно уловили суть, State<S, T> — это и есть обёрнутая Func<S, T>.


                                                        Смысл этой монады — во-первых, в том что это простейшая из монад, удобная для изучения (проще нее разве что Maybe/Either — но те вообще ничего интересного не делают). А во-вторых, монада IO<T> — это, в некотором роде, State<RealWorld, T> (если забыть про невыразимость RealWorld) и у нее во многом схожие свойства.


                                                      1. PsyHaSTe Автор
                                                        11.12.2019 16:27
                                                        +1

                                                        С тем же успехом я могу написать

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




                                                        А еще вы поняли, что такое монада (ну, одна из них). Не так уж сложно как говорят, правда?)


                                                        1. qw1
                                                          11.12.2019 17:23

                                                          Совершенно не интуитивно и не похоже на императивное состояние. К тому же, даёт как неоправданную нагрузку на GC, так и на CPU сначала при генерации функторов, затем с их вложенным вычислением.


                                                          1. PsyHaSTe Автор
                                                            11.12.2019 17:47

                                                            Совершенно не интуитивно и не похоже на императивное состояние. К тому же, даёт как неоправданную нагрузку на GC, так и на CPU сначала при генерации функторов, затем с их вложенным вычислением.

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


                                                            А как показывают бенчмарки, тот же хаскель быстрее и Java, и C#. Ну да, помедленнее плюсов, но я и не предлагаю его использовать на тех же задачах. А вот на задачах обычных сервисов и хождений в БД одни плюсы. Разве нет?


                                                          1. 0xd34df00d
                                                            11.12.2019 19:49

                                                            К тому же, даёт как неоправданную нагрузку на GC, так и на CPU сначала при генерации функторов, затем с их вложенным вычислением.

                                                            Там всё нормально разворачивается и инлайнится в подавляющем большинстве интересных случаев (с которыми мне приходилось сталкиваться, по крайней мере).


                                        1. Druu
                                          11.12.2019 16:46

                                          P.S. Когда уже стабилизируют деприкейт _ как имени переменной… Мешает очень.

                                          Делаешь на монаде экстеншн-метод .And() (аналог >>) и пишешь так:
                                          from stateValue in state.And(State.Get())

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


                                          1. PsyHaSTe Автор
                                            11.12.2019 16:50

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


            1. technic93
              09.12.2019 20:26

              По-моему проблема в том что должно быть this.function(). А что в си шарпе this не явный?
              Или где то есть глобальная ссылка на this?


              1. PsyHaSTe Автор
                09.12.2019 21:10
                +1

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


                Что до this, то это ключевое слово в сишарпе необязательно, и this.Field и просто Field равнозначны.


        1. vintage
          09.12.2019 14:54

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

          Любая функция ничего не делает, пока мы её не запустим и не получим результат.


          1. PsyHaSTe Автор
            09.12.2019 14:57

            Ну вот я запустил LazyPrint, на экране ничего не появилось:


            void Main()
            {
                LazyPrint();
                Print();
            }
            
            Task LazyPrint() => new Task(() => Console.WriteLine("I'm lazy print"));
            
            void Print() => Console.WriteLine("I'm strict print");


            1. vintage
              09.12.2019 15:07

              Не будем повторяться: https://habr.com/ru/post/479238/#comment_20980982


        1. mayorovp
          09.12.2019 15:11

          мы просто создаем описатель вычисления (в сишарпе это тип Task), который ничего не делает пока мы его не запустим и не получим результат

          Вот с такими заявлениями надо быть аккуратнее. Task в C#, напомню, ленивым не является (если говорить про тот, который создаётся через async)


          1. PsyHaSTe Автор
            09.12.2019 16:02

            Ну тут я немного слукавил, признаю. Таски являются холодными если создаются через new Task, и не являются таковыми если создаются через асинк/авейт и Task.Run.


            Честно говоря, мне кажется что это ошибка в дизайне языка. В том же F# кстати это было исправлено, там асинки честно ждут пинка от рантайма.


            1. Szer
              09.12.2019 17:08
              +1

              В том же F# кстати это было исправлено, там асинки честно ждут пинка от рантайма.

              Фшарповые Асинки появились на пару лет раньше тасков. Так что это скорее авторы сишарпа не смогли списать домашку и запороли дизайн Тасков


              1. mayorovp
                09.12.2019 19:32

                Да всё проще же. Шарповые таски сделаны в императивной парадигме, потому что их так проще воспринимать.


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


  1. vintage
    09.12.2019 12:00
    -1

    Сам по себе IO ничего не делает, если мы напишем print "Hello world" в хаскелле ничего не произойдет.

    То есть человек ошибся, а компилятор вместо того, чтобы сообщить об ошибке, молча это скушал? Я бы не восхищался таким поведением.


    1. PsyHaSTe Автор
      09.12.2019 12:06

      1. полагаю, на это есть ворнинг. В расте ведь есть MustUse атрибут
      2. Можно не писать 10 сообщений в корне?


      1. vintage
        09.12.2019 12:11

        1. Опять вы что-то там предполагаете. У вас астроголов в роду не было?
        2. Можно не писать. а можно и писать. У вас в сигнатуре статьи не написано, что нельзя писать 10 сообщений в корне.


        1. PsyHaSTe Автор
          09.12.2019 12:35

          1. как и предполагалось, ворнинг на это есть: https://repl.it/@Pzixel/Haskell-playground
          2. очень жаль, что в правилах этого нет. И "не у меня", это правила площадки, и я к ним отношения не имею.


          1. vintage
            09.12.2019 15:06

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


  1. vintage
    09.12.2019 12:02
    +1

    хаскель программа это алгоритм записанный на листочке, а рантайм — это робот, который этот алгоритм выполняет

    Любая программа — это алгоритм, записанный на листочке, а процессор — это робот, который этот алгоритм выполняет.


    1. PsyHaSTe Автор
      09.12.2019 12:11

      Вопрос в том, в какой момент происходит выполнение того, что написано — в тот же, когда выполнение дошло до некоторой строчки или потом при интерпретации.


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


      var query = Enumerable.Range(1, 10).Select(_ => throw new Exception());
      var result = query.ToArray();

      Смысл именно в том, что вы получите эксепшн не в первой строчке, а во второй. Потому что сама запись "throw new Exception" ничего не делает. Это инструкция для дальнейшей интерпетации (через ToArray(), например). Поэтому создание query это чистая функция. Если я напишу:


      var query = Enumerable.Range(1, 10).Select(_ => throw new Exception());

      То никаких исключений, ожидаемо, не произойдет.


      1. vintage
        09.12.2019 12:17
        +1

        Вопрос в том, в какой момент происходит выполнение того, что написано — в тот же, когда выполнение дошло до некоторой строчки или потом при интерпретации.

        В императивных языках это называется кодогенерация.


        Смысл именно в том, что вы получите эксепшн не в первой строчке, а во второй.

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


        1. PsyHaSTe Автор
          09.12.2019 13:20
          +2

          В императивных языках это называется кодогенерация.

          Каким образом кодогенерация к этому относится?


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

          Так работают ленивые вычисления, вне зависимости от языка. И это единственный способ сделать взаимодействие с внешним миром вроде БД или консоли чистым.


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


          1. qw1
            09.12.2019 13:34

            На практике проблем с этим нет, по крайней мере я не знаю сишарп разработчиков
            Когда начинали писать с Entity Framework, коллеги бывало недоумевали: почему мой запрос выдаёт непонятные исключения при выполнении ToList(). Внутри каких-то expressions использовались выражения, который EF не мог превратить в SQL, а stack trace был очень далеко от того места, где ошибочный expression был добавлен в дерево запроса.


            1. PsyHaSTe Автор
              09.12.2019 14:32

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


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


        1. bingo347
          09.12.2019 13:55

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

          С другой стороны, псевдокод на C#, эквивалент которого вполне можно встретить в императивном коде:
          class A
          {
            public void MethodA()
            {
              MethodB();
            }
          
            private void MethodB()
            {
              throw new Exception();
            }
          }
          
          class B
          {
            public void MethodA()
            {
              MethodB(new A());
            }
          
            private void MethodB(A a)
            {
              try {
                a.MethodA();
              }
              catch(Exception e) {
                Logger.Error(e);
                throw e;
              }
            }
          }
          
          Счастливой отладки


          1. qw1
            09.12.2019 14:03

            Счастливой отладки

            Тут хорошей идеей было бы использование InnerException. Вместо throw e:
            throw new MethodBException(e);

            И тогда в catch ничего логировать не надо, т.к. исключение брошено дальше, и должно быть залогированно там, где перехватывается.

            Второй хорошей идеей было бы в методах типа
            Logger.Error(e);
            всегда фиксировать стектрейс, сохранённый внутри e (и в цепочке его InnerExceptions), потому даже в вышеприведённом варианте нет никаких проблем узнать, где первоначально было выкинуто исключение.


            1. bingo347
              09.12.2019 14:26

              Хорошо, то оно хорошо, вот только компилятор меня не обязывает это сделать, мой псевдокод вполне себе компилируется, а в Rust/Haskell мне придется корректно извлечь результат из Result/Either и как-то среагировать на ошибку, иначе моя программа не скомпилируется


              1. qw1
                09.12.2019 14:32

                Насколько я знаю, в Rust можно возвращать Option, и если что-то пошло не так, программист может вернуть None и во всех методах, которые получили None, вернуть этот None выше. И компилятор даже не пикнет. Получив наверху стека вызовов None, сгенерированный где-то в неведомых глубинах, мы ничего не узнаем, откуда этот None пришёл. «Счастливой отладки».


                1. bingo347
                  09.12.2019 18:32
                  +1

                  Если мне пришел None — мне глубоко фиолетово откуда он пришел, ведь это вполне себе тоже результат вычислений, что результата нет. Option — это хорошая абстракция, чтоб не иметь головной боли с NullPointerException и не более.
                  Но я скорее предположу, что Ваша претензия к типу Result, но и тут проблем меньше.
                  Во-первых, в Rust четко разделены паники и ошибки как результат вычисления. Паники — это ошибки программиста, их почти никогда не ловят (хотя такая возможность есть, это крайний случай) и у них четкий стектрейс и дебажить их никаких проблем. Ошибки как результат вычисления — это штатная ситуация (сеть не доступна, файл не открылся), ее не нужно дебажить, ее нужно обрабатывать, ну или пробрасывать дальше, программисту чаще всего до лампочки, какой сискол там использовался под капотом, если его программа не смогла открыть файл из-за того, что юзер выдернул флешку.
                  Во-вторых, что бы пробросить ошибку, тип ошибки должен совпадать, а если моя функция возвращает Result<(), MyError>, а вызываемая Result<(), OtherError>, то чтоб пробросить его мне придется как то преобразовать OtherError к MyError, я могу сделать это на месте с помощью .map_err() или обобщенно, с помощью From/Into типажей, но сделать я это обязан, иначе моя программа просто не скомпилируется.
                  И это все резко отличает Rust/Ocaml/Haskell от языков с эксепшенами, где я мало того, что не вижу из сигнатуры, что там могут быть ошибки, так еще мне в одну кучу сыпят то, что я забыл написать if(x != null)


                  1. vintage
                    09.12.2019 18:38

                    1. 0xd34df00d
                      09.12.2019 18:40
                      -1

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


                      1. vintage
                        09.12.2019 19:00

                        Каких, например?


                        Провалились скорее потому, что никому эта строгость не сдалась.


                        1. 0xd34df00d
                          09.12.2019 19:51
                          +3

                          Каких, например?

                          Как сделать следующее?


                          1. Функция, принимающая функцию с произвольным списком исключений и имеющая такой же список исключений?
                          2. Фукнция, принимающая функцию с произвольным списком исключений при условии, что в этом списке есть тип Foo, вызывающая эту функцию и обрабатывающая Foo, а остальные ошибки пробрасывающая наверх?
                          3. Функция, принимающая две функции с произвольными списками исключений и возвращающая функцию со списком исключений, равным объединению тех списков?

                          Ну так, как пример.


                          1. vintage
                            09.12.2019 19:58
                            -3

                            Это всё можно было бы реализовать через дженерики.


                            1. 0xd34df00d
                              09.12.2019 20:01
                              +6

                              Checked exceptions в джаве постарше, чем дженерики, разве нет?


                              Ну и одних дженериков (то есть, System F) недостаточно, система типов должна быть чуть более выразительной, чтобы можно было выразить понятие объединение списка или наличия элемента в списке на уровне типов.


                              1. vintage
                                09.12.2019 20:10
                                -9

                                Не знаю, да и не важно.


                                Ну а выразительность дженериков вещь системная, она нужна не только и не столько для checked exceptions.


                          1. technic93
                            09.12.2019 20:37
                            +1

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


                            1. Vilaine
                              10.12.2019 05:59

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


                              1. technic93
                                10.12.2019 15:53

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


                          1. qw1
                            09.12.2019 22:12
                            +1

                            Как сделать следующее?
                            Месье знает толк в извращениях, или вот что с людьми делает изучение систем типов :-o


                  1. qw1
                    09.12.2019 19:11

                    Если мне пришел None — мне глубоко фиолетово
                    Ровно до тех пор, пока в этом месте None не должно было прийти и нужно выяснить, почему тут None.
                    Ошибки как результат вычисления — это штатная ситуация (сеть не доступна, файл не открылся), ее не нужно дебажить, ее нужно обрабатывать, ну или пробрасывать дальше, программисту чаще всего до лампочки, какой сискол там использовался под капотом, если его программа не смогла открыть файл из-за того, что юзер выдернул флешку
                    Но программа не должна падать от того, что юзер выдернул флешку?
                    Во-вторых, что бы пробросить ошибку, тип ошибки должен совпадать, а если моя функция возвращает Result<(), MyError>, а вызываемая Result<(), OtherError>, то чтоб пробросить его мне придется как то преобразовать OtherError к MyError, я могу сделать это на месте с помощью .map_err() или обобщенно, с помощью From/Into типажей, но сделать я это обязан, иначе моя программа просто не скомпилируется
                    Может быть, может быть. С checked exceptions это не взлетело, потому что просто лень предусматривать все возможные варианты конверсий ошибок. Если где-то из глубин вылезет FileNotFoundException или HostNameNotResolved, конечно «правильным» действием для «правильного» языка будет закрыть программу и отправить дамп к программисту разбираться.

                    В реальности гораздо гибче показать юзеру FileNotFound (C:\Users\...\config.xml) не отличая его от NullReferenceException и других ошибок, и сказать, что вот это конкретное действие не выполнилось, и часто юзеру будет очевидна причина.


                    1. 0xd34df00d
                      09.12.2019 19:19

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

                      Если None там не должно придти, то это надо выразить в типах.


                      В реальности гораздо гибче показать юзеру FileNotFound (C:\Users...\config.xml) не отличая его от NullReferenceException и других ошибок, и сказать, что вот это конкретное действие не выполнилось, и часто юзеру будет очевидна причина.

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


                      1. qw1
                        09.12.2019 19:30
                        +1

                        Если None там не должно придти, то это надо выразить в типах.
                        Так у нас декомпозиция задач, и программер, который читает из конфига настройку, может просто вернуть None, если настройки в конфиге нет, а программер, пишущий код уровнем выше, может не заморачиваясь вернуть None в ответ на None. Замечу, что мы рассматриваем в этой ветке не абстрактно-идеальный код, где на каждом уровне написано всё верно, а «компилятор не выдал ошибку, а значит и так сойдёт». Кидание конкретного исключения, которое без изменений придёт на самый верх, тут не самая плохая идея.
                        Ну так и тут, вы можете на самом верхнем уровне просто сконвертировать конкретное значение ошибки в строку и показать его пользователю.
                        То есть, нафиг эта строгость и типизация, пусть будет generic exception с текстом в виде строки.


                        1. 0xd34df00d
                          09.12.2019 19:56
                          +5

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

                          Ага, например, throw std::runtime_error { "no data" }.


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


                          То есть, нафиг эта строгость и типизация, пусть будет generic exception с текстом в виде строки.

                          Зачем же? Вы всё ещё выдаёте строгие и типизированные сообщения об ошибках, просто все они реализуют тайпкласс Show (что делается написанием deriving (Show)), поэтому в нужном месте вы их просто показываете как строку, и всё.


                          1. vintage
                            09.12.2019 20:12
                            +2

                            У эксепшена тем не менее будет стектрейс, позволяющий понять откуда он взялся. А у блуждающего по программе None — нет.


                            1. 0xd34df00d
                              09.12.2019 20:14
                              +4

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

                              Особенно в C++, например.


                              А у блуждающего по программе None — нет.

                              Если вам нужно сохранять контекст возникновения ошибки, то используете Either вместо Maybe. Можете туда прям засунуть колстек.


                              1. vintage
                                09.12.2019 20:22
                                -5

                                Давайте не вспоминать протухшие языки, а то они так и не помрут.


                                В том и суть, что "можете", а не "должны", а ещё лучше "оно как-то само".


                                1. 0xd34df00d
                                  09.12.2019 20:23
                                  +1

                                  В том и суть, что "можете", а не "должны", а ещё лучше "оно как-то само".

                                  Как бы вы отнеслись к тому, что у вас каждый null везде за собой стектрейс своей точки возникновения таскает?


                                  1. vintage
                                    09.12.2019 20:29
                                    -3

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


                                    1. 0xd34df00d
                                      09.12.2019 20:34

                                      А, ну так использовать Maybe/None для исключительных ситуаций действительно не стоит.


                                1. KanuTaH
                                  09.12.2019 20:37
                                  +1

                                  Давайте не вспоминать протухшие языки, а то они так и не помрут.

                                  Ну, нужно же на чем-то всякие CLR'ы и JRE писать.


                                  1. vintage
                                    09.12.2019 20:39
                                    -1

                                    Существует полно современных компилируемых языков. D, Rust, Nim.


                                    1. KanuTaH
                                      09.12.2019 20:42
                                      +1

                                      Да, где-то там существуют. Формально.


                                    1. 0xd34df00d
                                      09.12.2019 20:43
                                      +3

                                      D

                                      Я в прошлом комментарии всё думал, как бы поиронизировать на тезисе о протухшести плюсов от любителя D, но вы всё сделали за меня, спасибо.


                                      1. vintage
                                        09.12.2019 20:52
                                        -1

                                        Вы вот зря иронизируете. Или вы протухание определеяете по популярности?


                                        1. KanuTaH
                                          09.12.2019 20:54
                                          +2

                                          А по чему положено в вашем мире определять протухание? «Есть всего два типа языков программирования: те, на которые люди всё время ругаются, и те, которые никто не использует» © известно кто.


                                          1. vintage
                                            09.12.2019 20:57

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


                                            1. 0xd34df00d
                                              09.12.2019 21:07
                                              +2

                                              И что там инновационного в дизайне D?


                                              1. PsyHaSTe Автор
                                                09.12.2019 21:11
                                                +2

                                                В контексте статьи там есть ключевое слово pure. Больше киллер-фичей D к сожалению не знаю.


                                                1. vintage
                                                  09.12.2019 21:36
                                                  -1

                                                  Там ещё есть ключевое слово shared для контроля конкуретного доступа и immutable сами понимаете для чего.


                                              1. vintage
                                                09.12.2019 21:30
                                                -1

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


                                                1. 0xd34df00d
                                                  09.12.2019 22:10
                                                  +1

                                                  адекватная реализация шаблонов

                                                  Было в любом ML с параметрическим полиморфизмом лет 30-40 назад.


                                                  исполнения времени компиляции

                                                  Прям с гарантией завершимости?


                                                  1. vintage
                                                    09.12.2019 22:15
                                                    -1

                                                    При чём тут ML, если мы говорили про C++?


                                                    Сомневаюсь.


                                                    1. 0xd34df00d
                                                      09.12.2019 23:14

                                                      При чём тут ML, если мы говорили про C++?

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


                                1. gecube
                                  09.12.2019 22:58
                                  -2

                                  Извините, не понял Вас. По-Вашему, С++ — протухший? То-то на нем до сих пор Яндекса и Касперский пишут. А ещё выходят новые стандарты...


                                  Хотя согласен… С++ пора закапывать


                                  1. develop7
                                    10.12.2019 01:42

                                    при том количестве миллиардов денег и миллионов человекочасов, захороненных в C++, было бы странно, если бы на нём не продолжали писать и пилить стандарт дальше. да вот только не в коня корм, что наглядно видно на примере хаскеля


                          1. qw1
                            09.12.2019 22:16

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


                            1. 0xd34df00d
                              09.12.2019 22:31
                              +3

                              Так кто мешает из слоя ниже возвращать не Nothing :: Maybe Result, а Left err :: Either Error Result? Ленивые программисты уровнем выше точно так же воспользуются <$> и ничего не заметят.


                          1. svr_91
                            13.12.2019 14:26

                            В одной популярной библиотеке для парсинга конфигов в случае отсутствия значения возвращается exception с телом «SettingNotFoundException» :)
                            github.com/hyperrealm/libconfig/blob/master/lib/libconfigcpp.c%2B%2B#L282


                  1. PsyHaSTe Автор
                    10.12.2019 16:39

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


        1. nexmean
          09.12.2019 14:32
          +1

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


          1. vintage
            09.12.2019 15:22
            +1

            Когда-то для ноды тоже не было отладчика, так люди годами дебажили через console.log. Да и до сих пор многие не умеют пользоваться отладчиком.


            1. nexmean
              09.12.2019 18:13
              +2

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


              1. vintage
                09.12.2019 18:30

                Я слышал у вас там принято в REPL сидеть.


                1. 0xd34df00d
                  09.12.2019 18:32

                  Для быстрого тайпчекинга и проверки гипотез. После того, как я открыл для себя hie и ale/coc, для тайпчекинга переключаться стало не нужно, оно прям в IDE все показывает.


        1. impwx
          09.12.2019 16:11
          +2

          То есть программа остановится не в месте возникновения ошибки, а где-то в случайном месте программы, где решили привести коллекцию к массиву?
          Это не «случайное место» — это место, где из ленивой последовательности требуется результат. И в стектрейсе будет явно указано, что исключение выкинуто на первой строчке из анонимной функции, но произойдет это после выполнения второй и функция `ToArray` также будет в стектрейсе где-то ниже. Все логично и ожидаемо.


          1. vintage
            09.12.2019 16:21
            -2

            Вы это пользователю объясните, когда он в профиле ввёл что-то не правильно, а потом у него главная перестала открываться, так как именно там потребовался результат.


            1. PsyHaSTe Автор
              09.12.2019 16:26

              Для этого есть валидация формы сохранения.


              Или у вас в императивной версии всё автоматически само заработает?


              1. vintage
                09.12.2019 16:57
                -1

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


                1. impwx
                  09.12.2019 17:16
                  +1

                  Вы либо не понимаете суть ленивых вычислений, либо специально используете неуместную аналогию.


  1. PsyHaSTe Автор
    09.12.2019 12:05

    del


  1. s1im
    09.12.2019 12:11

    А для чего было копипастить код в ООП примере с кофе? Ведь для заказа одной чашки достаточно вызвать метод «купить N чашек кофе» с аргументом N=1 — BuyCoffees(1, ...)


    1. PsyHaSTe Автор
      09.12.2019 12:16

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


  1. vintage
    09.12.2019 12:26
    -1

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

    Если функция исполнилась — хотели бы. Если функция не исполнилась — не хотели бы. В данном случае логируется сам факт исполнения функции.


  1. nicholas_k
    09.12.2019 12:26
    +1

    Хотя люди обычно признают удобства ФП фич, ведь намного приятнее писать:

    int Factorial(int n)
    {
        Log.Info($"Computing factorial of {n}");
        return Enumerable.Range(1, n).Aggregate((x, y) => x * y);
    }


    чем ужасные императивные программы вроде

    int Factorial(int n)
    {
        int result = 1;
        for (int i = 2; i <= n; i++)
        {
            result *= i;
        }
        return result;
    }



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


    1. PsyHaSTe Автор
      09.12.2019 12:28
      +2

      Ну тут еще и синтаксис сишарпа не очень. Возьмем пример с ренжами из C# 8.0:


      int Factorial(int n) => (1..n).Fold((x, y) => x * y);

      Неужели это читается менее понятно чем вариант с циклами?
      Тут буквально написано "возьми числа от 1 до n и перемножь их".


      1. Andrey_Dolg
        09.12.2019 12:56

        Вариант 1
        int Factorial(int n)
        {
            return Enumerable.Range(1, n).Aggregate((x, y) => x * y);
        }
        


        1. nexmean
          09.12.2019 13:00

          factorial n = product [1 .. n]


          1. qw1
            09.12.2019 13:13

            Тут победит язык, у которого factorial есть в стандартной библиотеке.
            У него описание функции займёт 0 строк.


            1. nexmean
              09.12.2019 13:43

              factorial n = foldl1' (*) [1 .. n]


              1. qw1
                09.12.2019 14:05

                Все равно, это длиннее, чем

                 


                1. youlose
                  09.12.2019 20:33

                  на подключение и вызов подобной функции тоже надо код писать


                  1. PsyHaSTe Автор
                    09.12.2019 21:13
                    +1

                    Можно пойти по пути языка HQ9+, тогда ничего подключать не придется.


                  1. qw1
                    09.12.2019 22:18

                    — вы за меня ещё и функции вызывать будете?
                    — ага!


      1. S-e-n
        09.12.2019 13:24
        +1

        Имхо, такое сравнение некорректно в принципе. Императивный пример — вся реализации целиком. Функциональный дёргает Range и Aggregate/Fold, которые сами по себе абстракции над циклами/рекурсией, по крайней мере на концептуальном уровне, реализацию в C# я не знаю.


        1. PsyHaSTe Автор
          09.12.2019 13:29
          +3

          В этом весь смысл. Когда вы видите var x = foos.Fold(...) вы можете дальше не смотреть, вы знаете, что в результате получите одно значение определенного типа.


          А когда вы видите for (...) то вы не знаете ничего. Надо проверить начальное значение, условие выхода, что происходит с аккумулятором (там часто умножение, битовые сдвиги или еще что-нибудь такое происходит). Потом еще тело посмотреть, что цикл делает. Может он значение возвращает, а может что-то нехорошее делает.


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


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


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


          1. S-e-n
            09.12.2019 14:58

            А когда вы видите for (...) то вы не знаете ничего. Надо проверить начальное значение, условие выхода, что происходит с аккумулятором (там часто умножение, битовые сдвиги или еще что-нибудь такое происходит). Потом еще тело посмотреть, что цикл делает. Может он значение возвращает, а может что-то нехорошее делает.
            Вот это всё я наоборот считаю плюсом. В смысле что оно всё сразу в одном месте. У меня всё это происходит на автомате для императивщины.


            1. PsyHaSTe Автор
              09.12.2019 15:03

              Могу ответить такой аналогией: как вы отнесетесь к человеку, который все циклы всегда пишет for(;;)? Вы вот пользуетесь for/while/do_while/..., а он всегда пишет только for(;;)? Как по мне, это не очень хорошо.


              ну или например, как вы думаете, что понятнее:


              int[] b = new int[a.Length];
              for (int i = 0; i < a.Length; i++) {
                 int value = a[i];
                 b[i] = a[i] * a[i];
              }
              return b;

              или


              return a.Map(x => x*x).ToArray();

              Если вам ближе вариант №1, то я просто рекомендую получить опыт написания во втором стиле. Это вопрос привычки


              1. vintage
                09.12.2019 15:33

                Перепешите на свёртках списков код чуть сложнее, чем "привет мир", без потери наглядности:


                bool isEqual( int[] left , int[] right ) {
                
                    if( left.length != right.length ) return false;
                
                    foreach( int i ; 0 .. left.length ) {
                        if( left[i] != right[i] ) return false;
                    }
                
                    return true;
                
                }


                1. PsyHaSTe Автор
                  09.12.2019 16:06
                  -1

                  А в чем тут должна возникнуть проблема?


                  bool isEqual( int[] left , int[] right ) {
                  
                      if( left.length != right.length ) return false;
                  
                      return left.Zip(right, (left, right) => new {left, right})
                                 .Foldr(true, (acc, item) => acc && item.left == item.right);
                  }

                  Наглядность как по мне только выросла.


                  1. math_coder
                    09.12.2019 16:28

                    Это не эквивалентный код. Нужно заменить Foldr на что-то вроде FoldrUntil.


                    1. PsyHaSTe Автор
                      09.12.2019 16:31

                      В ленивом языке (например, в хаскелле) это будет эквивалентный код.
                      В неленивом придется извращаться как тут: https://docs.rs/itertools/0.6.0/itertools/trait.Itertools.html#method.fold_while


                      С другой стороны, мне пришло тут в голову, что вот так можно написать с сохранением раннего выхода и читаемости


                      bool isEqual( int[] left , int[] right ) =>
                          left.length == right.length && 
                          left.Zip(right, (left, right) => new {left, right})
                              .All(item => item.left == item.right);

                      Ну или с таплами


                      bool isEqual( int[] left , int[] right ) =>
                          left.length == right.length && 
                          left.Zip(right)
                              .All(x => x.Item1 == x.Item2);

                      Не уверен что так лучше. Но на мой взгляд почище и проще, чем вариант на циклах.


                      Еще один плюс, знаете как будет выглядеть функция которая считает это всё дело параллельно? А вот так:


                      bool isEqual(int[] left, int[] right) =>
                          left.Length == right.Length &&
                          left.Zip(right)
                              .AsParallel()
                              .All(x => x.Item1 == x.Item2);

                      Достаточно дописать одну строчку.
                      С циклами так не работает, к сожалению.


                      1. vintage
                        09.12.2019 16:40

                        В ленивом языке (например, в хаскелле) это будет эквивалентный код.

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


                        1. PsyHaSTe Автор
                          09.12.2019 16:43

                          Да, уверен. Например, делаем правую свертку на бесконечном списке (то есть идем справа от бесконечности), но компилятор хаскеля как-то догадывается посчитать правильный ответ:


                          foldr (\x xs -> if x > 10 then [] else x:xs) [] [1..]
                          [1,2,3,4,5,6,7,8,9,10]


                          Да, на главной странице https://www.haskell.org/ встроен рабочий интерпретатор


                          1. vintage
                            09.12.2019 17:04

                            У вас тут есть явно терминирующее значение []. С логическими значениями всё не так очевидно.


                          1. aikixd
                            09.12.2019 17:07
                            +1

                            Как? 0_о


                            1. PsyHaSTe Автор
                              09.12.2019 17:42
                              +1

                              Если развернуть последовательность foldr то вы получите последовательность:


                              foldr (:) [] [1..]
                              1 : foldr (:) [] [2..]
                              1 : 2 : foldr (:) [] [3..]
                              ...

                              Когда компилятор дойдет до 10 он увидет, что правое значение никак не используется (условие if x > 10 в лямбде), значит дальше вычислять не надо, и успешно завершит функцию.


                              1. mikeus
                                11.12.2019 15:47

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

                                foldr (\ x y -> if (mod x 10 == 0) then [-1] else x:y) [] [1,2..]

                                На первой встреченной десятке в выражении для foldr:
                                foldr f r0 A = A1 `f` (A2 `f` (... An-2 `f` (An-1 `f` (An `f` r0)) ...))

                                правый операнд f просто заменяется на значение [-1], и на этом вычисление оказываются полностью построенным как задано в описании. Остается только редуцировать его до результирующего значения (с помощью такой же процедуры последовательных простых подстановок).


                                1. PsyHaSTe Автор
                                  11.12.2019 16:04

                                  Мне кажется, это объяснение подходит для того что уже это знает, но они и так знают :)


                                  А кто не знает — ничего не понял.


                                1. 0xd34df00d
                                  11.12.2019 19:53

                                  Только не суть лямбда-исчисления, а суть ленивой стратегии вычислений (или call-by-need, или как она там). Для хаскеля Черч-Россер не выполняется.


                                  1. mikeus
                                    12.12.2019 00:21

                                    Да ну, какая здесь ленивость?

                                    res = foldr (\ x y -> if (mod x 10 == 0) then [-1] else x:y) [] [1,2..]
                                    выдаёт res = [1,2,3,4,5,6,7,8,9,-1]. А 11-й элемент компилятор «поленился» выдать? Так можно ему помочь:
                                    take 11 res
                                    Только вот его там нет.


                                    1. 0xd34df00d
                                      12.12.2019 00:32
                                      +1

                                      Вот прям тут сразу, где вы не пытаетесь вычислить [1..] перед вычислением foldr. Если ваша стратегия вычислений вдруг начнёт вычислять [1..] перед foldr, то это может занять некоторое время.


                                      1. mikeus
                                        12.12.2019 01:19

                                        Ну можно взять аргумент [1..1000000] (и предварительно вычислить, если надо). Изначальный вопрос был не про обработку бесконечных списков.


                          1. math_coder
                            09.12.2019 17:07

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


                          1. perevedko
                            10.12.2019 00:49

                            встроен-то он встроен, но на самом деле он выполняется где-то на сервере


                            1. gecube
                              10.12.2019 00:50
                              +1

                              И как это влияет на возможность проверять в нем сниппеты кода?


                        1. nexmean
                          09.12.2019 17:11

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


                      1. qw1
                        09.12.2019 17:04

                        В ленивом языке (например, в хаскелле) это будет эквивалентный код.
                        Это как? Haskell знает, что если один из множителей равен 0, то можно останавливать свёртку, т.к. дальше умножать бессмысленно — произведение всё равно будет 0? Если да, то это в некоторых случаях очень плохо — лишнее сравнение внутри цикла может убить производительность.


                        1. PsyHaSTe Автор
                          09.12.2019 17:44

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


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


                          1. qw1
                            09.12.2019 19:17
                            -1

                            Это не вопрос дефолтов, это вопрос семантики.
                            В вашем примере с
                            .Foldr(true, (acc, item) => acc && item.left == item.right);
                            имеет ли право компилятор залезть в переданную в Foldr лямбду и делать какие-либо предположения/оптимизации на основе её содержимого.


                            1. 0xd34df00d
                              09.12.2019 19:21
                              +1

                              А при какой семантике он не имеет права залезать в лямбду, но имеет право оптимизировать &&?


                              1. qw1
                                09.12.2019 19:34

                                Пример со свёрткой произведений. Нужно ли останавливаться на нулевом множителе.


                                1. mayorovp
                                  09.12.2019 19:56

                                  Нет, не нужно (но можно).


                                  1. qw1
                                    09.12.2019 22:19
                                    -1

                                    Как раз в этом случае можно здорово обломаться по производительности (см. пример выше).


                                    1. mayorovp
                                      10.12.2019 06:13

                                      Каким образом? Умный компилятор не будет ставить лишнее сравнение.


                                      1. qw1
                                        10.12.2019 11:07

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


                                        1. mayorovp
                                          10.12.2019 11:14

                                          Доказать что там 0 на этапе компиляции.


                                          1. qw1
                                            10.12.2019 12:04

                                            Вернёмся к исходному примеру.

                                            .Foldr(true, (acc, item) => acc && item.left == item.right);

                                            В общем случае, компилятор не может доказать, что там false. Но должен ли он останавливать свёртку, если на одном из элементов получили (item.left != item.right).


                                            1. mayorovp
                                              10.12.2019 12:17

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


                              1. Druu
                                10.12.2019 11:18

                                Тут как раз вопрос в том, почему он оптимизирует && (меняет местами операнды). По сути же на "acc && item.left == item.right" он должен виснуть, а на "item.left == item.right && acc" — нет, т.к. foldr f init xs встает как раз на место acc.


                            1. PsyHaSTe Автор
                              09.12.2019 19:21
                              +1

                              Вопрос в том, что означает a == b. В каком-нибудь хаскеле это инструкция создать Trunk с таким действием, но рантайм не будет его выполнять если он видит что результат не используется. А в си просто посчитает и вернет результат.


                  1. vintage
                    09.12.2019 16:33

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


                1. nexmean
                  09.12.2019 16:34
                  +1

                  В общем-то это не задача для решения через комбинаторы, но вот:


                  isEqual left right =
                    length left == length right
                    && (all id $ zipWith (==) left right)

                  для списков это решение эвалюирует весь список, чтобы посчитать длины.


                  Простой рекурсией это решается лучше:


                  isEqual (x:xs) (y:ys) = x == y && isEqual xs ys
                  isEqual []     []     = True
                  isEqual _      _      = False

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


                  isEqual a b = V.length a == V.length b
                    && all id
                    $  zipWith (==) (V.toList a) (V.toList b)


                1. YuryZakharov
                  09.12.2019 17:10

                  Как-то так:

                  private static bool AreEqual(int[] left, int[] right) =>
                              left.Length == right.Length &&  left.Zip(right).All(p=>p.First == p.Second);


                  1. nexmean
                    09.12.2019 17:12

                    Забыли сравнить длины.


                  1. vintage
                    09.12.2019 17:18

                    Что у вас будет в случае массивов разной длины?


                    1. YuryZakharov
                      10.12.2019 12:41

                      Уже пороавил, спасибо.
                      Подумал, можно ещё и так сделать, хотя и не на свёртке:

                              private static bool AreEqualToo(int[] left, int[] right) 
                                  => !left.Except(right).Any() && !right.Except(left).Any();
                      
                      

                      Надеюсь, вопросы производительности мы вынесли зв скобки в этом обсуждении.


                      1. mayorovp
                        10.12.2019 12:46

                        Но это ж разные проверки! Первая сравнивала два списка, а тут вы сравниваете два множества.


                      1. vintage
                        10.12.2019 13:04
                        -3

                        Зря надеетесь. Ни производительность не вынесли, ни потребление памяти.


                      1. PsyHaSTe Автор
                        10.12.2019 13:46

                        Вы тут создаете аж две хэшмапы просто чтобы проверить что там есть элементы. Это очень нехорошее решение по многим параметрам. Да и читается имхо хуже чем просто проверка Any(x => x.left == x.right)


              1. S-e-n
                09.12.2019 15:49

                int value = a[i];
                Это тут зачем?

                Мне ближе вариант №1 из-за того, что у меня циклы настолько тривиальными остаются крайне редко. И то, что было for (i = 0; i < end; i++) частенько превращается во что-нибудь вроде for (i = start; i < end; i+=step) в процессе разработки.

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


                1. PsyHaSTe Автор
                  09.12.2019 16:07

                  Это тут зачем?

                  Чтобы не вычислять два раза индекс a[i]. Можно конечно понадеяться что компилятор соптимизирует, но может и нет.


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

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


                  1. S-e-n
                    09.12.2019 16:19

                    Может тогда b[i] = a[i] * a[i]; должно быть b[i] = value * value;? Или там какая-то внутренняя магия компилятора со ссылками?

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


                    1. PsyHaSTe Автор
                      09.12.2019 16:21

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


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

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


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


                      1. S-e-n
                        09.12.2019 17:10

                        Ладно, я в общем-то не холиварить пришёл. Просто идея чистых функций мне нравится, а остальные атрибуты ФП — не особенно. И ООП — тоже не особенно. Поэтому и пытаюсь для себя понять — может быть процедурное с элементами ФП и есть оптимальный вариант?


                        1. PsyHaSTe Автор
                          09.12.2019 17:49
                          +1

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


                          А что лично вы вкладываете в этот термин? Остальные атрибуты ФП — это какие? Как я говорил, писать всё на комбинаторах необязательно, просто это удобно. Да, после циклов выглядит немного чужеродно, но тут вопрос привычки. Пописать так год, потом на циклы смотреть не захочется, уверяю вас :)


                          Хотя бывают сценарии, когда они лучше, безусловно.


                          1. poxvuibr
                            09.12.2019 19:10

                            Инкапсуляция это общий принцип программирования, не знаю почему именно с ООП его ассоциируют, без него никуда.

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


                          1. S-e-n
                            10.12.2019 15:42
                            -2

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

                            А что лично вы вкладываете в этот термин? Остальные атрибуты ФП — это какие?
                            Иммутабельность, комбинаторы, замыкания, рекурсия вместо циклов, каррирование, из того, о чём обычно вещают.
                            Да, после циклов выглядит немного чужеродно, но тут вопрос привычки. Пописать так год, потом на циклы смотреть не захочется, уверяю вас :)
                            Немного порефлексировал, и понял, что мне так не нравится в этой затее. А то, что комбинаторы — частное решение, в то время, как циклы — общее. Без _значительного_ профита частное решение я просто не буду использовать, когда есть более общее. Насчёт удобства — кому как.


                            1. PsyHaSTe Автор
                              10.12.2019 16:27
                              +2

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

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


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

                              Так наоборот же. Например если вы хотите использовать цикл со счетчиком, вы возьмете "частный" for, хотя он является частным случаем while. Так же можно логику продолжить дальше. Чем более "конкретный" метод используется, тем меньше нужно смотреть на тело цикла чтобы понять какой результат получится. Ну и писанины меньше, часть логики спрятана в методе. Этакий юникс вей где лучше иметь небольшие функции, делающие своё дело, чем один гигантский MakeEverythingGood, который и жнец, и на дуде игрец (for).


                              1. S-e-n
                                10.12.2019 17:27
                                +1

                                А без общего понятийного аппарата договориться трудно.
                                Ну как в ООП завезут общий понятийный аппарат, тогда можно о нём будет говорить. Извините за резкость, но надоело, что технология, которой много лет, до сих пор требует сверки понятий перед тем, как о ней поговорить. Приклеивание процедур к struct-ам — самое однозначно трактуемое определение ООП из известных мне.
                                Этакий юникс вей где лучше иметь небольшие функции, делающие своё дело, чем один гигантский MakeEverythingGood, который и жнец, и на дуде игрец (for)
                                Мне больше нравится подход: «немного базовых элементов и комбинаторный взрыв».


                                1. PsyHaSTe Автор
                                  10.12.2019 18:10
                                  +1

                                  Приклеивание процедур к struct-ам — самое однозначно трактуемое определение ООП из известных мне.

                                  В расте есть приклеивание методов к структам, а наследования нет. Он ООП?


                                  Мне больше нравится подход: «немного базовых элементов и комбинаторный взрыв».

                                  Ну вот комбинаторы это как раз про это. Их ведь немного: Fold(самый базовый)/map/filter/any/all/zip/unzip/groupby/orderby. По сути всё. Запомнить их очень нетрудно, как мне кажется. Да и что они делают понятно из названия.


                                  1. S-e-n
                                    11.12.2019 13:10

                                    В расте есть приклеивание методов к структам, а наследования нет. Он ООП?
                                    Значит на расте ООП возможно.

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


                                    1. PsyHaSTe Автор
                                      11.12.2019 13:50
                                      +1

                                      Да нет, зачем мне реализация? Хотя не вижу ничего сложного в том, чтобы реализовать на любом языке. Ну вот All например


                                      bool All<T>(this IEnumerable<T> source, Predicate<T> predicate) {
                                          foreach (var v in source) {
                                              if (!predicate(v)) return false;
                                          }    
                                          return true;
                                      }

                                      Не вижу проблем написать это на любом языке.




                                      При этом и посылка неверная. Я сходу не напишу GroupBy эффективно на любом языке. Пользоваться им это мне не мешает.


                                      1. S-e-n
                                        11.12.2019 14:06

                                        Да нет, зачем мне реализация?
                                        Затем, чтобы понимать, что конкретно происходит.

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


                                        1. mayorovp
                                          11.12.2019 14:09

                                          Да, для стандартных комбинаторов и вменяемых языков я в этом уверен.


                                        1. PsyHaSTe Автор
                                          11.12.2019 14:17

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

                                          Развивайте уверенность, что тут скажешь :)


                                          1. S-e-n
                                            11.12.2019 14:46

                                            Спасибо, но предпочитаю знать и понимать.


                                            1. PsyHaSTe Автор
                                              11.12.2019 16:58
                                              +1

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


                                              Лично я реализации из стандартной библиотеке доверю больше, чем "инхаус решениям". А вы?


                                              1. S-e-n
                                                11.12.2019 18:15
                                                -1

                                                Я не про страшное, а про поведение на edge case-ах, особенности реализации в конкретном языке и подводные камни в виде взаимодействия с другими фичами языка. И я уже не только про комбинаторы, но вообще про мелкие «удобные» абстракции.

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

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

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


                                                1. PsyHaSTe Автор
                                                  11.12.2019 18:43

                                                  И я уже не только про комбинаторы, но вообще про мелкие «удобные» абстракции.

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


                                                  1. S-e-n
                                                    11.12.2019 18:55

                                                    То есть мелкие удобные абстракции — это плохо?
                                                    Я в деталях объяснил, что я считаю.

                                                    думаю можно дальше тему не развивать
                                                    Согласен.


                              1. qw1
                                10.12.2019 21:36

                                Так наоборот же
                                Это зависит от модели вычислений. Если у нас машина Тьюринга, то изменения состояний (они же branch, или уровнем чуть выше — if/for/while) — первичны. Если брать за основу ?-исчисление, то комбинаторы первичны. Просто не надо думать о программах, как о ?-выражениях ))) Думайте о них, как об инструкциях для традиционного процессора.


                                1. 0xd34df00d
                                  10.12.2019 22:12

                                  Думать о программах как о термах лямбда-исчисления куда полезнее. Не зря [типизированное] лямбда-исчисление — один из языков матлога (вольно выражаясь), а с формализмом машины Тьюринга таких успехов почему-то нет.


                                  1. qw1
                                    10.12.2019 22:51

                                    Я считаю, наоборот. Все значимые результаты теории вычислимости доказываются на МТ (проблема останова, существование универсального вычислителя и всё такое). Не заметно, чтобы на лямбдах что-то такое пытались. Хотя подходы эквивалентны. Значит, МТ удобнее в смысле исследования вычислений.


                                    1. 0xd34df00d
                                      11.12.2019 01:24
                                      +1

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


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


                                1. PsyHaSTe Автор
                                  10.12.2019 23:10
                                  +3

                                  Думайте о них, как об инструкциях для традиционного процессора.

                                  Самое время вставить тут ссылку на статью "Си это не низкоуровневый язык" :)


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


                                  Не заметно, чтобы на лямбдах что-то такое пытались. Хотя подходы эквивалентны. Значит, МТ удобнее в смысле исследования вычислений.

                                  Одновременно с этим большая часть передовых подходов в software engineering приходит со стороны лямбда-счисления.


                                  Я бы не стал опираться на то, что с МТ некоторые теоремы доказывать проще.


                                  Серьёзно. Any/All/Filter это всё высказывания на уровне теории множеств. "Все","Любой","Такие, что" и так далее. Все люди, с которыми я общался, когда им непонятно начинали на бумажке рисовать объекты, стрелочками обозначать связи, обозначать кто кому кем является, и т.п. И ни разу не видел людей которые начинают писать "шаг один — записать такое-то значение в такую-то ячейку".


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


                                  1. qw1
                                    11.12.2019 00:15
                                    -1

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


                                    1. PsyHaSTe Автор
                                      11.12.2019 00:24
                                      +2

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

                                      Позвольте не поверить. Мне лично неудобно думать. И я не знаю людей, которым правда это удобно. Есть люди, которые говорили что им удобно, но попытка удержать хотя бы 10 мутабельных переменных в цикле сразу расставляла всё по своим местам.


                                      Только объяснение любого нетривиального алгоритма, вроде RB-дерева, или quicksort/mergesort элементарно в нотации ячеек памяти и превращается в аццкий ад, когда функциональщики пытаются эти же алгоритмы на своём эльфийском писать )))

                                      Мержсорт отлично на ФП переносится. Квиксорт, впрочем, тоже.


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


                                    1. 0xd34df00d
                                      11.12.2019 01:33
                                      +2

                                      вроде RB-дерева

                                      Спокойно делается в ФП-мире (см. книжку Окасаки).


                                      quicksort/mergesort элементарно в нотации ячеек памяти и превращается в аццкий ад, когда функциональщики пытаются эти же алгоритмы на своём эльфийском писать )))

                                      Ъ инплейс-квиксорт действительно не очень приятно писать, а merge sort же как раз на это всё отлично ложится, разве нет?


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


                                    1. mayorovp
                                      11.12.2019 09:02
                                      +1

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


                                      И персистентное RB-дерево тоже в функциональном варианте выглядит довольно просто.


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


              1. KvanTTT
                10.12.2019 01:54

                Так может во втором варианте еще и Pow(x, 2) использовать — так еще лучше задачу иллюстрирует :)


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


                1. PsyHaSTe Автор
                  10.12.2019 02:04

                  Так может во втором варианте еще и Pow(x, 2) использовать — так еще лучше задачу иллюстрирует :)

                  Стараюсь не использовать эту функцию, потому что она работает с double-аргументами. А выходить за пределы int-арифметики очень не люблю, чтобы потом не получить в одном килобайте 1023.937528 байт.


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

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


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


              1. Sagrer
                11.12.2019 16:05

                С точки зрения человека, не знающего толком C#в первом примере вполне понятно что берём массив a и из него делаем массив квадратов b. Смущает только строчка int value = a[i];

                Для второго варианта можно, например, предположить, что для массива a вызывается метод, который превращает этот массив в map (вероятно, что-то на основе деревьев а не хеш-таблиц) таким образом, что для каждого элемента исходного массива вызывается лямбда, в которую этот элемент передаётся аргументом, а результат записывается в создающуюся map. Затем получившийся map опять преобразуется в массив. Вполне возможно что в чём-то я ошибаюсь, но более-менее знаю только плюсы. Я к тому, что первый вариант очевиден в общем-то кому угодно, для второго — для понимания надо знать особенности конкретного языка и его стандартной библиотеки.


                1. PsyHaSTe Автор
                  11.12.2019 16:10

                  Ну map это функция которая занимается отображением, к хэшмапе отношения никакого не имеет. В C++ практически такая же функция называется transform. Вот так через неё выражается map:


                  template<typename Src, typename Dst, template<class, typename ...> typename Container>
                  Container<Dst> map(Container<Src>& container, Dst(*f)(Src)) {
                      Container<Dst> result;
                      result.reserve(container.size());
                      std::transform(container.begin(), container.end(), std::back_inserter(result), f);
                      return result;
                  }

                  Используется вот так:


                  vector<int> a = {1, 2, 3};
                  auto f = [](int x) -> int { return x*x; };
                  vector<double> b = fmap(a, +f);

                  Работает так же, как и код C# выше, за исключением того, что он требует аллокации вектора куда складывать результаты, а в C# у вас ленивый итератор.


                  Разница между map как функцией и Map как структурой данных такая же, как между программным стеком и структурой данных Stack. Можно путаться, но это скорее новичковая ошибка, и один раз узнав разницу уже не перепутаете.


                  Смущает только строчка int value = a[i];

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


                1. mayorovp
                  11.12.2019 16:32
                  +1

                  Метод, который превращал бы массив в Map, назывался бы ToMap. А одиночное слово "Map" явно имеет какой-то другой смысл...


                  1. Aldrog
                    11.12.2019 22:45
                    +2

                    Более того, по синтаксису очевидно, что это функция, а значит слово map скорее всего используется как глагол.


      1. Szer
        09.12.2019 14:01

        (1..n)

        Хе-хе. Границы ренжей в сишарпе 8 имеют разную инклюзивность.


      1. pin2t
        09.12.2019 14:05
        -1

        Неужели это читается менее понятно чем вариант с циклами?

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

        Куча вопросов


        1. qw1
          09.12.2019 14:09
          +2

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


        1. nexmean
          09.12.2019 14:09
          +4

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


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


  1. aikixd
    09.12.2019 13:58

    Давайте проверим, выполняется ли наше правило для первой функции? Оказывается, что нет, потому что если мы заменим где-нибудь Factorial(5) на 120 то у нас поменяется поведение программы — в логи перестанет писаться информация которая раньше записывалась (хотя если мы подойдем с позиции "да и хрен ними, с логами" и не будем считать это желаемым поведением, то программу можно будет считать чистой.

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


    Концептуально, функции не исполняются, а только превращают аргументы в результат:


    public static int Add1(int x)
    {
        switch (x)
        {
            case 0: return 1;
            case 1: return 2;
            ...
        }
    }
    
    Никакого поведения тут нет.


  1. user_man
    09.12.2019 14:05
    -1

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

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

    Программирование сложнее примитивного деления на «мне нравится ФП» и «мне не нравится ФП». Сторонники ФП по необразованности, к сожалению, часто ставят себя выше остальных, и всё лишь потому, что они обнаруживают в ФП лишь некоторые возможности, которые давно есть в других языках. Ну и в результате выставляют себя глупцами, да. А как ещё назвать людей с самомнением, да к тому же не желающих изучать альтернативы?

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

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


    1. nexmean
      09.12.2019 14:29
      +3

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

      Чушь полная. Большинство фич в мейнстримовые языки приходят из ФП. Начиная от генериков, которые хрен знает когда появились в жаве, заканчивая ADT, которые кое-как добираются до мейнстрима, встречают дикий восторг императивных программистов, хотя в ФЯ они появились ещё в 80-е.


      А как ещё назвать людей с самомнением, да к тому же не желающих изучать альтернативы?

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


      Ну и до кучи — возможность изменять внешнее состояние полезна. И если язык позволяет эту возможность использовать — это хороший язык. А если программист при использовании этой возможности не умеет избегать ошибок — это плохой программист. Сторонники ФП нашли выход из такой ситуации — запретили менять внешнее состояние.

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


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

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


      1. gatoazul
        09.12.2019 23:46

        ADT появились в императивном языке CLU (1974 г). Язык разрабатывался под руководством Барбары Лисков, той самой, что LSP.


        1. 0xd34df00d
          10.12.2019 00:27
          +2

          Боюсь, что тамошние ADT (которые abstract) имеют малое отношение к тутошним ADT (которые algebraic).


      1. druss
        10.12.2019 01:37
        +1

        все, кто умеет в ФП — сторонники ФП, все кто не умеет — его противники

        Главное не начать говорить, что все, кто противники — просто не умеют в ФП


    1. PsyHaSTe Автор
      09.12.2019 14:50

      А вся эта идеологическая шелуха про «ФП позволяет писать надёжно» и тому подобное, есть следствие глубокого непонимания сторонниками ФП всего остального мира, связанного с созданием программ.

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


      Если вы не согласны, что написание независимых модулей, состояние которых не влияет друг на друга, то можете подробнее рассказать, как это получается? У меня сходу не получается такого представить. Меньше взаимосвязей — больше надежность. Low coupling high cohesion как они есть.


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

      Называйте как хотите. ФП так называется не для того, чтобы присвоить чьи-то заслуги (как я уже сказал), а чтобы был общий понятийный аппарат. Вот вы сказали "абстрактная фабрика", и все сразу всё поняли. Сказали "ФП", и все поняли, что речь о программе написанной в чистых функциях.


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

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


      А если программист при использовании этой возможности не умеет избегать ошибок — это плохой программист.

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


      Когда люди совершают ошибки с инструментом, хороший путь — починить инструмент. Плохой — ругать людей, и говорить, что они плохие и неправильные.


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

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


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

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


      идеологическая шелуха

      следствие глубокого непонимания сторонниками ФП всего остального мира

      человек, крайне поверхностно знакомый с альтернативами (либо вообще о них не знающий)

      Сторонники ФП по необразованности

      выставляют себя глупцами

      не желающих изучать альтернативы

      признак очень ограниченного интеллекта

      автор просто забыл про все альтернативы

      Что-то мне подсказывает что вы несколько предвзяты.


      1. user_man
        09.12.2019 20:44
        -1

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

        У меня к вам просьба — прочитайте процитированный выше ваш текст и найдите в нём смысл. Я не нашёл.
        ФП так называется не для того, чтобы присвоить чьи-то заслуги (как я уже сказал), а чтобы был общий понятийный аппарат.

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

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

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

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

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

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

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

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

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


        1. vintage
          09.12.2019 20:54

          А в каком разделе математики есть изменяемое состояние?


          1. qw1
            09.12.2019 22:23

            У меня deja vu. Когда-то я задавал точно такой же вопрос, и ув. 0xd34df00d нашёл мне раздел математики, изучающий изменяемые состояния )))


            1. 0xd34df00d
              09.12.2019 22:33

              Только я этого не помню :(


              Но вообще можно просто правильно параметризовать и получить подобие состояний. Или можно в приложении к CS/системам типов почитать того же Пирса, в TAPL в районе 13-й главы это всё описывается.


          1. user_man
            10.12.2019 15:24

            В любом разделе, где исследуется разница между двумя состояниями (значениями, множествами, любыми другими носителями информации).


            1. vintage
              10.12.2019 15:37
              -1

              Значения математике не являются изменяемыми.


        1. 0xd34df00d
          09.12.2019 23:38
          +3

          а в функциональных языках с их ленивостью

          Ленивость не является неотъемлемым атрибутом функциональных языков.


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

          Именно поэтому доказуемая завершимость функций сформулирована и реализована на практике для ФП с его рекурсивными функциями, ага.


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

          Потому что вы говорите не о тех минусах.


          Есть минусы в конкретных реализациях, связанные с легаси, например (хаскелю 30 лет, в конце концов), или, наоборот, с молодостью (идрису меньше десятки, и его пилят почти что полтора студента), но они не проистекают непосредственно из свойств ФП.


    1. CheY
      09.12.2019 21:55
      +1

      Касаемо первого абзаца. Вот цитата про функцию из теории категорий в книжке Category Theory for Computer Scientists:

      A function f is a mathematical entity with the fol-
      lowing properties:

      F–1 f has a domain and a codomain, each of which must be a set.
      F–2 For every element x of the domain, f has a value at x, which is an
      element of the codomain and is denoted f(x).
      F–3 The domain, the codomain, and the value f(x) for each x in the domain
      are all determined completely by the function.
      F–4 Conversely, the data consisting of the domain, the codomain, and the
      value f(x) for each element x of the domain completely determine the
      function f.
      The domain and codomain are often called the source and target of f,
      respectively.


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


  1. kuftachev
    09.12.2019 14:21

    Не понял главного, почему, если Вы пишите на C# и считаете, что важно использовать ФП инструменты там, где это возможно, то почему не берете F# для этого.

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


    1. PsyHaSTe Автор
      09.12.2019 14:56

      Честно говоря, не вижу смысла в F#. Ну то есть это тот же C# только с АДТ и странным синтаксисом. При этом под который не работает куча привычных инструментов. Одни только АДТ того не стоят (особенно с релизом паттерн матчинга в C# 7.3), а других плюсов то и нет.


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


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


      1. ApeCoder
        09.12.2019 16:12

        А что такое ХКТ — я по быстренькому не нашел?


        1. PsyHaSTe Автор
          09.12.2019 16:14

          Higher kinded types. Вот пропозал в сишарп: https://github.com/dotnet/csharplang/issues/339


        1. mayorovp
          09.12.2019 16:15

          Higher-Kinded Types


      1. Szer
        09.12.2019 17:13
        +1

        и странным синтаксисом.

        Но ведь это обычный ML...


        1. PsyHaSTe Автор
          09.12.2019 17:52

          Но если вы уже настроились на ML то лучше взять хаскель, разве нет?


          1. Szer
            09.12.2019 18:36

            Но если вы уже настроились на ML то лучше взять хаскель, разве нет?

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


            1. 0xd34df00d
              09.12.2019 18:39

              Я че-т не думаю, что идиоматичных F#-пакетов хотя бы столько же, сколько хаскель-пакетов.


              А рантайм и у ghc отличный.


              1. Szer
                09.12.2019 18:42

                Фшарп затем на дотнете и задумали чтобы присосаться к готовому набору пакетов, а не делать свои с нуля (привет оКамлу, да и хаскелю)
                Драйвера ДБ, СДК, UI, асп нет тот же. Их незачем переписывать.


                Если для Вас это не аргумент, то вперёд писать прод на Блодвене.


                1. 0xd34df00d
                  09.12.2019 18:45

                  Хожу в БД beam'ом и пишу вебки на серванте. А переписывать для идиоматичности — вряд ли заточенные под сишарп либы будут иметь приятный для F# API.


                  А Блодвен уже Идрис 2.


                  1. Szer
                    09.12.2019 18:49

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


                    Большинство усилий направляется на совместимость с сишарпом, а не на ФП фичи именно поэтому.


                    1. 0xd34df00d
                      09.12.2019 18:57

                      Это как раз понятно и вполне разумно. И ровно поэтому и возникает вопрос «зачем писать на F#, когда есть хаскель, если конкретные фишки дотнетовской инфраструктуры не нужны».


                      1. sshikov
                        09.12.2019 21:40

                        >если конкретные фишки дотнетовской инфраструктуры не нужны».

                        Ну, я как-то года четыре назад решил написать на питоне нечто, лазающее в MS SQL. С удивлением обнаружил, что один из имеющихся драйверов тупо обрезает все колонки базы типа varchar до 100 символов. Не то чтобы это было типично, но все-таки в мире Java и .Net такого вы скорее всего не увидите вообще. Это как-бы не очень конкретная фишка, но тем не менее такое имеет место.


                  1. akryukov
                    09.12.2019 21:30

                    Хаскель умеет в hdfs ходить?


                    1. 0xd34df00d
                      09.12.2019 22:09

                      Я не пользовался, но быстрый гугл что-то выдаёт: http://hackage.haskell.org/package/hadoop-rpc-0.1.1.0/docs/Network-Hadoop-Hdfs.html


                      Выглядит стрёмно, но, я слышал, с HDFS иначе никак.


                      1. sshikov
                        10.12.2019 21:35

                        Это еще далеко не весь API :) Вообще я бы сказал так — умение ходить в hdfs не является чем-то абсолютно необходимым. Скажем, я вполне могу себе представить Spark приложение, которое вообще ничего (ну, почти) не знает про HDFS, но при этом перелопачивает терабайты данных в нем. А еще я могу представить Spark приложение на питоне и на R. И ни один из них именно что не умеет пользоваться HDFS (не считая WebHDFS, который в общем-то просто REST API над файловой системой, и который в итоге умеют все).


                        1. 0xd34df00d
                          10.12.2019 22:15

                          Да это всё вообще решаемые проблемы, особенно в случае HDFS или какого-нибудь s3, где латентность весома. На текущем месте уже были какие-то питоноскрипты, инкапсулирующие логику по получению нужных credentials'ов, ретраям и тому подобному, так что я не стал изобретать велосипед (хотя мог бы, амазонка хорошая), а просто дёргал питон из хаскеля.


                          1. sshikov
                            10.12.2019 22:22

                            Ну, ради объективности — разница в производительности все же видимо будет. И иногда даже ого-го какая.


                            1. 0xd34df00d
                              10.12.2019 22:27

                              Если у вас латентность обращения к s3 — 500-1000 мс, то 50-200 мс на запуск интерпретатора и тормоза питона не особо на что-то влияют. По крайней мере, недостаточно, чтобы переизобретать велосипед.


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


                              1. sshikov
                                10.12.2019 22:33

                                Не знаю про конкретно ваш случай, но скажем в случае scala (spark) + python большие накладные расходы появляются просто на то, чтобы дважды преобразовать данные туда-сюда. Запуск интерпретатора при этом одноразовый, а преобразование — на каждую строку файла, грубо говоря. Ну т.е. может когда у вас гигабайты — оно и ничего, а когда начинаются терабайты и петабайты — то уже как-то не хочется терять (впрочем, поэтому у нас и S3 нескоро наверное будет на практике).


                        1. akryukov
                          11.12.2019 07:59

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

                          Я раза три прочитал, но не понял что вы имеете в виду. Как у нас получится читать и обрабатывать файлы в файловой системе так, чтобы не знать о файловой системе?


                          1. gecube
                            11.12.2019 08:16

                            Spark инкапсулирует в себя работу с hdfs. Программисту самому не надо вручную брать некие файлы и читать их через конвенциональный интерфейс работы с hdfs. Более того — Спарк как-то должен учитывать локальность (расположение) данных и он всю магию прячет в себя. Хотя иногда это все ломается, например, есть сделать "безопасный" кластер хадупа, в котором нельзя просто взять и прочитать произвольные данные.


                            1. akryukov
                              11.12.2019 09:09

                              Ну так в этом случае программист вообще ничего не знает про hdfs, а само приложение все прекрасно знает об hdfs.


                              1. gecube
                                11.12.2019 09:21

                                Ну, не совсем. Вы же, например, когда работаете с БД — будь то постгрес, мускуль, оракл — не думаете же о том, как лежат файлы БД на диске? Даже драйвер БД об этом не знает. Это дело лишь самого сервера БД.


                                1. akryukov
                                  11.12.2019 09:37

                                  Каким образом ваш тезис опровергает мой?


                                  1. sshikov
                                    11.12.2019 19:33

                                    Вы в курсе, что такое Hive, его Metastore, и что такое Spark SQL? И где там HDFS, если вы не создаете таблиц?


                                    1. akryukov
                                      11.12.2019 20:29

                                      Думаю что да. Поэтому и спрашиваю. Как вы так напишете спарк приложение, которое ничего не знает о файловой системе?


                                      1. sshikov
                                        11.12.2019 20:44

                                        >Думаю что да.
                                        Теряюсь в догадках — это был ответ на мой вопрос: «И где там HDFS, если вы не создаете таблиц?» :)

                                        val cat= spark.sharedState.externalCatalog
                                        cat.listDatabases.foreach{println}
                                        


                                        Почему вы считаете, что приведенное выше — не спарк приложение? Что вы хотите от приложения, чтобы для вас оно было полноценным?


                          1. sshikov
                            11.12.2019 19:30

                            А зачем вам файлы в файловой системе, если у вас есть скажем Hive?


                            1. akryukov
                              11.12.2019 20:36

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


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


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


                              1. sshikov
                                11.12.2019 20:49

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

                                >У вас что ли ни разу не было, что хайв не может работать с тем, что наделала спарк-джоба?
                                Ну, обычно это все-таки по той причине, что спарк не идеально совместим с Hive. И они по-разному понимают например бакетирование или партиционирование.


              1. Szer
                09.12.2019 18:44

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


                1. 0xd34df00d
                  09.12.2019 18:47

                  Так как ghc генерирует нативный код, то рантайм — это только про вещи типа гринтредов (как с ними в CLR?) и GC (и есть подозрение, что заточенный под иммутабельность он будет эффективнее).


                  Итоговая скорость — скорее вопрос качества компилятора и к рантайму отношение имеет весьма малое.


            1. PsyHaSTe Автор
              09.12.2019 19:02

              Ну так а зачем вам F#? Это тот же C#, только с возвможностью discriminated unions создавать. В чем смысл использовать его? Просто писать на ML синтаксисе? Мне кажется это скорее минус, чем плюс.


              1. Szer
                09.12.2019 19:08

                Продуктивность выше.


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


                Там же не только ДУ (а это как бы супер), ещё комп экспрешны, цитирование, операторы, инлайн функции, SRTP и многое, многое другое.


                1. PsyHaSTe Автор
                  09.12.2019 19:25

                  Ну может быть, надо глянуть.


                  Когда последний раз я смотрел — там был "странный" сишарп. Доклад на FPure про F# меня лично не убедил, но мб докладчик не смог обосновать нормально.


                  И это не потому, что я боюсь ML или пугаюсь новых языков. Инлайн фунции, операторы в сишарпе давно есть. Под цитированием макросы имеются ввиду? Вот это было бы неплохо. И что такое SRTP непонятно, а то гугл выдает только Secure Real-time Transport Protocol.


                  1. Szer
                    10.12.2019 00:28

                    Инлайна в сишарпе нет. Есть атрибут обещания джита.
                    Операторы только стандартные, сделать тот же '>>=' нельзя.


                  1. Szer
                    10.12.2019 00:31
                    +1

                    Извините что вторым ответом, с телефона сложно.


                    Цитирование это преобразование куска кода в AST с последующей работой с ним.


                    SRTP — statically resolved type parameters. возможность сделать хоть хкт, хоть структурную типизацию, хоть любого вида констрейны на тип. Тайп классы описываются в SRTP, да.


                    1. PsyHaSTe Автор
                      10.12.2019 01:11

                      Цитирование это преобразование куска кода в AST с последующей работой с ним.

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


                      SRTP — statically resolved type parameters. возможность сделать хоть хкт, хоть структурную типизацию, хоть любого вида констрейны на тип. Тайп классы описываются в SRTP, да.

                      А можно пример пожалуйста? А то по ссылке только накопипащенные fmap для разных типов.


                      Если можно, то хотелось бы увидеть, что вот так объявляется тайпкласс функтора, вот так пара реализаций (для списка и опшна, например), и вот так можно написать функцию которая будет работать с любым функтором. Сразу стало бы понятно, какие возможности у F#.


                      1. Szer
                        10.12.2019 01:20

                        Лично мое мнение, что это все неидиоматично для фшарпа и выглядит так себе. Но тем не менее...


                        Вот полный набор хкт енкодинга
                        https://eiriktsarpalis.wordpress.com/2019/07/02/applying-the-tagless-final-pattern-in-f-generic-programs/


                        А вот пример инлайн сртп функции, невыразимой в сишарпе


                        // associate HKT encoding to underlying type using SRTPs let inline private assoc<'F, 'a, 'Fa when 'F : (static member Assign : App<'F, 'a> * 'Fa -> unit)> = ()


                        1. PsyHaSTe Автор
                          10.12.2019 01:39

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




                          Что до необходимости ХКТ, лично я считаю, что без возможность объявлять/комбинировать монады, создавать трансдьюсеры и прочее на ФП писать очень копипастно и неудобно. Хотя я, конечно, могу ошибаться.


                    1. nexmean
                      10.12.2019 01:16

                      А вот rank-2 уже нельзя. В том же хаскеле очень трудно найти библиотеку, которая могла бы без них обойтись. Ту же Cont монаду без этого никак не построить.


                      1. Szer
                        10.12.2019 01:37

                        А вот rank-2 уже нельзя. В том же хаскеле...

                        Ну так и фшарп не хаскель, зачем на него ориентироваться?


  1. ferzisdis
    09.12.2019 14:34

    Спасибо за статью, очень интересно читать.

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

    int Foo(Action<string, string> logging){
        logging("info", "just info");
        return 1;
    }
    

    или возвращать кортеж, чтобы рантайм пытался это интерпретировать?
    (int, Log) Foo(){
        return (1, new InfoLog("just info"));
    }
    

    На мой взгляд, это не очень удобно делать. Например, раньше у нас метод просто возвращал число, а теперь мы хотим добавить лог в отладочных целях. Теперь мне нужно переделать всю иерархию вызовов, чтобы она учитывала кортеж или делегат?
    Если это так, то есть другой вопрос: Не будет ли расти количество аргументов в связи с этим до того момента, пока это невозможно будет воспринимать (Например, (int, string, bool, Log, TraceInfo) Foo(bool, Action, Action...) {...} )?


    1. nexmean
      09.12.2019 14:37

      logAndRet1 :: HasLogger m => m Int
      logAndRet1 = do
        logInfo "just info"
        pure 1

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


    1. PsyHaSTe Автор
      09.12.2019 14:59
      +1

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


      1. 4410
        09.12.2019 15:49

        Логирование это что-то нетривиальное?


        1. PsyHaSTe Автор
          09.12.2019 16:10

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


      1. technic93
        09.12.2019 18:08

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

        А как с оптимизацией дела обстоят? Ведь надо компилятору обратно все это раскрутить и поставить инструкцию write там где надо а не тащить это все вверх по стеку.


        1. PsyHaSTe Автор
          09.12.2019 18:15

          С оптимизацией дела обстоят неплохо. В среднем — в пару раз медленнее Rust-плюсового кода. И на порядок быстрее джавы. Я уже приводил цифры, как у нас было 4 версии приложения Java->Scala->Haskell->Rust. С каждой итерацией время выполнения падало в разы. Памяти правда скушал прилично, но всё еще меньше джавы или скалы.


          Upd. Нашел картинку:


          image


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


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


          1. technic93
            09.12.2019 18:19

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


            1. PsyHaSTe Автор
              09.12.2019 18:28

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


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


            1. 0xd34df00d
              09.12.2019 18:28

              Очень хорошо, если конкретная монада известна (то есть, почти всегда).


              Вот этот код у меня работает на 30-50% быстрее, чем аналогичный на плюсах, кстати, и я не понимаю, почему (в дизасм хаскеля вот лезть не хочется). Да, он неидиоматичный, да, хвостовая рекурсия руками, да, аннотации строгости руками, но тем не менее.


              Надо б тоже статью наваять.


              Ещё интересно, что если бы в хаскеле была более мощная система типов, то все эти ансейфриды вполне могли бы быть сейф.


              1. vintage
                09.12.2019 18:35

                Покажите аналогичный код на плюсах-то.


                1. 0xd34df00d
                  09.12.2019 18:37

                  size_t lev_dist(const std::string& s1, const std::string& s2)
                  {
                    const auto m = s1.size();
                    const auto n = s2.size();
                  
                    std::vector<int64_t> v0;
                    v0.resize(n + 1);
                    std::iota(v0.begin(), v0.end(), 0);
                  
                    auto v1 = v0;
                  
                    for (size_t i = 0; i < m; ++i)
                    {
                      v1[0] = i + 1;
                  
                      for (size_t j = 0; j < n; ++j)
                      {
                        auto delCost = v0[j + 1] + 1;
                        auto insCost = v1[j] + 1;
                        auto substCost = s1[i] == s2[j] ? v0[j] : (v0[j] + 1);
                  
                        v1[j + 1] = std::min({ delCost, insCost, substCost });
                      }
                  
                      std::swap(v0, v1);
                    }
                  
                    return v0[n];
                  }


                  1. vintage
                    09.12.2019 18:54

                    Кажется алгоритмы всё же отличаются.


                    1. 0xd34df00d
                      09.12.2019 18:58
                      +1

                      В чем?


                      1. vintage
                        09.12.2019 19:03
                        -1

                        Боюсь это надо спрашивать тех, кто понимает эльфийский.


                        1. 0xd34df00d
                          09.12.2019 19:06
                          +2

                          Ну кажется-то вам же.


                          Оба кода реализуют алгоритм Левенштейна, вместо полной матрицы бегающий по двум последним рассматриваемым рядам.


                          1. vintage
                            09.12.2019 19:41
                            -4

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


                            1. 0xd34df00d
                              09.12.2019 19:58
                              +2

                              А, ну они там все есть, просто записаны по-другому. Вместо for (хвостовая) рекурсия, и так далее.


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


              1. technic93
                09.12.2019 18:53

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


                1. 0xd34df00d
                  09.12.2019 18:58

                  Например в rust есть предложение как можно оптимизировать случаи когда функция прозрачна к Result.

                  Я эту часть не понял.


                  1. technic93
                    09.12.2019 18:59

                    1. 0xd34df00d
                      09.12.2019 19:02

                      Прикольная штука.


                      Оптимальная компиляция ФП-кода — не основной мой интерес, в этом я шарю мало, но в релизнотесах ghc регулярно упоминают всякие оптимизации кодогенератора. Можно их почитать, в принципе.


          1. sshikov
            10.12.2019 21:53

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


            1. PsyHaSTe Автор
              10.12.2019 23:15

              В случае в джавой-скалой потребление одно и то же.
              В случае хаскелля он просто кушает поменьше + гц на нашей нагрузке интереснее работал.
              В случае раста там вообще не принято особо аллоцировать. Любая аллокация в куче — это явные Box::new, которых в программах писать неудобно, поэтому их мало. Сама архитектура раста предполагает максимально переиспользование стека — поэтому перемещение значение, экзистенциальные типы и вот это всё, всё направлено на то, чтобы выделений в куче не было.


              Поэтому его потребление практически константное на любых нагрузках. Программа на расте просто практически не аллоцирует память в куче.


              1. sshikov
                10.12.2019 23:19

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

                А что у вас за объекты, которые аллоцируются? Их сколько в штуках, гигабайтах, и какой у них жизненный цикл, грубо говоря?


                1. PsyHaSTe Автор
                  10.12.2019 23:23
                  +1

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

                  Нельзя, потому что во-первых в JVM нет value-типов.
                  Во-вторых даже в языках в которых они есть куча аллокаций делают сам рантайм. Ну вот пришла к вам строка по HTTP — это аллокация в памяти. Ваш JSON-сериализатор начал её разбирать — нааллоцировал тоже кучу всего. В итоге вы еще не начали даже обрабатывать запрос который поступил, а у вас уже куча аллокаций на ровном месте. Сюда же замыкания (во всех языках они аллоцируются в куче, раст обычно аллоцирует их на стеке), генераторы, и т.п. Даже строки раст умеет аллоцировать на стеке, и десериализовывать JSON на константной памяти.


                  Так, с миру по нитке, голому рубаха.


                  1. sshikov
                    10.12.2019 23:29

                    >Нельзя, потому что во-первых в JVM нет value-типов.
                    Ну, это не мешает в целом работать с какими-нибудь off-heap данными. Ну то есть, я понимаю, что это будет возможно уже и не java, а хз что, я скорее тут про гипотетическую возможность так сделать.


                    1. PsyHaSTe Автор
                      11.12.2019 00:03
                      +1

                      Ну гипотетически возможно всё.


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


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


                      Знатоков JVM/CLR чтобы там тонко их настраивать и писать инлайн-вставки на низкоуровневых языках (потому что в некоторых местах приложения мы опускались вплоть до подсчёта кэш миссов в конкретных циклах), у нас не было, да и не уверен что получилось бы написать быстрее или лучше.



  1. oam2oam
    09.12.2019 15:20

    Да, статья очень интересная и важная! Но тут-то я и понял, почему видимо ФП неприменим в программировании для микроконтроллеров… Там программа — «антифункциональна», если можно так сказать и скорее каждым символом норовит изменить что-то сильно внешнее :)


    1. PsyHaSTe Автор
      09.12.2019 16:10
      +1

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


      1. 0xd34df00d
        09.12.2019 18:30

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


        И даже нужно, данные и коданные полезно различать.


        1. nexmean
          09.12.2019 18:35

          Может стоит тогда быть хотя-бы оптимальным с опциональной ленью, а не как идрис?


          1. 0xd34df00d
            09.12.2019 18:42

            Так там она как раз опциональная и есть, причем двух видов: как коданные (Inf) и именно как лень (Delay). Например, эта и следующая за ней секции.


        1. PsyHaSTe Автор
          09.12.2019 19:08

          Да нет, возьмем тот же раст. Там списки не являются функторами потому что у мапа сигнатура


          fn map<B>(self, f: impl Fn(A) -> B) -> Map<Self<B>> { ... }

          В то время как результатом по идее должно было быть Self<B>. Если бы мы спрятали факт наличия типа Map за боксом у нас бы всё заработало.


          1. 0xd34df00d
            09.12.2019 19:22

            Я Раст не знаю, может, это растоспецифика, но зачем там вообще Map в возвращаемом типе, почему бы просто не вернуть список?


            1. PsyHaSTe Автор
              09.12.2019 19:29

              Потому что компилятор потом всё это дело оптимизирует. Зиро-кост итераторы, всё такое:


              https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=3c2ed4cf80442db1801240caf644ef8f


              Именно для того чтобы обойти боксинги и разбирать эффективно стейт машину.


            1. mayorovp
              09.12.2019 19:47

              Потому что вернуть ленивую обёртку выгоднее.


              1. 0xd34df00d
                09.12.2019 20:00

                Если вы не используете список как генератор (или как поток), то это не вполне очевидно.


  1. ApeCoder
    09.12.2019 16:00
    -1

    Так ведь? С одной стороны да. А с другой именно вторая программа в отличие от первой является функциональной.

    Как это? Там есть *= которая не является ссылочно-прозрачной. Функционально чистой была бы программа, которая только бы вызывала эту функцию, но не состояла из ее определения. Например, если бы это была чистая программа эксплуатирующая грязный рантайм с чистым интерфейсом.


    1. PsyHaSTe Автор
      09.12.2019 16:13

      Это всё еще ссылочно прозрачная программа. Несмотря на изменение локального стейта.


      В качестве ориентира можно посмотреть на язык D, где есть ключевое слово pure. Если мне не изменяет память, в примере его использования они как раз и используют факториал (или фибоначчи) с изменяемым локальным стейтом. Как я говорил, для ФП программы иметь локальный мутируемый стейт это нормально. Редко когда нужно, но — нормально.


      1. ApeCoder
        09.12.2019 16:36

        что такое "локальный мутируемый стейт"? До какой степени локальный?


        *= совершенно аналогичен функции


        void  MultuiplyBy(ref int accumulator, int multiplier)
        {
             accumulator *= multiplier
        }
        

        acumulator не является локальным состоянием для функции MultuiplyBy но может являться локальным для какого-то скопа сверху.


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


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


        С моей точки зрения функция, которую вы написали, является чистой, а программа — нет.


        1. PsyHaSTe Автор
          09.12.2019 16:40

          Это ограничения уже самого сишарпа. По-хорошему оно должно работать с STRef и ничего не модифицировать.


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


          1. ApeCoder
            11.12.2019 11:24

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


            на практике в данном случае не особо мешает

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


            1. PsyHaSTe Автор
              11.12.2019 11:42

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

              С точки зрения D — является. С точки зрения практических свойств программы — тоже, ведь функция ссылочно прозрачная, а значит что она делает внутри нам всё равно.


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

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


              1. ApeCoder
                11.12.2019 13:59

                С точки зрения D — является

                Там определение чистой функции а не чистой программы.


                У нас есть ваше определение


                Функциональная программа — программа, состоящая из чистых функций.

                По которому не понятно, что значит "состоящей из" и вот например из википедии


                In computer science, purely functional programming usually designates a programming paradigm—a style of building the structure and elements of computer programs—that treats all computation as the evaluation of mathematical functions. Purely functional programming may also be defined by forbidding changing-state and mutable data.

                Purely functional programming consists in ensuring that functions, inside the functional paradigm, will only depend on their arguments, regardless of any global or local state.

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


                Что внутри тела, неважно, до тех пор, пока она ссылочно прозрачна.

                Неважно для того, кто вызывает эту функцию, или для программиста который пишет/меняет ее тело?


                1. PsyHaSTe Автор
                  11.12.2019 14:20

                  Там определение чистой функции а не чистой программы.

                  Ну так в нашем случае программа состоит из этой единственной функции. значит все (одна) функции — чистые.


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

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


                  Это была иллюстрация, что так можно делать. Но я нигде не говорю, что так делать нужно :)


                  Неважно для того, кто вызывает эту функцию, или для программиста который пишет/меняет ее тело?

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


                  1. ApeCoder
                    11.12.2019 15:02

                    С нашим факториалом можно обращаться — распараллеливать рассчеты разных факториалов и вот это все.

                    Это иллюстрация того, что можно сделать с чистой функцией. А что можно сделать с чистой программой, что нельзя сделать с нечистой?


                    Есть ли у вас какой-то термин для программы, которая состоит только из вызовов чистых функций?


                    1. mayorovp
                      11.12.2019 15:05

                      Нету у чистых программ (в вашем понимании) никаких особенных свойств, которыми бы не обладали нечистые (в вашем понимании). И именно поэтому выделение особого класса чистых программ не имеет смысла.


                      1. ApeCoder
                        11.12.2019 15:14

                        А в каком-нибудь понимании есть?


        1. vintage
          09.12.2019 17:11

          1. ApeCoder
            11.12.2019 11:29
            -1

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


            Если принять ее за черный ящик

            В программе нет никого, кто использует функцию (т.е. принимает за черный ящик), а, наоборот, есть определение и реализация. Реализация определена не только через чистые функции, => программа не является чисто функциональной, хотя функция является.


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


  1. Flowneee
    09.12.2019 16:13
    +1

    На самом деле из этой сигнатуры


    fn foo<T>(a: &[T], b: T) -> T { ...какое-то тело... }

    Можно даже сделать более интересный вывод: эта функция всегда возвращает b просто потому что


    • сама функция возращает значение типа T (не ссылку);
    • ничего не известно про тип T (в том числе и о том, что его можно копировать/клонировать) + как указано в статье, сконструировать его нельзя по той же прчине;
    • только аргумент b передается по значению.

    То есть функция не сможет вернуть что-либо, содержащееся в a (так как передан по немутабельной ссылке), и не сможет сконструировать сама значение типа T. Остается b.


    1. PsyHaSTe Автор
      09.12.2019 16:17

      Отлично :) Вы нашли пасхалку!


      На самом деле ваше замечание совершенно верно. Функция всегда может вернуть только b из-за того как работают правила владения в расте.


      "Честная" функция которая может вернуть любое значение должна была бы выглядеть так:


      fn foo<'s, T>(a: &'s [T], b: &'s T) -> &'s T { ...какое-то тело... }

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


  1. Dimonkov
    09.12.2019 16:48
    +1

    Еще про сигнатуры и возвращаемые значения:
    Справедливости ради, стоит сказать, что в C# если автор функции с опытом, то он не будет просто возвращать default(T). Он либо затребует new констрейнт на T, либо попросит через параметр/иньекцию фабрику для T, либо будет возвращать некий Result(ну или nullable reference type, когда C# 8 станет попопулярнее). Жаль конечно, что нельзя это заэнфорсить, но в большинстве случаев это так.

    По поводу метода, что берёт на вход строку, а возвращает T, сразу же в голове вот это всплыло — JsonConvert.DeserializeObject(string value);

    https://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_JsonConvert_DeserializeObject__1.htm


    1. bingo347
      09.12.2019 21:44
      +1

      По поводу метода, что берёт на вход строку, а возвращает T, сразу же в голове вот это всплыло — JsonConvert.DeserializeObject(string value);
      В Rust опять же это было бы видно из сигнатуры, так как T в этом случае должен реализовывать Deserialize


  1. Severus1992
    09.12.2019 16:49

    Спасибо за статью, очень познавательно и интересно читать.
    До сих пор мучают следующие вопросы, не могли бы помочь разобраться:
    1. Как должен выглядеть следующий код в функциональной парадигме:
    public class Car
    {
    public Car(string name, int power)
    {
    Name = name;
    Power = power;
    }
    public string Name { get; }
    public int Power { get; private set;}

    void Add(Turbo turbo)
    {
    Power += torbo.Power;
    }
    }

    public class Turbo
    {
    public Turbo(int power)
    {
    Power = power;
    }
    public int Power { get;}
    }


    Метод Add должен возвращать новый объект Car?
    Car Add(Turbo turbo)
    {
    return new Car(this.Name, this.Power + turbo.Power);
    }

    Если да, то есть вероятность, что программист возьмет старый объект класса, забыл про новый из результата выполнения метода:
    var car = new Car("name", 100);
    var turbo = new Turbo(50);
    car.Add(turbo);
    return car;

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

    Вопрос номер два:
    Означает ли использование ФП частичный отказ от использования паттерна Состояние (State)?


    1. gsedometov
      09.12.2019 18:17

      в условном хаскеле будет примерно так:
      car = mkCar "name" 100
      turbo = mkTurbo 50
      add car turbo


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


      1. Чтобы реализовать шаблоны State и Strategy не нужно ничего, кроме чистых функций


    1. CheY
      09.12.2019 23:50
      +3

      Шанс того, что ошибёшься и возьмёшь старый объект, не больше, чем в императивном стиле. Если программист кладёт старое значение в переменную, а потом новое значение в другую переменную, то вероятность ошибки есть всегда и не зависит от того, как писать.

      Иммутабельность не про это, а про то, что объект X всегда остаётся неизменным с момента создания. И если, например, X был передан в поток A, то он никак не изменится, что бы не происходило в потоке B.

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

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


  1. Davidov
    09.12.2019 19:00
    +1

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

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


    1. PsyHaSTe Автор
      09.12.2019 19:17

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


      1. sshikov
        10.12.2019 23:23

        >хотя скорее всего не делаем
        Это оптимистичный взгляд на вещи :) один из минимум двух возможных…


        1. PsyHaSTe Автор
          10.12.2019 23:27

          Это реалистичный взгляд)


          Я исхожу из того, что программисты неглупые люди. Ленивые, забывчивые, часто опечатывающиеся, но не глупые.


          А такую функцию можно написать только специально, из вредности, из разряда


          #define TRUE FALSE // счастливой отладки

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


          1. sshikov
            10.12.2019 23:35

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

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


  1. jayb19
    09.12.2019 21:15
    +1

    Статья как бальзам на душу!

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


    1. gecube
      09.12.2019 23:17

      Можно вывести правило. Сомневаешься? Строй свою стейт машину )


      1. jayb19
        10.12.2019 07:04

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


  1. JordanoBruno
    09.12.2019 22:26

    Хотя люди обычно признают удобства ФП фич, ведь намного приятнее писать..

    Уже в первом примере идет какая-то вкусовщина. С таким подходом можно что угодно оправдать: «Хотя люди любят пиво, но ведь намного приятнее выпить водки..»


  1. rmrfchik
    09.12.2019 23:18

    ФП это не набор соглашений, требований и ограничений. ФП не накладывает никаких требований на неизменяемость, чистоту и прочее.
    ФП это набор согласованных правил.
    Просто это правила отличаются от императивных (а не от ООП). ООП это вообще игра на другом поле. ФП можно сравнивать с императивной парадигмой, но не с ООП.
    Что касается приведённого примера, то в ней, например, result *= i; является операцией, которая в ФП отсутствует.
    Можно как угодно себя уговаривать, что это программа в стиле ФП, но она такой не станет. Нельзя в середине уравнения сократить на 0 и получить верный результат. Даже если он очень похож на верный.


    1. PsyHaSTe Автор
      09.12.2019 23:24
      +1

      Что касается приведённого примера, то в ней, например, result *= i; является операцией, которая в ФП отсутствует.

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


      factorialST :: Int -> Int
      factorialST n = runST $ do           
                      result <- newSTRef 1         // инициализируем начальное значение 1
                      forM_ [2..n] $ \x -> do      // для каждого значения 2..n
                          modifySTRef result (*x)  // мутируем наш result, домножая его на x
                      readSTRef result             // читаем результат из ячейки памяти result

      Вот ссылка на плейграунд, если мне не доверяете: https://repl.it/@Pzixel/PeruFantasticCosmos


      Ну и если остались какие-то сомнения:


      What is the difference between forM and forM_ in haskell? The forM_ function is more efficient because it does not save the results of the operations. That is all. (This only makes sense when working with monads because a pure function of type a -> () is not particularly useful.)

      То есть forM_ это простой цикл, он не сохраняет никаких результатов.


  1. rmrfchik
    09.12.2019 23:41

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


    И да, хаскель конечно разрешает "нечистый код" с помощью unsafe.


    Всё это от того, что в ФП отсутствует понятие потока выполнения, а есть только правила преобразования которые undecidable. Поэтому надо в ФП прийти "снаружи" и навести порядок железной рукой. Что и делают все эти наши компиляторы.


    1. PsyHaSTe Автор
      09.12.2019 23:54
      +1

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


      Понятно, что в конце концов оно там развернется, а потом опять свернется (потому что интерпретируем мы на вполне себе императивном ЦПУ), но это всё уже не важно. Код практически 1 в 1 повторяет то что написанно на сишарпе. То что тут есть явная ст монада, а в сишарпе нужно за этим руками следить — ну тут уж извините, не завезли туда такого.


      1. rmrfchik
        10.12.2019 07:56

        Да нет же. Хоть оно и выглядит как утка, но не крякает, как утка. Хотя бы потому, что хаскельный кусок кода я могу "взять с собой" по программе, а императивный нет.
        В том смысле, что modifySTRef result (*x) это обычное применение функции, которое в нотации do выглядит, как утка. Но его не обязательно так применять.


        Ввиду отсутствия потока выполнения, хаскельный чётко определяет вход-выход функции и зависимости. У императивного подхода такой "свободы" нет — для того, чтобы понять результат print(i++, ++i++) в императивном языке нужно заглянуть в спецификацию и молится, чтобы там не было написано undefined behaviour, а в функциональном в код монады, а там никакого undefined.


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


        1. mayorovp
          10.12.2019 08:32

          Что вы понимаете под "взять с собой"?


          1. rmrfchik
            10.12.2019 12:26

            Присвоить переменной это действие и вызывать по необходимости.


            1. mayorovp
              10.12.2019 12:47
              +1

              Ну так и для result *= i так тоже можно сделать, просто кода будет больше.


              1. rmrfchik
                10.12.2019 14:09

                В общем случае нет. Ну или много будет плясок и мы придумаем монады и замыкания.
                Вот как есть result *= i нельзя присвоить переменной и звать по необходимости.
                Та же ява требует переменные effectivelly final для засовывания в замыкание.


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


                1. mayorovp
                  10.12.2019 14:20

                  А те же C# и Kotlin этого от переменных не требуют


                  1. rmrfchik
                    10.12.2019 16:52

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


                    Мой основной поинт в том, что modifySTRef не модифицирует переменную в замыкании.


                    Если мы поместим result = i в замыкание и позовём это замыкание три раза, то получим result=resulti*3.
                    Если мы позовём modifySTRef три раза, то всё зависит от нашей композиции. Обычая do нотация тоже построит цепочку вызовов, а другая монада легко может свести всё к одному вычислению. Вопрос в управляемой композиции.


                    Поэтому монады в целом и do-нотация в частности это не про нечистый код в хаскеле, это про композицию.


                    1. vintage
                      10.12.2019 17:06

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


                    1. PsyHaSTe Автор
                      10.12.2019 17:07

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

                      Ничего особенного — копируют их в учу. Поэтому и возможны любопытные эффекты:


                      var lambdas = new List<Action>();
                      for (int i = 0; i < 5; i++)
                      {
                          lambdas.Add (() => Console.Write(i));
                      }
                      
                      foreach (var lambda in lambdas)
                      {
                          lambda();
                      }

                      Выведет 55555.


                      Поэтому монады в целом и do-нотация в частности это не про нечистый код в хаскеле, это про композицию.

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


                      Но в том же D рантайм не такой умный, поэтому там есть компилятор, который следит что вы мутируете только локальные переменные. И они получаются вполне себе чистыми. Вот ссылка на документацию.


                      1. vintage
                        10.12.2019 17:40
                        -3

                        Важный и совсем неочевидный вопрос: возвращаемый функцией finder делегат использует значение x, а где находится x после того, как finder вернет управление? На самом деле, в этом вопросе слышится серьезное опасение за происходящее (ведь D использует для вызова функций обычный стек вызовов): инициатор вызова вызывает функцию finder, x отправляется на вершину стека вызовов, функция finder возвращает результат, стек восстанавливает свое состояние до вызова finder… а значит, возвра­щенный функцией finder делегат использует для доступа адрес в стеке, по которому уже нет нужного значения! «Продолжительность жизни» локального окружения (в нашем случае окружение состоит только из x, но оно может быть сколь угодно большим) — это классическая проблема реализации замыканий, и каждый язык, поддерживающий замыкания, должен ее как-то решать. В языке D применяется следующий подход. В общем случае все вызовы используют обычный стек. А обнаружив замыкание, компилятор автоматически копирует используемый контекст в кучу и устанавливает связь между делегатом и областью памяти в куче, позволяя ему использовать расположенные в ней данные. Выделенная в куче память подлежит сбору мусора. Недостаток такого подхода в том, что каждый вызов finder порождает новое требование выделить память. Тем не менее замыкания очень выразительны и позволяют применить многие интересные парадигмы программирования, поэтому в большинстве случаев затраты более чем оправданны.


                      1. rmrfchik
                        10.12.2019 17:48

                        Ничего особенного — копируют их в учу. Поэтому и возможны любопытные эффекты:

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


                        (define ilist (let loop ((i 0) (i-clos '()))
                                        (if (= i 4)
                                            i-clos
                                            (loop (+ i 1) (append i-clos (list (lambda () (display i))))))))
                        (for-each (lambda (clos)
                                    (clos))
                                  ilist)

                        выведет 01234


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

                        Может оно так и выглядит, но устроено совсем по другому, несёт другие гарантии и вообще, это ДРУГОЕ. Просто ФП это другое программирование, это не императивное с ограничениями, а другое.
                        Если пытаться на него смотреть с императивной колокольни, то всё будет казаться кривым, недоделанным и ограниченным. Лучше, мне кажется, понять, что лежит в его соно


                      1. Druu
                        10.12.2019 18:49

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

                        Так оно дешугарится ровно в те же бинды, в которые может дешугариться код на c# с ровно теми же самыми гарантиями безопасности. С формально-математической точки зрения код в ио невозможно отличить от обычного императивного кода в обычном императивном ЯП.


    1. 0xd34df00d
      10.12.2019 00:28
      +1

      И да, хаскель конечно разрешает "нечистый код" с помощью unsafe.

      Это нечистый код, прикидывающийся чистым. Чинится при помощи {-# LANGUAGE Safe #-}.


  1. kxl
    10.12.2019 01:27

    ФП стиль возможен даже на JS… www.ozon.ru/context/detail/id/142823895 — хорошая книжка, помогает сменить мышление


    1. CheY
      10.12.2019 02:08
      +1

      Если бы он ещё наряду с «возможностями» давал «ограничения» и «гарантии») Хотя всегда есть возможность использовать транспайлеры типа Elm и PureScript.


      1. bingo347
        10.12.2019 08:14

        Ограничений частично можно достичь с помощью некоторых плагинов eslint, не настолько идеально конечно, как в Elm/PureScript/ReasonML, но вполне достаточно, чтоб писать чистый код… С гарантиями чуть сложнее, хотя и их можно достичь с помощью некоторых библиотек.
        Мне вполне удается писать вполне себе ФП код на JS/TS, и я может и не против пересесть на PureScript, вот только я с ним не расширю свою команду…


      1. PsyHaSTe Автор
        10.12.2019 17:02
        +3

        1. LongMap
          11.12.2019 21:27

          Люди не смотрят на другие языки ибо могут все на плюсах?


          1. PsyHaSTe Автор
            11.12.2019 21:29

            Нет, перевод будет примерно таким: "По [странному] совпадению, я недавно начал писать на Rust. Я нахожу странным это чувство: люди смотрят на другие языки не потому, что вы не можете что-то сделать в С++, а потому что вы можете [сделать много нехороших вещей]".


    1. jayb19
      10.12.2019 07:07

      На JS он смотриться намного орагничнее чем притянутые классы.


    1. bingo347
      10.12.2019 08:15

      А есть где в электронном виде эту книжку купить? Не люблю за бумагу переплачивать


  1. third112
    10.12.2019 06:22

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

    [...]

    предложенная реализация будет обладать [худшей] вычислительной сложностью

    [...]

    и что с такой сложностью не дождаться, когда, например, просеются 10 миллионов чисел.
    М.б. тут спецы в ФП снимут это ограничение и предложат быстрое решение РЭ в ФП? Заранее спасибо!


  1. Legomegger
    10.12.2019 07:16
    +1

    Спасибо за хорошее объяснение как используются «необходимые грязные детали» в ФП. И за понятное определение

    Ссылочная прозрачность — свойство, при котором замена выражения на вычисленный результат этого выражения не изменяет желаемых свойств программы

    Очень познавательно!


  1. Druu
    10.12.2019 09:04

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

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


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

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


    Тогда наша функция будет иметь вид:

    И она точно так же тривиальна и ее нельзя протестировать. К счастью, мы можем заменить прямые вызовы конструкторов Charge/Cofee на соответствующие фабрики (ну никто же в здравом уме не будет создавать счет не через абстрактную фабрику, правда ведь? ;)) и замокать их. И написать тест с моками.


    А вот если это тест, то он просто может проверить возвращенный объект Charge на все интересующие его свойства.

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


    1. mayorovp
      10.12.2019 09:41

      И она точно так же тривиальна и ее нельзя протестировать. К счастью, мы можем заменить прямые вызовы конструкторов Charge/Cofee на соответствующие фабрики

      А в чём трудность в её тестировании и в чём смысл создавать Value Object через фабрику?


    1. PsyHaSTe Автор
      10.12.2019 13:59
      +1

      Ноуп. Отсутствие эффектов не является ни достаточным, ни, что самое интересное, необходимым свойством ФП. Ф-и в стейт-монаде (ну и ИО как следствие) семантически грязные, но их использование ФП не "ломает".

      В ФП нет эффектов, а "ду монада" это просто сахар для пачки биндов. А то что a.bind(b) и b.bind(a) не всегда эквивалентны вроде должно быть очевидно.


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

      Проверить что покупка 10 чашек кофе возвращает суммарный чек в 10 раз больше чем покупка одной чашечки — это проверка компилятора?


      И она точно так же тривиальна и ее нельзя протестировать. К счастью, мы можем заменить прямые вызовы конструкторов Charge/Cofee на соответствующие фабрики (ну никто же в здравом уме не будет создавать счет не через абстрактную фабрику, правда ведь? ;)) и замокать их. И написать тест с моками.

      Ага, если написать фабрику через фабричный вызов еще одной фабрики, то все проблемы сами собой решаться. Нет, я не буду использовать фабрику пока мне это не надо. Со случаями, когда её лучше использовать, можно ознакомиться у gof'а или фаулера.


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

      Да, но у нас в системе скорее всего есть сотни мест, которые выставляют чеки на те или иные действия, а реально списывает их один "списывальщик". Упростить эти сотни мест — вполне ценная вещь.


  1. dididididi
    10.12.2019 09:57
    -10

    П-ц. Вначале было три строчки про кофе, понятные даже дауну(мне). Идеальный код. Никаких багов, потому что он примитивный как лом. Там тестов не нужно, потому что в таком тупом коде не может быть багов!

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

    Отрубать руки по локоть и не подпускать к компу таких.


    1. jayb19
      10.12.2019 10:11
      +3

      Есть такая вещь как пример. Если бы автор поместил код из рабочего проекта было бы лучше? Для наглядности и пишется такой «идеальный код».

      Отрубать руки по локоть и не подпускать к компу таких

      Программирование уже давно перестало быть чем-то специфическим. Как раз благодаря смелым и нестандартным решениям мы имеем полную свободу в кодинге. Ограничения только в голове. Если мы не согласны с чем-то это просто субьективное мнение. Зачем же так радикально?


      1. dididididi
        10.12.2019 10:58
        -5

        Было бы лучше, если бы он поместил наглядный пример. Было плохо — стало хорошо.

        А так мне сказали: сейчас я научу вас забивать гвоздь в доску, для это нужно семь датасаентистов, протонный ускоритель, пластинка из чистого урана, эталон килограмма из палаты мер и весов и полтора месяца времени. А я могу сделать это только молотком секунд за 30. Придумай и приведи пример, где это может быть полезно!

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


        1. jayb19
          10.12.2019 13:06
          +6

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


          1. dididididi
            10.12.2019 13:47
            -3

            Формат статьи, как бы должен что-то вроде: «было плохо->применил неведомую хрень ->стало хорошо ->Юзайте эту хрень!»

            Тут же наоборот: было хорошо -> стало плохо. Придумайте сами, где юзать хрень, у меня не получилось


            1. jayb19
              10.12.2019 14:27
              +3

              Формат статьи определяет автор. Никто никому ничего не должен. Если в человеке недостаточно свободы — он сам так выбрал. Человек по своей(стадной) природе любит все усложнять, ограничивать, но больше всего — перекладывать ответственность. Рамки только в голове.
              Если вы не видите где можно применить подход, описаный автором, значит вам это не требуется. Мы же не пьем таблетки от простуды когда здоровы. Но есть заболевшие люди, которые в них нуждаются, а вы, оставаясь в полном здравии, сетуете на горькоту этих таблеток.
              Я надеюсь что понятно изложил суть ваших необоснованых замечаний.


              1. dididididi
                10.12.2019 16:47
                -4

                В США учат писать эссе, в РФ нет, поэтому считается, что можно писать как попало)) Типа свобода и автор так видит.

                Ну вы точнее описывайте)) Сюжет статьи такой: «У меня был кашель, я поел заячьего помета, теперь у меня понос. Юзайте.» И тут вы: Эгей! Статья ништяк, потому что я давно хотел пролечить свой запор.

                У автора тоже нет простуды, но горьких таблеток он наелся.


    1. PsyHaSTe Автор
      10.12.2019 14:04
      +1

      Ну вот вам "идеальный код" для второго случая:


      public (Coffee[], Charge) BuyCoffees(int count, CreditCard card)
      {
          Coffee[] coffees = new Coffee[count];
          Charge totalCharge = Charge.Empty(card);
          for (int i = 0; i < coffees.Length; i++) {
              var (cup, charge) = BuyCoffee();
              coffees[i] = cup;
              totalCharge = totalCharge.Combine(charge);
          }
          return (coffees, totalCharge)
      }

      Так вам стало понятнее, что происходит?




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


      1. UnclShura
        11.12.2019 17:26

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


        1. gecube
          11.12.2019 17:39
          +1

          Может быть потому что в императивном мире тоже могут сначала налить кофе, а только лишь потом убедиться в том, что клиент неплатежеспособен? :-)


        1. PsyHaSTe Автор
          11.12.2019 17:50

          Ну очень просто ведь: налили кофе, выставили за него счет. Посмотрели, хватает ли средств на списание. Если хватает, списали деньги и отдали кофе, иначе выкинули кофе и клиенту его не отдали.


          1. UnclShura
            11.12.2019 18:14

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


            1. mayorovp
              11.12.2019 18:32
              +1

              Всё, что вы перечислили, делается снаружи этого кода.


              Вот мы вызвали код, у нас есть Coffee[] и Charge. Теперь можно:


              1. заблокировать ту сумму, что записана в Charge
              2. налить кофе, которое записано в Coffee[]
              3. подтвердить транзакцию.

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


              1. UnclShura
                11.12.2019 19:22

                Ну так а я о чем? В таком виде функция просто фабрика объектов. Без обвязки с транзакцией и другого аццкого состояния смысла в ней особого нет, да и на лбом не-ФП языке так написать можно. Я прекрасно понимаю что ФП делает в вычислениях. Но как только начинаются реальные задачи, где на каждом углу массивы с ветвлением логики, транзакции, ввод/вывод и UI это чуть-ли не 90% где они эти достоинства?

                Про чистые вычисления тоже можно потом песнь затянуть с распаралеливанием на 10К процессоров, графических сопроцессоров и прочих ASIC.


                1. mayorovp
                  11.12.2019 19:28
                  +1

                  Смысл в этом есть, называется "бизнес-логика".


                  1. UnclShura
                    11.12.2019 19:44
                    +1

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

                    А вы говорите чистые функции :)


                    1. PsyHaSTe Автор
                      11.12.2019 20:40
                      +2

                      Открыл один из репозиториев моей текущей работы, посмотрел в бизнес-логику, не нашел там хаков и костылей.


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


                      1. UnclShura
                        12.12.2019 12:24
                        +1

                        Ну вот уж нет. Гораздо безопаснее исходить из того, что все, что написано год назад волшебным образом превращается в адище. Технология не стоит на месте и то, что было бело и пушисто вчера сегодня попахивает. Вспомните goto, singleton, да собственно наследование vs композиция. Где гарантия, что завтра условно монады не статут с душком?

                        Вот как раз по-этому весьма важно как язык и среда (они теперь неотделимы!) заставляет писать просто, поддерживает рефакторинг и насколько он гибок к изменениям. За это C# и люблю. Это нифига не гарантия, что он не устареет со временем, но по крайней мере пока, все кругло.


                        1. koldyr
                          12.12.2019 12:36
                          +3

                          Где гарантия, что завтра условно монады не статут с душком?


                          Гарантия в том, что это не практики а математика.
                          Они могут морально устареть, но стать от этого неправильными не могут никак.


                        1. PsyHaSTe Автор
                          12.12.2019 14:18

                          Гарантии примерно те же, как то, что завтра int не устареет, и строчки внезапно принимать не начнет.


                          1. UnclShura
                            12.12.2019 14:21

                            Бросьте. Вспомните float vs double. Где тот float?


                            1. PsyHaSTe Автор
                              12.12.2019 15:07
                              +1

                              И? Программы работающие с ним как работали так и работают.


                              Если вам нужно будет починить точность — вы сделаете тривиальный фикс. В чём проблема?


                            1. 0xd34df00d
                              12.12.2019 20:31
                              +2

                              Регулярно использую. Меньше по размеру, больше влезает в SIMD-регистр/кешлайн/шину памяти/память.


            1. PsyHaSTe Автор
              11.12.2019 18:45

              А откуда вы узнаете, сколько денег за счёт надо выставить не разливая чашек? Ведь если вы вспомните, то цена чашки определяется через cup.Price. Вам нужно иметь уже налитую чашку кофе чтобы посчитать, сколько она стоит.


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


              1. gecube
                11.12.2019 20:13

                Коллега, видимо, будет плодить сущности. Типа FutureCupOfCoffee, которая представляет намерение о покупке чашки кофе. CupOfCofee — сама чашка кофе. Charge — класс объекта списания денег. И в результате простая, казалось бы задача, приводит к эволюционному взрыву и появлению целой вселенной классов )
                Касательно дизайна: да, есть два сценария. Сначала платим — потом получаем кофе (кафешки с барной стойкой, автозаправки и пр.) И обратный сценарий — сначала заказываем кофе в Н чашек, пьем и только потом оплачиваем (как в ресторанах с официантом). И обязательно — оставить возможность переключения логики с первой на вторую. У нас же эджайл и гибкая внешняя среда )))


                1. PsyHaSTe Автор
                  11.12.2019 20:38

                  Я про такой дизайн и подумал, как, наверное, и любой человек читающий эту ветку. Правда я представлял это как CoffeeOrder или CoffeeTicket.


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


        1. 0xd34df00d
          11.12.2019 19:55

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

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


        1. koldyr
          11.12.2019 20:25

          … или негарантированном порядке вычислений

          и тут, внезапно, на сцене появляется ее величество ассоциативность.


  1. apxi
    10.12.2019 11:28

    Тут гадание о работе функции по сигнатуре и на простом примере уже несколько постов о том как там в ней внутри все должно быть устроено, а Вася возьмет и сделает внутри этой функции по другому, а не так как вы по сигнатуре гадаете и все равно придется в нее лезть и изучать как она там работает и пока что мне кажется что в обычном коде разобраться легче (может быть привык х.з.).
    Попробуйте сделать пример чистой функции по расчету НДФЛ по одному сотруднику (на входе id сотрудника и дата на которую нужно сделать расчет, на выходе сумма).
    Каждый инструмент нужен в своем месте, где то удобней функциями особенно в математике, где то в ООП, где то процедурное программирование. Главное не впадать в крайности и уметь выбрать самый правильный инструмент, ну или хотя бы наиболее подходящий в данном конкретном случае.


    1. gecube
      10.12.2019 12:22

      Попробуйте сделать пример чистой функции по расчету НДФЛ по одному сотруднику (на входе id сотрудника и дата на которую нужно сделать расчет, на выходе сумма).

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


    1. PsyHaSTe Автор
      10.12.2019 14:10
      +1

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

      Так не получится же.


      Я же и говорю, про силу языков. Если у вас сигнатура функции T -> T, то у вас есть три варианта как её написать:


      1. вернуть переданный аргумент fn foo<T>(x: T) -> T { x }
      2. войти в вечный цикл fn foo<T>(x: T) -> T { foo (x) }
      3. завершить работу с паникой: fn foo<T>(x: T) -> T { panic!("I won't work!") }

      На этом перечень того что вы можете сделать не совершив UB нет. В ФП языке вы НЕ можете


      1. создать объект T локально
      2. считать его из внешнего источника
      3. сгенерировать его на ходу
      4. ...

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


      Попробуйте сделать пример чистой функции по расчету НДФЛ по одному сотруднику (на входе id сотрудника и дата на которую нужно сделать расчет, на выходе сумма).

      А какие тут сложности должны возникнуть? Вон, компиляторы на хаскеллях пишут, я не думаю, что они проще чем расчёт НДФЛ. Правда, ваш код надо будет завернуть в Reader монаду или еще какую, потому что айди сотрудника явно недостаточно, нужно в базу сходить и вот это всё, в остальном не вижу никаких проблем.


      1. apxi
        10.12.2019 14:38

        Мне просто хочется посмотреть как этот код (расчет НДФЛ) будет выглядеть и как сложно его будет понять. Много пишут про то что ФП это круто, но ни кто пока не привел нормального примера работы с БП и каких либо расчетов (тот же НДФЛ или расчет закрытия месяца или там расчет себестоимости). Может я плохо искал, но я пытался найти, пытался сам понять, не понял, может стар уже стал, но увы…


        1. PsyHaSTe Автор
          10.12.2019 14:44
          +5

          Ну так давайте внятное ТЗ :)


          Ну например: есть БД, в ней хранятся такие-то сущность пользователя. Написать консольную программу, которая по айди сотрудника и двум датам выведет НДФЛ начисленный за это время. Формула для рассчета вот такая-то.


          Что-то в таком духе.


  1. maxim_ge
    10.12.2019 12:02

    public class Cafe
    {
        public (Coffee, Charge) BuyCoffee(CreditCard card)
        {
            var cup = new Coffee()
            return (cup, new Charge(card, cup.Price))
        }
    }
    

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

    Другой вариант — ядром реализовать «грязные действия» типа BuyCoffeeIfInStock… но как-то слишком много грязи выходит…

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

    Есть идеи, как это изящно сделать на чистых функциях?

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


    Чашка кофе продается обычно в рамках чего-то большего, и вот для этого большего вполне себе оправдано «притащить фреймворк».


    1. koldyr
      10.12.2019 12:09
      +2

      Вы просто не умете готовить этих кошек.


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


    1. PsyHaSTe Автор
      10.12.2019 14:19

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

      Ну, в ФП принято вместо зависимостей отдавать результат вычисления этих зависимостей. Поэтому вы скорее всего сделаете еще один параметр bool shouldReturnCoffee. Правда, в таком простом случае проще вызывающему коду не вызывать эту функцию)


      На дотнексте 2018 был хороший доклад от Марка Симана на тему как работает DI в функциональных языках. Очень рекомендую почитать: https://blog.ploeh.dk/2017/01/27/from-dependency-injection-to-dependency-rejection/


      Есть идеи, как это изящно сделать на чистых функциях?

      Монады :) Причем я серьезно, "монада" это как "паттерн" в ООП — решение всех частовстречаемых проблем. Например, есть монада которая позволяет таскать неявный контекст кроме явно переданных параметров, получается как конструктор в ООП (в который мы передали параметры чтобы в каждый метод отдельно их не совать), есть монада Writer которая кроме возврата результата позволяет делать какие-то еще действия, например, писать в лог. Но в отличие от нашего пример с Log.Info это всё контролируется вызывающим кодом и он может с этим что-то сделать (например, выкинуть накопленные логи). В императивном коде вы так сделать не сможете.


      Чашка кофе продается обычно в рамках чего-то большего, и вот для этого большего вполне себе оправдано «притащить фреймворк».

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


  1. trolley813
    10.12.2019 12:40

    А с другой именно вторая программа в отличие от первой является функциональной.

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


    1. PsyHaSTe Автор
      10.12.2019 14:22

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


  1. Igor_ku
    10.12.2019 13:37
    +2

    В универе учим haskell, и после 3 месяцев изучения начинает казаться, что это идеальный язык. Может, мнение измениться когда дойдем через пару недель до монад. Но я искренне не понимаю, почему доля ФП в продакшене такая маленькая. Как по мне, ФП языки очень недооценённые, а очень жаль!


    1. CheY
      10.12.2019 16:14
      +2

      По своим ощущениям.

      Да, первое знакомство и базовые вещи на уровне содержимого книжки «Learn You a Haskell for Great Good» оставляет исключительно положительные впечатления.
      Но потом ты пишешь первую неучебную программу и ловишь segfault от сжирания всей памяти, пытаясь обработать какой-нибудь файлик в 500 Мб. Понимаешь всю цену «ленивости» языка, и что хоть Haskell по умолчанию ленивый, но программы рекомендуется писать по умолчанию строгими. Читаешь про BangPattern'ы, deepseq, разницу между Lazy и Strict (а потом value-strict и spine-strict) версиями структур данных, про unboxed типы — во многих книгах этого просто нет.

      Затем смотришь на то, как применяют Haskell для реальных больших приложений, а не просто «типа скриптов» — например, в веб-фреймворках. Открываешь описание работы Servant'а и понимаешь, что все те описания монад, трансформеров и прочих typeclass'ов в книжках было лишь самой вершиной айсберга и тебе не рассказали про 101 pragm'у языка, каждая из которых добавляет новые возможности языка, поверх которых строится функционал каждого второго реального приложения на Haskell. И эти прагмы неожиданно сложные — даже не в плане того, как они реализованы, а в плане того, что они тебе дают. Например, попробовать сразу после LYHS разобраться в содержимом статьи wiki.haskell.org/GHC.Generics — гиблое дело.

      И тут становится очевидной главная, имхо, проблема Haskell'а — очень большой gap между книжками и реальным production-использованием. И это на фоне действительно большей сложности языка по сравнению с другими мейнстримными. Если взять сферический rails, то после 1-2 книжек ты сможешь без особых сложностей освоить RoR на отличном уровне и быстро разбираться в новых возможностях, пролистывая доку/исходник. А с Haskell это не так. Радует только то, что эта сложность абсолютно логична и объяснима, чего иногда нет в других языках. В Haskell действительно очень многие вещи делаются абсолютно иначе, чем в привычных императивных языках и здесь совсем другая, более фундаментальная и вместе с тем очень утилитарная роль типов.


      1. Igor_ku
        10.12.2019 20:49

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

        Однако, что точно является фактом, это то что haskell навсегда меняет стиль программирование в Java например


      1. 0xd34df00d
        10.12.2019 20:56
        +1

        Надо разделять сложность языка и сложность задачи.


        Но потом ты пишешь первую неучебную программу и ловишь segfault от сжирания всей памяти, пытаясь обработать какой-нибудь файлик в 500 Мб. Понимаешь всю цену «ленивости» языка, и что хоть Haskell по умолчанию ленивый, но программы рекомендуется писать по умолчанию строгими.

        Так как раз эта ленивость и позволяет обрабатывать файлы в 500 мегабайт (или 500 гигабайт, были и такие) в O(1) по памяти, сжирая 1-2-10 мегабайт на всё. Единственное, где вам придётся вспоминать про ленивость — когда вы будете выбирать, импортировать Data.HashMap.Lazy или Data.HashMap.Strict, условно.


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


        Читаешь про BangPattern'ы, deepseq, разницу между Lazy и Strict (а потом value-strict и spine-strict) версиями структур данных, про unboxed типы — во многих книгах этого просто нет.

        ЕМНИП оно есть в Parallel and Concurrent Programming in Haskell.


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


        Открываешь описание работы Servant'а и понимаешь, что все те описания монад, трансформеров и прочих typeclass'ов в книжках было лишь самой вершиной айсберга и тебе не рассказали про 101 pragm'у языка, каждая из которых добавляет новые возможности языка, поверх которых строится функционал каждого второго реального приложения на Haskell. И эти прагмы неожиданно сложные — даже не в плане того, как они реализованы, а в плане того, что они тебе дают.

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


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


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

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


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


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

        Ну, собственно, вы про это и пишете.


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


      1. sshikov
        10.12.2019 21:50
        +1

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

        Условно — замените у себя «файлик 500Мб» на 10Tb, и у вас будут проблемы в любой парадигме при любом подходе, если вы его изучили на уровне Learn You a Haskell for Great Good, и там остановились.


      1. 0xd34df00d
        12.12.2019 01:52

        во многих книгах этого просто нет

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


        Это, на самом деле, дико иронично (книга по библиотеке, ё-моё), но тем не менее.


  1. queses
    10.12.2019 14:22
    +1

    Отличная статья!

    Хотелось бы узнать побольше про разделение «чистого кода» и «грязного рантайма»: как они связываются, как выглядят описания DB или HTTP -запросов, что в такой архитектуре происходит с DI.

    Материала, как мне кажется, на ещё одну статью, был бы очень рад прочитать


    1. PsyHaSTe Автор
      10.12.2019 14:23

      Спасибо, я старался!


      По поводу DI я чуть выше дал ссылку на прекрасную статью, там это хорошо расписано.


  1. im_age
    10.12.2019 14:25
    +1

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

    В первом примере есть вывод в лог (условный print из последнего примера), и мы считаем, что программа написана не в стиле ФП, потому что если заменить Factorial(5) на 120, у нас перестанет выводиться в лог сообщение.

    Собственно вопрос в — а чем бы отличалось поведение программы, если вместо вывода в лог была бы функция с print (как в haskell из последнего примера) и грязный интерпретатор?
    Мне кажется замени мы в этом случае вызов функции факториала на ее значение, мы все еще потеряли бы вывод в лог/консоль сообщения.


    1. PsyHaSTe Автор
      10.12.2019 14:37

      Добрый день, отличный вопрос.


      В случае с хаскеллем дело в том, что программа с сигнатурой Int -> Int не может ничего распечатать. Даже если мы попробуем это написать:


      factorial :: Int -> Int
      factorial n = do
        let _ = print "Some log here"
        foldl (*) 1 [1..n]
      
      main = print $ factorial 5

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


      Чтобы интерпретатор до него добрался, нам надо его как-то вернуть. Но любой способ его вернуть не меняя сигнатуру функции приведет в ошибке компиляции. Поэтому нам ничего не остается, кроме как поменять сигнатуру функции, ну и немного подправить main потому что функция factorial стала монадической, поэтому её нельзя просто так вызывать. Получаем:


      factorial :: Int -> IO Int
      factorial n = do
        _ <- print "Some log here"
        pure $ foldl (*) 1 [1..n]
      
      main = do
        result <- factorial 5
        print result

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




      На сишарпе эквивалент будет примерно такой:


      class Program
      {
          static int Factorial(int n)
          {
              var _ = new Task(() => Console.WriteLine("Some log here"))
                  // в языке нет (), поэтому приходится обходиться так
                  .ContinueWith(_ => default(object)); 
              return Enumerable.Range(1, n).Aggregate((x, y) => x * y);
          }
      
          static async Task Main(string[] args)
          {
              Console.WriteLine(Factorial(5));
          }
      }

      и для второго варианта:


      class Program
      {
          static async Task<int> Factorial(int n) // Task<T> это то же что IO T
          {
              var _ = await Task.Run(() => Console.WriteLine("Some log here"))
                  .ContinueWith(_ => default(object));
              return Enumerable.Range(1, n).Aggregate((x, y) => x * y);
          }
      
          static async Task Main(string[] args)
          {
              var result = await Factorial(5); // вместо <- в сишарпе используется await
              Console.WriteLine(result);
          }
      }


      1. im_age
        11.12.2019 04:02

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

        1. Выходит, что пометка возвращаемого результата как IO, это синт. сахар, который скажет интерпретатору найти все IO операции в функции и выполнить их?

        2. Правда тут еще возникает вопрос о порядке выполнения, т.к. при IO это может быть важно, но об этом насколько я понял заботится do.

        3. В итоге если бы мы все же хотели заменить вызов функции ее результатом, то поскольку теперь тип возвращаемого значения не int, а IO int, мы, без нарушения этого контракта, не смогли бы просто заменить его на 120, но были бы вынуждены как и интерпретатор понять, какой именно IO эффект порождается функцией и добавить его как результат работы к значению 120? (в этом объяснении есть ощущение что я что-то упускаю или очень упрощаю, но по-другому свести все вместе как-то не получалось :))


        1. mayorovp
          11.12.2019 09:17

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

          Нет, это работает в другую сторону. Любую IO-операцию можно скомбинировать с чем-то, и получить другую IO-операцию. А можно забыть. Если операция "дошла" до возврата из main — она выполнится. Забытые операции выполняться не будут.


          Правда тут еще возникает вопрос о порядке выполнения, т.к. при IO это может быть важно, но об этом насколько я понял заботится do.

          do — всего лишь синтаксический сахар для комбинирования операций. Они выполняются в том порядке, в котором комбинировались.


          А третьего вашего вопроса я не понял.


        1. PsyHaSTe Автор
          11.12.2019 11:51
          +2

          1. Пометка возвращаемого результата как IO это не сахар, а просто пометка. У каждой монады есть пара методов, которые интерпретатор выполняет когда до них доходит. В случае IO это какое-то ИО собственно, в случае стейт монады это сохранение стейта, в случае List-монады это применение действия в каждому элементу списка и т.п.
          2. нужно понимать, что как async/await разворачивается в цепочку ContinueWith, так и в do и <- всё разворачивается в такую же цепочку bind, который по смыслу то же самое делают, только называется иначе. Ну и a.ContinueWith(b) это не то же самое что b.ContinueWith(a), поэтому в do-записе порядок важен. В каком запишете в таком цепочка и будет выполнена. Именно тут (и только тут) порядок может влиять на результат, поэтому только в do-блоках нужно быть аккуратнее при перестановке местами строчек. Но т.к. вам язык помогает их найти (вы явно видите блок) — то проблем с этим нет.
          3. Мы спокойно можем поменять результат функции) Она же по сути возвращает структуру IO(Print("Some log here"), 120). Можем подставить её тело в мейн:

          main = do
            result <- IO(Print("Some log here"), 120)
            print result

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


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


          1. epishman
            11.12.2019 11:56
            +3

            Часто Ваш комментарий тянет на статью. Жаль что не могу плюсануть 20 раз, но как приятно созерцать чистый разум!


          1. mayorovp
            11.12.2019 12:33

            Разве у IO есть конструктор?


            1. PsyHaSTe Автор
              11.12.2019 12:54

              Ну в хаскелле он захачен на уровне компилятора, видимо (как-то ведь сам рантайм создает объекты этого типа?), но в тех же котах это обычный тип: https://typelevel.org/cats-effect/datatypes/io.html#pure-values--iopure--iounit


              Но учитывая что конструкторы это просто функции создающие значения, то мы сами такую можем сделать:


              makeIO action result = do
                _ <- action
                pure $ result
              
              main = makeIO (print "Some logs here") 120 >>= print


              1. mayorovp
                11.12.2019 13:01

                "В тех же котах" одновременно присутствует как монада IO, так и обычный императивный код, унаследованный ещё от Java, отсюда и возможность такого конструктора.


                Но я не могу назвать это достоинством.


                1. PsyHaSTe Автор
                  11.12.2019 13:08

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


                  Впрочем, хаскеллю 30 лет, думаю это можно списать на возраст. Все же за это время появилось лучшее понимание, что и как делать. Будем надеяться, что дотти и идрис2 будут расти и популяризироваться.


                  1. mayorovp
                    11.12.2019 13:21

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


                    1. PsyHaSTe Автор
                      11.12.2019 13:51

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


                      1. mayorovp
                        11.12.2019 14:07

                        Но это же бесконечная рекурсия, которая без "магии" со стороны компилятора никогда не разрешится:


                        fn add(self, other: $t) -> $t { self + other } 

                        Напомню, оператор + вызывает функцию add


                        1. PsyHaSTe Автор
                          11.12.2019 14:23

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


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


                          1. mayorovp
                            11.12.2019 14:24

                            Так вы и монады свои реализовать можете в Хаскеле. Зачем вам ещё и возможность внутрь IO залезть и всё испортить?


                            1. PsyHaSTe Автор
                              11.12.2019 14:27

                              Не собираюсь ничего портить, и обсуждение скатилось куда-то не туда.


                              Предлагаю на этом закончить.


          1. im_age
            11.12.2019 13:47

            Теперь гораздо понятнее, спасибо!


  1. epishman
    10.12.2019 17:54
    -2

    Принцип красивый и статья отличная, но в реальной жизни… приведенный пример с покупкой кофе просто ужасен. Мой опыт в основном со стороны ERP, и он говорит что:
    1) чем проще базовые интерфейсы, тем более высокоуровневую абстракцию на них можно построить без риска получить рак мозга;
    2) чем проще интерфейс, тем чаще и охотнее его переиспользуют.
    У нас была функция купитьКофе(), которая инкапсулировала в себе платеж. Это соответствует реальной жизни — мы говорим «совершена сделка купли-продажи», и детали платежа — наличкой там платили, картой, или вообще взаимозачетом — нас не интересуют, если сделка закрыта, значит кофе как-то был оплачен. Автор предлагает вынести кишки наружу, то есть мы получим функционально-чистый подход, но при этом существенно ослабим инкапсуляцию! Когда у вас приложение из 1000 таблиц, 200 форм и миллиона строк кода — любая протечка инкапсуляции уменьшает желание переиспользовать этот кусок кода.
    Пример отлично инкапсулированного API — это например установление TLS-соединения, которое достаточно сложное внутри, но очень простое снаружи (даже корневой сертификат мы не указываем где лежит — процедура сама ищет, и она конечно становится от этого весма «грязной»).


    1. gecube
      10.12.2019 23:34
      +2

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


      нас была функция купитьКофе(), которая инкапсулировала в себе платеж

      Мы говорим про классический ООП? Функция купитьКофе() где будет? В инстансе покупателя, кофейни или где ещё? Вообще в этом отношении smalltalk'а на вас нет. Функции — зло, обмен сообщения — добро. Странно, что не все ещё стали писать в этом стиле. Зато все красиво разделено и масшатбируемо.


      1. epishman
        11.12.2019 08:45

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


      1. epishman
        11.12.2019 09:23

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


      1. mayorovp
        11.12.2019 09:24

        Не знаю о каком классическом ООП вы говорите, но названная вами проблема решается инверсией зависимостей.


      1. vintage
        11.12.2019 10:57

        В случае фп — все просто. В случае классического ООП

        Используется ambient context с которым тоже всё элементарно.


    1. Vilaine
      11.12.2019 08:15
      +1

      Автор предлагает вынести кишки наружу, то есть мы получим функционально-чистый подход, но при этом существенно ослабим инкапсуляцию!
      «Кишки наружу» звучит как Inversion of Control, а это рекомендуемый подход и в императивном коде. Инкапсуляция касается лишь области ответственности кода, который в ООП должен работать лишь со своими данными, поддерживая инварианты логики.
      Когда у вас приложение из 1000 таблиц, 200 форм и миллиона строк кода — любая протечка инкапсуляции уменьшает желание переиспользовать этот кусок кода.
      При таких масштабах теряется контроль над сложностью и чистота с иммутабельностью занимают одно из первых мест по борьбе с ней.

      Оба моих комментария не касаются ФП вообще, замечу.


      1. epishman
        11.12.2019 08:41

        Большинство таких масштабных проектов написано в процедурном стиле, часто даже без ООП. И единственный метод борьбы со сложностью остается инкапсуляция поведения, ну и немножко полиморфизма с интроспекцией. В итоге такую систему хрен протестируешь. Я бы хотел видеть или даже войти в проект разработки ERP в функциональном стиле, но почему-то таких нет.
        PS
        Инверсия управления не противоречит инкапсуляци, насколько я понимаю.


        1. Vilaine
          11.12.2019 08:52
          +1

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


        1. akryukov
          11.12.2019 09:12
          +1

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

          Возможно Veidt и CrushBy что-то знают об таких ERP


          1. epishman
            11.12.2019 09:15

            Спасибо


          1. CrushBy
            11.12.2019 10:04

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


            1. epishman
              11.12.2019 12:00

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


              1. CrushBy
                11.12.2019 12:08

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

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

                И, как правильно указано в статье, ООП и ФП — это все таки разные вещи, напрямую не связанные друг с другом.


                1. epishman
                  11.12.2019 12:21

                  Ну да, хотя по моему это просто следствие ошибочных подходов, принятых в 90-е, когда все писали ОО-обертки над таблицами, CRUD, и прочие низкоуровневые EJB. Это не взлетело потому что медленно и трудоемко. У 1С была здравая идея абстрагироваться от таблиц, и перейти к высокоуровневым объектам, но это тоже половинчатое решение. Реально полезный высокоуровневый объект — это Клиент или Товар, или БанковскийСчет, а никак не РегистрСведений. А поскольку наследования в платформе не было и нет — все с радостью вернулись к SQL-запросам, потому что работают быстро. В Вашем случае аналогично — заворачивание таблиц в функции мало что дает, инкапсулировать нужно очень-толстые-объекты, тогда это будет выгодно. Классическая SOA это было хорошо, а вот микросервисы уже спорная идея. Я за толстые объекты и толстые функции (и их композиции, которые в случае чистоты можно кэшировать), хотя это сейчас немодно.


                  1. CrushBy
                    11.12.2019 12:47
                    +1

                    В 1С вернулись к SQL-запросам по той же причине, почему в разных проектах отказываются от ORM. Основная причина — производительность. Работать с объектами через тот же ORM, вообще не зная о реляционной базе данных, значительно удобнее. Но проблема в том, что во многих случаях это дает очень большой overhead по многим причинам.

                    Функции, например, в LINQ (и в том же lsFusion) именно «компилируются» в SQL запросы и по сути выполняются на сервере. Поэтому падение производительности — минимально. И я бы не сказал, что LINQ не взлетел. Им достаточно активно пользуются, не переходя во многих случаях к написанию SQL-запросов в лоб.


                    1. PsyHaSTe Автор
                      11.12.2019 13:05

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


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


                      А в 90% запросов SQL просто тупо не нужен, EF генерирует тот же код что руками бы был написан.


  1. prefrontalCortex
    12.12.2019 12:38
    -2

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


  1. littorio
    13.12.2019 12:36
    -1

    int Factorial(int n)
    {
        int result = 1;
        for (int i = 2; i <= n; i++)
        {
            result *= i;   // ???
        }
        return result;
    }

    Вы дали определение вида "состоит из чистых функций", так? Ну так у вас вон оператор '*=' как раз не является чистым (при одних и тех же входных данных — переменной i, результат может быть каким угодно) => вся функция не является чистой. И чем глубже вы в это залезаете, тем ближе вы к определению "не меняет состояние".


    1. mayorovp
      13.12.2019 12:45
      +1

      С фига ли функция не является чистой, если она детерминирована и у нее нет побочных эффектов?


    1. koldyr
      13.12.2019 13:39
      -1

      f :: a->State s b не чистая,
      Но runState (f x) s0 — чистая.
      Такой вот фокус.


      1. PsyHaSTe Автор
        13.12.2019 15:08

        С чего бы она не чистая? А runState на вход что принимает? Случаем не State? Который в программе нельзя создать, раз по-вашему функции создания стейта запрещены.