Функциональное программирование — это не то, что нам рассказывают

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



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


    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. Я не хочу ловить в рантайме ошибки "Метод не был замокан"/"Тип не был зарегистрирован"/"...", в конце концов я не для того выбирал статически типизированный язык.


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

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 784

      +22

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


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


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

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

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

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


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

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

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

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

              –1
              да но как это отменяет суть процесса? Происходит композиция функций. Просто передача параметров не напрямую а с упаковкой/распаковкой.
                0
                А ещё функтор и аппликативный функтор. И ещё законы там всякие должны выполняться.

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


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

                  +1

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

                    0

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

                      0

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

                        0

                        У меня в коде и не используется, очевидно же :)
                        Это просто пример был, для того чтобы понятно, о чем речь в общем. В реальном коде вместо этого везде что-то вроде 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 и преходит), и вы, будучи неосторожным, вполне можете встретиться с неожиданным поведением монады, которая этому закону не удовлетворяет

                          0
                          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
                            В Linq, кстати, тоже return не используется (если даже написать аналог return'а, то шарп не сможет вывести полиморфный аргумент).

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

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


                            SelectMany — не бинд

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

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

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


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

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

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

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

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

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


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

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

                                    0
                                    А это тогда что? 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<> быть монадой.
                                    
                                    А кто говорил, что ей что-то мешает быть монадой?
                                      0

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

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

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

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

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

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

                                              0

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


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


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

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

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


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

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

                              0

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

                                0

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

                    • НЛО прилетело и опубликовало эту надпись здесь
                        +17

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

                          +5

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

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

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

                          +12

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


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

                          +14
                          Из этой статьи я узнал, что писал в функциональном стиле на PHP4 14 лет назад.
                            +14
                            Да, и хотелось бы уточнить, что это не плевок в сторону статьи, это плевок в сторону овер9000 названий в айти и ритуального понимания их.
                              +10

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


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

                              +1

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

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

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

                                +5

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

                                  0

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

                                    +4

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



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


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

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

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

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

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

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

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

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

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

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

                                      +16

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

                                        –22

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

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

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


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

                                            –13

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

                                              +20

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

                                                –12

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

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

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

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

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

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

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

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

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


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

                                                              +12
                                                              Наркоман.
                                                                +2
                                                                А функция sendMessage (ну или getResponse) тоже должна либо стопроцентно гарантировать доставку по сети либо крашить приложение?
                                                                  –5

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

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

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

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

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

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

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

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

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

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


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

                                                                  +5

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

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

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

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

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

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

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

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

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

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

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

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

                                                                          +4

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

                                                                            –2

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

                                                                              +4

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


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

                                                                                –5

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

                                                                              0

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

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

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


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

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

                                                                        –1

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

                                                                          0

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

                                                                            0

                                                                            А если есть?

                                                                              –1

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

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

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

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

                                                                      0

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

                                                                        –1

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

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

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

                                                                              Ну и ССЗБ.

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

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


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

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

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

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


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

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

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

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

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


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

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


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

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


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

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

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

                                                                                              0
                                                                                              Компьютер штука глупая. Глупость туда — глупость оттуда.
                                                                                              Говорила моя коллега с работы)
                                                                                    0
                                                                                    Kotlin
                                                                                0

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

                                                                                  –6

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

                                                                                    +4

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


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

                                                                                      –2

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

                                                                                        +2

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


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

                                                                                          0

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


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

                                                                                            –2

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

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

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

                                                                                                  0
                                                                                                  Ну не совсем, это скорее вариант нового соединения при существующей сессии, для которого будет нужен новый ключ, который можно сгенерить заранее. Я просто к тому, что генерацию заранее каких-то данных, которые сейчас пока не используются, но будут использоваться потом — для ускорения процесса установки соединения — вполне можно себе представить.
                                                                                          +1
                                                                                          Вам нужно получить сессию, вы пишете что-нибудь типа SessionManager.get()

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

                                                                                            –2

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

                                                                                              +3

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

                                                                                                –4

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

                                                                            +1
                                                                            Не обязательно так
                                                                              –3

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

                                                                    +6
                                                                    А как записать алгоритм, по которому надо покупать кофе, пока не кончатся деньги?
                                                                    for (;;) {
                                                                        cup = new Coffee();
                                                                        if (card.Balance < cup.Price) break;
                                                                        resultList.Add(cup);
                                                                        card.Charge(cup.Price);
                                                                    }

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

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


                                                                      Ну а так да, хороший пример когда 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;
                                                                      }
                                                                        0

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


                                                                        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));
                                                                        }
                                                                          +3
                                                                          Я так понимаю, 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
                                                                            Я так понимаю, 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
                                                                              Да нет, совершенно нормально создать его в функции. Это же просто начальный элемент. Как единица для факториала, вам не надо передавать её снаружи
                                                                              Так откуда мы узнаем, сколько денег с карты уже списано?
                                                                              Если я вызову 10 раз ф-цию BuyCoffeesForAllMoney(Card), то она 10 раз и купит одно и то же, я же ожидаю от второго и последующих вызовов, что денег на карте больше нет и ничего купить нельзя.
                                                                                +1
                                                                                Так откуда мы узнаем, сколько денег с карты уже списано?

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


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


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

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


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

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

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

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

                                                                                      +2

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

                                                                                        +3

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

                                                                                          +1

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


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

                                                                                            +1

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

                                                                                +2

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

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

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

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

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

                                                                                  +3

                                                                                  Ну тут уже придется расчехлять монады (в сишарпе монадический 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), который ничего не делает пока мы его не запустим и не получим результат.

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

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

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


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


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

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

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

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

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


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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

                                                                                                          0
                                                                                                          Да, точно…
                                                                                                          0
                                                                                                          1. yield и await — одно и то же. async function и generator function — тоже. Вся разниа в том, то async/await передаёт между функциями промисы, а генераторы могут передавать что угодно.
                                                                                                          2. Не привязан он ни к чему. Вы так же можете возвращать типизированный промис, а внешняя функция будет вас вызывать, когда он отрезолвится.
                                                                                                          3. Может.
                                                                                                            0
                                                                                                            3. Может
                                                                                                            Как это выглядит синтаксически?
                                                                                                                +2
                                                                                                                Ну это не встроенная в язык конструкция, как yield в c#. Своих велосипедов можно наделать сколько угодно, назвать их как угодно. Например, сделать Singleton.GetInstance, каждый раз возвращающий новый объект и на основе этого утверждать, что Singleton так и работает.
                                                                                                                  0

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

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

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

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


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

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

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

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

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


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

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

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

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

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

                                                                                                                                А что мешает?


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

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

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

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

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


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

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

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

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

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

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


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

                                                                                                                                          0
                                                                                                                                          Ну так из этого следует, что
                                                                                                                                          return dummy.next(maxValue)
                                                                                                                                          возвращает значение не для переданного здесь maxValue, а для предыдущего?
                                                                                                                                            0
                                                                                                                                            Поигрался с этим в браузере.
                                                                                                                                            Получается, что параметры 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>
                                                                                                                                              0
                                                                                                                                              Пока не будет сделан вызов next — yield не вернет управления.
                                                                                                                                                0
                                                                                                                                                Это понятно.
                                                                                                                                                Неочевидно было, что вызов next(5), который вернул 6 из let x = yield 6;
                                                                                                                                                это самое значение 5 не пишет в переменную x, а только параметр следующего next будет записан в x.
                                                                                                                                                  0

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

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

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

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

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

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

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

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