Монады как паттерн переиспользования кода


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


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


    Но ведь в интернете буквально сотни статей про ФП и монады, зачем писать еще одну?


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


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


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


    Вступление


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


    Я долго думал на каком языке писать примеры, перебрал все варианты, которые знал. В итоге остановился на модифицированном C#. Scala оказалась слишком вербозной, Rust хотя и имеет концепцию трейтов, не может выразить самый простой из требуемых тайпклассов, ну а Haskell знают не все.


    Но обычный сишарп не обладает нужными фичами, поэтому в статье я буду использовать синтаксис C# 10 (который еще не вышел), в частности расширение Shapes и расширение HKT. Первый из них добавляет в язык шейпы (aka тайпклассы, aka трейты). Если привести пример зачем они нужны, то вот так мы могли бы объявить тайпкласс для того, чтобы помечать классы как сериализуемые


    public shape JsonDeserialize<T>
    {
        static T Deserialize(JObject input);
    }

    Такой тайпкласс превратил бы рантайм эксепшн JsonSerializationException: Could not create an instance в ошибку времени компиляции. Лично я с этой ошибкой часто встречаюсь на проектах с десериализацией нетривиальных типов в кастомных форматах, поэтому и пример про него.


    Шейпы отличаются от интерфейсов двумя особенностями: во-первых, они позволяют объявлять статические методы (и даже константы), а не только инстансные, а во-вторых, позволяют расширять чужие классы. Например, ICollection<T> не наследует IReadOnlyCollection<T>, и мы ничего с этим не можем поделать. Будь они тайпклассами, мы легко могли бы зачинить эту проблему. Или мы можем расширить функциональность стандартных классов. Если вы когда-нибудь хотели написать генерик-функцию вида where T : Number, работающую с любыми числами, то вы сразу должны оценить, какая это нужная штука: с тайпклассами объявить такой Number не составляет никаких проблем.


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


    public static T<int> CreateSomethingOfInts<T>() where T : <>, new() 
    {
        return new T<int>();
    }

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


    // можем создать инстанс любого типа с одним генерик параметром
    
    var list = CreateSomethingOfInts<List>();
    var hashSet = CreateSomethingOfInts<HashSet>();
    // …
    // Array<T>, Nullable<T>, LinkedList<T>, ... - можно использовать любой подобный тип
    // получим в результате Array<int>, Nullable<int>, LinkedList<int>, ...

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


    Что ж, прелюдия довольно ощутимо затянулась, приступим.


    Functor


    И первое с чего мы начнем — с функтора. "Как же так, ты же про монады рассказать обещал!" — скажете вы. Да, но функтор — базовый строительный блок многих ФП понятий, в том числе и монады, поэтому без него не обойтись.


    Итак, что такое функтор? Можно долго рассуждать в терминах объектов категорий и морфизмов между ними, а можно взять наш понятийный аппарат ООП разработчиков и сказать, что Функтор — это любой объект, реализизующий тайпкласс Functor следующего вида:


    public shape Functor<T> where T : <>
    {
        static T<B> Map<A, B>(T<A> source, Func<A, B> mapFunc);
    }

    Собственно, это всё. Это полное определение, прочитав которое вы можете смело сказать "я знаю, что такое функтор". Если бы терминологию придумали джависты, то они назвали бы его Mappable, потому что он, собственно, определяет единственный метод Map, который позволяет преобразовать наш тип-контейнер, параметризованный типом A, в такой же контейнер, но уже с элементами типа B.


    Например, был у нас массив чисел и функция ToString, получили массив строк. Или был список строк, а получили список длин этих строк. А может и не список, и не массив, а стек какой-нибудь. Суть одна — у нас есть наша структура данных, в которой лежат какие-то объекты. У нас есть функция A => B, которая преобразует один такой объект в другой такой объект. Тогда с использованием функции Map мы можем сделать такой же контейнер, как тот, что хранит A, но теперь в нём будут B.


    Для Map сущестует единственное правило: если наша mapFunc это Identity-функция вида x => x, то контейнер должен остаться неизменным. То есть, чтобы считаться "законным" функтором, для нашего контейнера всегда должно выполняться вот это равенство:


    Map(something, x => x) === something

    Это правило достаточно очевидное, оно, по сути, говорит, что сам контейнер по себе ничего со значением не делает, и всё взаимодействие с его элементами мы можем контролировать при помощи mapFunc. Там нет никаких рандомов, внешних взаимодействий, и так далее, мы можем безопасно вызывать Map как угодно.


    Давайте подумаем какие типы из стандартной библиотеки удовлетворяют этому правилу?
    Ну, самое простое, это итераторы:


    public extension EnumerableFunctor of IEnumerable : Functor<IEnumerable>
    {
        public static IEnumerable<B> Map<A, B>(IEnumerable<A> source, Func<A, B> map) =>
            source.Select(map);
    }
    
    // проверяем закон функторов
    var range = Enumerable.Range(1, 10);
    Console.WriteLine(Map(range, x => x).SequenceEquals(x)) // выведет True

    Раз этот код компилируется и тест проходит, то мы доказали, что итератор в дотнете является функтором! Хотя в дотнете нет тайпклассов, тем не менее IEnumerable — это функтор, раз закон выполняется


    Какой еще тип может вести себя подобным образом? Подумайте немного, вы с ним работаете каждый день по 100 раз на дню.


    Скрытый текст

    И конечно же это Nullable. Давайте реализуем для него тайпкласс функтора:


    public extension NullableFunctor of Nullable : Functor<Nullable>
    {
        public static B? Map<A, B>(A? source, Func<A, B> map) =>
            source is A notNullSource ? map(notNullSource) : default(B?);
    }
    
    // проверяем закон функторов
    int? nullableTen = 10;
    int? nullableNull = null;
    Console.WriteLine(Map(nullableTen, x => x) == nullableTen); // выведет True
    Console.WriteLine(Map(nullableNull, x => x) == nullableNull); // выведет True

    Ссылка на плейграунд


    Таким образом мы доказали, что Nullable — это тоже функтор.


    Другой очевидный ответ — Task, для него нетрудно реализовать Map самостоятельно.




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



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


    Вот мы и познакомились с одним из страшнейших зверей мира ФП — целым функтором! А дальше нас ждет ещё более сложный тайпкласс и зовут его...


    Applicative


    Аппликативный функтор! Который определяет не один метод, а целых два:


    public shape Applicative<T> where T : <>
    {
        static T<A> Pure<A>(A a);
        static T<C> LiftA2<A, B, C>(T<A> ta, T<B> tb, Func<A,B,C> map2);
    }

    Что мы тут видим? Аппликативный функтор, это любой тип, который умеет:


    1. Создаваться из одного значения любого типа. По сути аналогичен констрейнту where T : new(), за исключением того, что new() не принимает аргументов, а мы принимаем один.
    2. А тут уже интересно. Интерфейс говорит нам, что если у нас есть два значения T<A> и T<B> и функция, преобразующая пару значений A, B в C, то мы можем получить T<C>. Название LiftA2 происходит из того, что мы как бы "поднимаем" вычисление над двумя голыми переменными A и B в вычисление над аппликативами T<A> и T<B> соответственно.

    Непонятно? Давайте разбираться. Самый простой способ разобраться в чем-то – сделать это что-то своими руками. Класс называется аппликативный функтор, в предыдущем разделе мы как раз пару функторов разобрали, возможно, они как-то связаны?


    "Talk is cheap, show me the code", поэтому в качестве доказательства что наш класс является аппликативом мы, как и раньше, постараемся просто реализовать соответствующий интерфейс. Если компилятор нас не остановит — то значит мы успешно доказали то, что хотели, если же у нас в какой-то момент возникнут трудности — значит мы не правы. Давайте начнем с итератора:


    public extension EnumerableApplicative of IEnumerable : Applicative<IEnumerable>
    {
        static IEnumerable<A> Pure<A>(A a) => new[] { a };
        static IEnumerable<C> LiftA2<A, B, C>(IEnumerable<A> ta, 
                                              IEnumerable<B> tb, 
                                              Func<A, B, C> map2) =>
                ta.SelectMany(a => tb.Select(b => map2(a, b)));
    }

    Ну, вроде у нас всё получилось. Что интересно — всё это дело прекрасно компилируется восьмым сишарпом, если закомментировать часть про public extension. То есть в обычном кроваво-энтерпрайзном языке есть давно все эти прелести, просто они не оформлены в виде тайпкласса, от которого можно абстрагироваться (зачем это вообще может понадобиться я покажу ниже).


    Что же насчёт Nullable? Тоже никаких проблем:


    public extension NullableApplicative of IEnumerable : Applicative<Nullable>
    {
        static A? Pure<A>(A a) => a;
        static C? LiftA2<A, B, C>(A? ta, B? tb, Func<A, B, C> map2) =>
            (ta, tb) switch {
                (A a, B b) => map2(a, b), // Если оба не null - то вычисляем
                _ => default(C?)          // кто-то нулл - результат null
            };
    }

    В примере с итератором выше мы релизовали семантику "каждый с каждым", но мы все прекрасно знаем, что есть другая равнозначная семантика "первый с первым, второй со вторым, ...". К сожалению, реализовать один и тот же интерфейс для одного типа двумя различными способами нельзя, поэтому нам подойдет паттерн Адаптер, который в ФП мире называют ньютайп. Для итератора таким адаптером является класс ZipList:


    public class ZipList<T> : IEnumerable<T>
    {
        private IEnumerable<T> _inner;
    
        // .. конструктор и реализация IEnumerable<T> опущена для краткости
    }
    
    public extension ZipListApplicative of ZipList : Applicative<IEnumerable>
    {
        static IEnumerable<A> Pure<A>(A a) =>
            // вообще тут должен быть бесконечный генератор элемента 'a'
            Enumerable.Repeat(a, int.MaxValue); 
        static IEnumerable<C> LiftA2<A, B, C>(IEnumerable<A> ta, 
                                              IEnumerable<B> tb, 
                                              Func<A, B, C> map2) =>
            ta.Zip(tb, map2);
    }

    Оказывается, ZipList давно существует в стандартной поставке, просто очень хорошо скрывается. Но без общего зонтичного типа Applicative он не особо полезен, поэтому дотнет обходится просто одинокой функцией Zip.


    Мы узнали, что такое аппликативы, а какие-нибудь примеры использования будут? Что мы можем с ними сделать? Ну, с ними можно много чего интересного делать — библиотеку парсер-комбинаторов с их помощью удобно выражать, проперти-тест фреймворк можно написать, но для статьи такие примеры слишком большие, поэтому давайте возьмем чего-нибудь попроще. Например, можно написать функцию, которая из пары аппликативных функторов нам сделает аппликатив пары оригинальных значений, то есть: (F<A>, F<B>) -> F<(A, B)>. Давайте напишем:


    public static T<(A, B)> Combine(T<A> ta, T<B> tb) =>
        LiftA2(ta, tb, (a, b) => (a, b));
    
    var eta = Enumerable.Range(3, 2);
    var etb = Enumerable.Range(15, 4);
    
    int? nta = 10;
    int? ntb = null;
    
    Combine(eta, etb) // [(3, 15), (3, 16), (3, 17), (3, 18), (4, 15), (4, 16), (4, 17), (4, 18)]
    Combine(nta, nta) // (10, 10)
    Combine(nta, ntb) // Null
    Combine(new ZipList<int>(eta), new ZipList<int>(etb)) //  [(3, 15), (4, 16)]

    Ссылка на плейграунд


    С одной стороны, функция простая, можно даже сказать скучная. А с другой — посмотрите, мы написали очень абстрактную функцию Combine, которая совершенно ничего не знает о переданных значениях, но при этом умеет производить очень сильно различающиеся действия. Для двух списков она считает комбинаторику всех пар, для нуллейблов оно возвращает либо пару элементов, если оба переданных параметра имели значение, либо null. Для ZipList мы сцепили соответствующие элементы двух списков, причем результирующий список был усечен до самого короткого из двух. Таким образом, аппликатив позволяет нам разделить действие над элементами контейнера (это наша функция (a, b) => (a, b)) и форму контейнера (это T<>). То есть, с одной стороны, мы можем описывать вычисления, не заботясь о форме контейнера (опциональное значение/список/промис/что угодно), а с другой мы, наоборот, можем реализовать некий контейнер, а варианты работы с этим контейнером оставить на откуп клиентскому коду.


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



    pure позволяет нам создать контейнер, содержащее единственное значение



    liftA2 позволяет нам использовать функцию от двух аргументов, имея на руках
    два контейнера с соответствующими типами, упакованными внутри


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


    А почему функтор? Имея функции LiftA2 и Pure легко реализовать Map:


    static T<B> MapAnyFunctor<T, A, B>(T<A> source, Func<A, B> map) where T : Applicative =>
        LiftA2(source, Pure(0), (a, _) => map(a));

    Как это работает? Очень просто — мы создаем мусорное значение и с помощью функции Pure оборачиваем его в аппликатив T<>. Теперь у нас есть T<A> и T<НашМусорныйТип>, которые по типам подходят для LiftA2. Нам остаётся только её вызвать, а в передаваемом коллбеке игнорировать это мусорное значение, вызывая map для элементов настоящего контейнера T<A>. Написав эту функцию мы доказали, что любой аппликатив является функтором. Можно дополнить нашу изначальную сигнатуру:


    public shape Applicative<T> : Functor<T> where T : <>
    {
        static T<A> Pure<A>(A a);
        static T<C> LiftA2<A, B, C>(T<A> ta, T<B> tb, Func<A,B,C> map2);
    }

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


    Бонус

    Также легко реализовать тайплкасс аппликатива для Task<T>:


    public extension TaskApplicative of Task: Applicative<Task>
    {
        public static Task<A> Pure<A>(A a) => Task.FromResult(a);
    
        public static async Task<C> LiftA2<A, B, C>(Task<A> ta, Task<B> tb, Func<A, B, C> map2)
        {
            await Task.WhenAll(ta, tb);
            return map2(ta.Result, tb.Result);
        }
    }

    Ссылка на плейграунд


    Monad


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


    public shape Monad<T> where T : <>
    {
        static T<A> Pure<A>(A a);
        static T<B> Bind<A, B>(T<A> ta, Func<A, T<B>> mapInner);
    }

    Если говорить по-русски, то:


    Монада — это любой класс с функциями Pure и Bind, которая принимает аргумент типа T<A> и функцию, преобразующую распакованное значение A в T<B>, и возвращает значение того же типа T<B>

    И никаких моноидов в категориях эндофункторов, заметьте. Сигнатура может выглядит немного перегруженной, но она иллюстрирует простую идею: у вас есть упакованное в контейнер значение типа А. И у вас есть функция из голого A в такой же контейнер, но уже со значением B. Функция Bind позволяет "связать" эти два выражения вместе, получив из пары (T<A>, A => T<B>) значение T<B>.


    Таким образом монада — это простейший интерфейс, который тривиально реализовать для того же итератора, что мы в очередной раз и сделаем:


    public extension EnumerableMonad of IEnumerable : Monad<IEnumerable>
    {
        static IEnumerable<B> Bind<A, B>(IEnumerable<A> ta, Func<A, IEnumerable<B>> mapInner) =>
            ta.SelectMany(mapInner);
        // Pure такой же как и в аппликативе
    }

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



    Монады позволяют имея на руках контейнер с элементами типа А и функцией из А в такой же контейнер типа В получить контейнер типа В


    Любая монада также является аппликативом, поэтому реализацию Pure копипастить не надо: её можно отнаследовать от базового аппликатива. Что до LiftA2, то в качестве упражнения предлагаю реализовать её при помощи функций Pure и Bind, там нет ничего сложного.


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


    let foo = do
      a <- someA
      b <- someB
      pure (doSomethingWith a b)

    в последовательность вызовов функции Bind которую мы только что разобрали:


    var foo = Bind(someA, a => Bind(SomeB, b => Pure(DoSomethingWIth(a,b)))

    Паттерн очень простой. Всё что справа от стрелочки <- идет как первый аргумент функции Bind,
    то, что слева — становится именем параметра лямбды, которая передаётся как второй аргумент.
    Элементарно.


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


    А что у нас в сишарпе? А в нём есть аж целых три его захардкоженных вариации. Например, что это за код?


    var values = from x in new []{ 1, 2, 3 }
                 from y in new []{ 4, 5, 6 }
                 select x + y;

    Это ни что иное, как do-нотация для монады итератора (в хаскелле итератор называется списком):


    let values = do 
        x <- [1,2,3]
        y <- [4,5,6]
        pure (x + y)

    Давайте теперь посмотрим на такой код:


    var maybeA = GetMaybeA();
    var maybeB = maybeA?.GetB();
    var maybeC = maybeB?.GetC();
    var result = maybeC;

    А это do-нотация для монады Maybe (она же Option, она же с некоторой натяжкой — Nullable):


    let result = do
        maybeA <- getMaybeA
        maybeB <- getB maybeA
        maybeC <- getC maybeB
        pure maybeC

    Что насчет вот такого кода?


    var valueA = await GetSomeA();
    var valueB = await GetSomeB(valueA);
    var result = valueB;

    А это do-нотация для монады IO (про которую мы не говорили, но, по сути, это просто аналог Task из сишарпа):


    let result = do
        valueA <- getSomeA
        valueB <- getSomeB valueA
        pure valueB

    Таким образом у нас в языке образовалось сразу три различных синтаксиса для того,
    чтобы делать абсолютно одно и то же: имея на руках объект типа T<А> и функцию
    из A в T<B>, получить T<B>, будь то A[] и A -> B[], или A? и A -> B?,
    или Task<A> и A -> Task<B>,… И это далеко не полный перечень.


    На этой ноте предлагаю перейти к первому пункту обещанного параграфа под названием...


    Зачем нам монады


    Упрощение синтаксиса языка


    Первым пунктом, следующим из предыдущего абзаца, стоит выделить упрощение языка. Посмотрите, сколько мусора натащил сишарп, чтобы выразить простую идею "Сделай что-нибудь, а затем сделай что-нибудь еще". И асинк-авейт, и LINQ, и null propagation являются частными случаями общей идеи. Причем которые очень часто ломаются на ровном месте. Захотел вызвать статический метод на nullable-параметре? Всё, элвис-оператор использовать не получится, пиши, как в старые-добрые времена проверку на нулл. Захотел заавейтиться внутри лямбды? Тебе компилятор скажет всё, что он думает об этой затее. Ну, хоть в случае списка, ломаться особо нечему, за исключением уродливых скобочек если нужно сделать хоть что-то выходящее за рамки LINQ-синтаксиса (например, вызвать First в конце запроса).


    А еще большая проблема различного синтаксиса в том, что это всего лишь сахар: ту же функцию MapAnyFunctor написать в текущем сишарпе не выйдет. Мы годами ждали фичу async enumerable, которую наконец-таки релизнули (как всегда с кучкой костылей, все ведь например уже в курсе магических атрибутов для CancallationToken?), но сколько лет мы её ждали? Сколько человеко-лет понадобилось, чтобы её реализовать?


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


    Если вы думаете, что сишарп в этом плане выделяется, то спешу вас обрадовать: это не так. Тот же Rust с удовольствием прошелся по тем же граблям, и продолжает идти. Сюда относятся: и try-блоки, и Try-трейт, и всё тот же элвис-оператор, а асинк-авейт. Уверен, в будущем кортима раста будет еще годами запиливать async enumerable, как это сделала команда сишарпа, тратя кучу ресурсов на проблему, которой изначально не должно было быть.


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


    И главное: при этом она базируется на интерфейсах, а не на захардкоженных в компиляторе эвристиках преобразования кода в стейт-машины. На интерфейсах, которые позволяет разработчику не ждать годами, пока команда языка соблаговолит наконец реализовать комбинатор пары монад, и которые не требуют костылить в язык кучу хаков. Что насчёт асинк энумерейбла, который автоматически параллелит получение данных по сети (мы не обсуждали, но в хаскелле для параллелизации есть монада Par)? Ну, пока ничего, ждем C# 15, в котором, возможно, это появится. А может и не появится.


    Упрощение стандартной библиотеки языка


    В сишарпе есть некоторое количество функций, работающих по принципу "сделай что-то, а потом еще что-то". Как мы уже выяснили, все функции такого вида отлично ложатся на монадический Bind. Это и ContinueWith, и SelectMany, и некоторые другие. Но если сишарпе их хотя бы не так уж и много, то в Rust это выглядит совершенно вопиюще. В Option/Result/Future накокопипащены буквально десятки функций, делающих ровно одно и то же, и которые могли бы быть выражены в терминах общего тайпкласса: большая часть операций через Monad, некоторые потребовали бы более редких вроде MonadFail/Bifunctor/..., но общий смысл остается тем же.


    А по факту что мы имеем? Абсолютно ужасную копи-пасту в стандартной библиотеке. Вот в версии 1.29 появляется flatten для итератора, а вот спустя более чем год он же, но для опшна. Для футур он живет в стороннем крейте, который надо подключать.


    Вот год назад появился transpose для Option/Result друг в друга, при том, что transpose из итератора для них появился аж в версии 0.8 в 2013 году. transpose для футур (которые как мы помним реализация IO монады для раста) до сих пор нет, еще 7 лет подождем, и они появятся.


    Продолжать можно ещё долго, но суть остается прежней: можно было реализовать Monad трейт один раз, и дальше эти transpose/flatten/... появлялись бы во всех совместимых типах автоматически. Да, для конкретных классов реализация по-умолчанию может быть не оптимальной, но ведь всегда можно выполнить специализацию, особенно в стандартной библиотеке. В итоге имеется огромная проблема, которой в языке от 2015 года вообще не должно было быть изначально. Но, монад нет, и починить это в текущей версии языка невозможно, остается только копипастить однотипные реализации из типа в тип.


    Сишарп тут в абсолютно схожей ситуации. Посмотрите на вот этот пакет System.Linq.Async. Разработчики из майкрософта в нём занимаются буквально тем, что копипастят реализацию LINQ из corefx, расставляя где надо async-await. Ну, там всё немного сложнее, но суть та же. Это еще один пример библиотеки, которой никогда не должно было существовать. Люди пишут руками код, который компиляторы давным-давно научились генерировать. Уже в первых версиях хаскелля были комбинаторы filterM/mapM/whateverM, которые как вы можете догадаться по их названию позволяют сделать фильтрацию/маппинг/… коллекции, производя при этом монадический эффект (отсюда буковка M в конце), в случае обсуждаемой библиотеки этим эффектом был бы асинхронный запрос.


    Однако, не стандартной библиотекой единой живы, и наш следующий пункт


    Сторонние библиотеки


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


    Но в тех же комментариях один из хабровчан дал замечательный ответ, который объясняет, что вовсе не случайно в хаскеле такие библиотеки есть, а в Go нет. Именно возможность спрятать конкретные реализации за тайпклассами Functor/Applicative/Monad/… и позволяет таким библиотекам существовать. Нет тайпклассов и HKT — не будет крутых библиотек, зато нужны будут разработчики, которые на зарплате будут копипастить реализации для новых конкретных монад, буде условному майкрософту или мозилле вздумается добавить их в язык. И, в отличие от общего решения, они будут это делать для тех кейсов, которые сочтут достойными. Если ваша монада не такая популярная, как список или опциональное значение, то останетесь без удобного способа смоделировать предметную область.


    Для опытных шарпистов, кстати, это вовсе не новость. Например, есть куча полезных библиотек, на базе IEnumerable. Не будь такого интерфейса — не было бы и их. Куча удобных ORM в сишарпе основанна на IQueryable, который является такой специализированной монадой списка для БД, и без которого я думаю ситуация с ORM в сишарпе была бы куда печальнее. Именно подобные абстракции дают возможность творить по-настоящему мощные библитеки, и если даже на единственной монаде списка мы можем делать такое, то чего мы можем достичь с их совокупной мощью? А если мы еще и комбинировать их будем?


    И именно благодаря им появляется возможность вместо сотен строчек кода написать десяток, который просто склеивает уже существующий библиотечный код. И это не обязательно код от высоколобых математиков из стандартной библиотеки Haskell, это может быть и ваш собственный My.Big.Corporation.Utils, который решает вашу конкретную практическую проблему, но решение которой чуть сложнее чем "отнаследовались от пары базовых классов и порядок". И дело не в том, что задача такая простая, что её может решить даже библиотечный код, а библиотечный код настолько абстрактный, что без проблем сможет помочь вам в вашей сложнейшей бизнес-логике.


    Заметка об абстрактности

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


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



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


    Выразительность


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


    public M<Comment> GetArticleComment<M>(int articleId) 
        where M : MonadWriter<LogMessage[]>, MonadReader<Config>, MonadHttp<AllowedSite>

    Где MonadWriter/MonadReader/MonadHttp — это те самые сегрегированные по принципу SOLID интерфейсы, каждый из которых отвечает за свой маленький аспект. То есть наша функция говорит о том, что ей нужно уметь писать логи (при этом только в формате LogMessage!), читать конфиг (но только Config!) и ходить по Http (но только на AllowedSite!), и используя всё это она в качестве результата вернет комментарий.


    Возможно, это выглядит немного чужеродно, но концепт на самом деле очень простой. Мы делаем это сотни раз, когда после авейта возвращаем значение, а оно завернуто в Task. Мы пишем return 10, тогда как возвращаемое значение Task<int>.


    Тут ровно та же история, только вместо Task может быть любая монада M, а соотвественно действием — любой эффект, а не только асинхронный запрос.


    Причем, таким образом с монадами мы одновременно следуем и последней букве SOLID, решая одну из самых больших головных болей в ООП разработке — инверсию зависимостей. Нам не нужны гигантские Autofac/Windsor/Ninject/… которые падают в рантайме "нишмагла найти зависимость", вы просто описываете в обычных where условиях нужный функционал, и если вы забыли передать зависимость, то компилятор вам об этом напомнит. Вам не нужна магия, внешняя по отношению к языку, вы просто пишете на сишарпе, а компилятор вам поможет.


    Тестируемость


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


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


    class MyService 
    {
        async Task<Comment> SomeBusinessLogicAsync(int commentId) {
            var comment = await this.remoteClient.GetAsync($"some/url/{commentId}");
            // .. do stuff ..
            return await DoOtherStuffAsync(comment);
        }
    }

    Теперь мы хотим этот код протестировать. Что мы обычно делаем в C# в таком случае?
    Ну, хорошим стилем в сишарпе считается делать тестируемые типы, поэтому наш MyService принимает remoteClient в виде аргумента конструктора, который мы и будем мокать.


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


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


    public class Id<T>
    {
        public T Value { get; }
    
        public Id(T value)
        {
            Value = value
        }
    }
    
    public extension IdMonad of Id : Monad<Id>
    {
        static IdMonad<A> Pure<A>(A a) => new Id<A>(a); // просто создаем обертку
        static IdMonad<B> Bind<A, B>(IdMonad<A> ta, Func<A, IdMonad<B>> mapInner)  =>
            mapInner(ta.Value); // просто вызываем функцию над обернутым значением
        // реализации map и liftA2 возмем по-умолчанию
    }

    Теперь вместо функции:


    Task<Comment> SomeBusinessLogicAsync(int commentId);

    Давайте напишем


    M<Comment> SomeBusinessLogicAsync<M>(int commentId) where M : Monad =>
        this.remoteClient.Get($"some/url/{commentId}").Bind(comment => 
            // .. do stuff ..
            return DoOtherStuff(comment);
        );

    С do-нотацией было бы вообще 1к1, но и так сойдет. Соответственно в нашем бизнесовом коде будет:


    var comment = await myService.SomeBusinessLogicAsync<Task>(547);

    А в коде с тестами:


    var comment = myService.SomeBusinessLogicAsync<Id>(547).Value;

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


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


    Более того, в качестве монады M может быть любая, а не только Task или Id, поэтому мы автоматически получаем функцию, которая, например, автоматически умеет в ретраи (если в качестве M мы передали такую монаду которая умеет их делать), может ничего не вернуть (если M — Option), может завершиться с ошибкой (если M — Result), и так далее. И что самое главное — да нам это тут вообще не важно. Мы хотим просто написать функцию получения комментария, а будет ли он качаться синхронно, асинхронно, с ретраям или без — это инфраструктурная хрень, и желательно чтобы она конфигурировалась снаружи. Поэтому: логика — отдельно, инфраструктура — отдельно.


    Заключение



    Если честно, я даже не думал, что получится так много текста. Первоначально я планировал рассказать и про Traversable, и Foldable, и как они помогли решить ту задачу с деревьями, но сейчас я понимаю, что уже полностью исчерпал лимит внимательности у вас, как читателей.


    Давайте подытожим, какие в итоге существуют основные тайпклассы и что они умеют:


    • Функтор (ооп. Mappable)


      Что такое: это любой класс, реализующий функцию Map определенной сигнатуры, для которой выполняется одно простое правило.


      Назначение: Класс позволяет заниматься маппингом значения внутри контейнера, преобразуя T<A> в T<B>.


      Пример: преобразование итератора одних значений в итератор других значений;
      преобразование результата асинхронной операции


    • Аппликативный функтор (Аппликатив, ооп. PairMappable)


      Что такое: это любой класс, реализующий пару функций Pure и LiftA2, для которых выполняются
      их простые правила (в основном, связанные с композицией). Реализация этих методов
      гарантирует автоматическую реализацию тайпкласса "Функтор".


      Назначение: Класс позволяет комбинировать вместе пару независимых вычислений T<A> и T<B> в общий T<C>.


      Пример: сцепление двух контейнеров (например, List, Option, ZipList, ..);
      парсинг языка с контекстно-независимой грамматикой


    • Монада (ооп. NestedJoinMappable):


      Что такое: это любой класс, реализующий пару функций Pure и Bind (и опять правила).
      Реализация этих методов гарантирует автоматическую реализацию тайпкласса "Аппликатив".


      Назначение: Класс позволяет комбинировать зависимые вычисления, где T<B> зависит от A, который
      в свою очередь находится в контейнере того же типа T<A>


      Пример: выполнение нескольких асинхронных операций, зависящих друг от друга; парсинг языка с контекстно-зависимой грамматикой



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


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

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


    Монады — просто инструмент, которым надо уметь пользоваться. Его изучение — это удачная инвестиция, которая сэкономит не один месяц жизни, уничтожив причину многих вопросов "ну КАКОГО хрена оно не работает, я же всё проверил" в зародыше. И хотя ни одна техника программирования не убережет вас от всех проблем, грех не воспользоваться инструментом, который решает значительную их часть.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 255

      0

      Добавлю, что если определить операцию >=> (называется "композиция Клейсли" (Kleisli composition)) таким образом:


      (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
      (f >=> g) = \x -> f x >>= g

      Можно тогда определить операции bind (который >>=) и join через эту операцию >=>, так что определение монады можно эквивалентно делать через разные операции, они всё равно друг через друга выражаются.


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


      1. f >=> return ≡ f
      2. return >=> g ≡ g
      3. (f >=> g) >=> h ≡ f >=> (g >=> h)

      То есть return оказывается правой и левой единицей в такой структуре, и операция >=> обладает ассоциативностью.
      Такие структуры называют страшным словом моноид, но на самом деле ничего страшного нет, такими свойствами обладают множество вещей: числа относительно сложения, числа относительно умножения, строки относительно конкатенации, множества относительно объединения и так далее.

        +10

        Не уверен, что этот пример упрощает понимание) Я специально придерживался C# где можно чтобы не пугать людей. 2 года назад у меня сигнатура и определение >=> вызвали бы паническую атаку.

          +10

          Для людей, незнакомых с хаскеллем, переведу на сишарп:


          Func<A, T<C>> FishOperator<T>(Func<A, T<B>> f, Func<B, T<C>> g)
          {
              return x => Bind(f(x), g);
          }

          И соответственно законы:


          1. FishOperator(f, Pure) = f
          2. FishOperator(Pure, g) = g
          3. FishOperator(FishOperator(f, g), h) = FishOperator(f, FishOperator(f, g))

          return это алиас на pure, который задеприкейтили потому что он похож на return в императивных языках, но по сути ощутимо отличается.

            +1

            Последний закон должен быть
            FishOperator(FishOperator(f, g), h) = FishOperator(f, FishOperator(g, h))
            очепятался немного

          0
          .
            0
            Такое описание с использованием стрелки Клейсли на мой взгляд наглядно раскрывает сущность монад: собственно это есть один из вариантов реализации композиции функциональности с помощью типов. Всё.

            А чем больше пытаются объяснить разными способами, что такое монады, тем менее понятно для непосвященных это становится. Хотя достаточно просто задать вопрос: какие способы композиции функциональности (сиречь кода, или функций) вы знаете? Я лично знаю три:
            1) естественный — операция композиции (.)
            f :: b -> c
            g :: a -> b
            (f . g) = \ x -> f (g x)
            
            Все языки натуральным образом его поддерживают без необходимости вводить дополнительные реализующие его операторы.

            2) с помощью типов, просто надо вспомнить что код — это тоже обычный тип данных и часть функциональности можно спокойно структурировать/вынести/обернуть в единообразный тип T результата функции) — операция композиции (>=>)
            f :: a -> T b
            g :: b -> T c
            (f >=> g) = \ x -> f x >>= g
            
            Операция bind (>>=) здесь вводится как функция внутренней реализации композиции.

            3) сопрограммы (но это не точно).
              +2
              А не присоветуете хороший текст, который раскрыл бы вот какую тему: связь между теорией категорий и ее применением в коде? Ну т.е., вот мы имеем числа, и сложение. И они обладают определенными свойствами. И тоже самое в коде, в виде реализации моноида. Какие конкретно выводы мы можем сделать применительно к коду из того, что доказали наличие определенных свойств у чисел?

              Я, честно говоря, надеялся что автор тут это раскроет, но как-то не сложилось. То есть, получился подход к теме еще с одной стороны (помимо чистой теории категорий и картинок с коробочками), но вот объединения всех взглядов на предмет опять в одном тексте как-то не получилось.
                +1

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


                Теоркат — скорее источник вдохновения, нежели вещь которая находит непосредственное применение в коде.


                Реализация моноида — это просто способ сделать некоторые вычисления более удобными, например выразить операции суммы, произведения, all, any, и так далее. Я через моноид например делал многопоточный подсчёт количества слов в тексте: разбиваем текст на какое-то количество чанков, независимо считаем в каждом сколько слов. В итоге получаем множество объектов вида (кусок предыдущего слова, количество слов, начало следующего слова). Склеивая их друг с другом, подсчитываем слова, профит. Реализация в виде моноида позволяет не писать никакой многопоточный код — можноиспользовать стандартный, просто даёте на вход файл и говорите "сверните моим моноидом", на выходе — подсчитанное количество слов в этом файле. Просто и удобно.


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

                  0

                  Так стоп, прикладная математика работает так: есть реальная сущность -> формализируем -> получаем мат.модель -> делаем лукап в мат теории -> получаем полезную теорему -> все развернули обратно в реальность -> ура есть профит.


                  Если же у меня просто какие то реальные сущености. И я их классифицирую то это ботаника.

                    +1

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

                      +1

                      Аналогия весьма отдалённая, т.к. в физике всегда есть что то что на текущий момент нельзя объяснить :)


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

                        +2

                        Так ведь наоборот же, оказалось что объяснение давно уже было)


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


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

                          0

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

                    0
                    Я, честно говоря, рассчитывал бы от математики получить примерно вот что: если мы реализовали в коде математическую структуру X, и доказали, что выполняются некоторые правила (типа наличия единицы и ассоциативности), то наш код будет обладать еще вот такими свойствами. Ну т.е., доказательства наличия некоторых свойства нашего кода, если он уже обладает некоторыми другими, более простыми.
                      +6

                      Ну, такие свойства есть. Собственно, основное свойство такое, что если мы пишем монады, то мы всегда можем их композировать друг с другом. А всё программирование заключается в том, чтобы побить сложную задачу накучку мелких, решить их по-отдельности, и собрать обратно. И вот когда мы собираем их обратно, обычно и возникают проблемы, потому что паззл не совпадает, и мы начинаем молотком их забивать, "закостыливая" некоторые места. Отсюда все эти эксепшны "should never throw" и так далее.


                      Гарантия того, что паззл гарантированно собирается — очень неплохая штука.

                        0
                        >всегда можем их композировать друг с другом
                        Ну как-бы из ассоциативности нечто похожее и должно вытекать, вроде )))

                        В целом это все понятно — на интуитивном уровне. Т.е. есть у нас единица, и есть ассцоиативная операция — можем сделать fold при помощи этой операции и этой единицы. Разве что гарантией я бы это не называл (ну или я не вижу, откуда у нас тут гарантии).
                          +1

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



                          У нас есть функция a -> m b и b -> m c, и мы их хотим скомпозировать в a -> m c. В этом нам монады и помогают.


                          А когда мы не можете отличить a -> b и a -> m b и используем одно вместо другого, и получается заддосивание шлюзов и другие неприятные вещи. Потому что это знание все равно есть, но вместо проверки компилятором одно живет в духе "давайте венгерской нотацией называть функции с суффиксом Async и сделаем правило не вызывать Async функции в цикле", а может и еще хуже: где-нибудь закопанно на корпоративной вики.

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

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

                              0

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

                                0
                                forM_ явно показывет, что тут происходит эффект (причем какой конкретно)

                                Какой эффект явно происходит в Id?


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

                                У меня map так не делает, только forEach.

                                  0
                                  Какой эффект явно происходит в Id?

                                  Если я пишу коде (Monad m) =>… то мне не важно, какой там эффект, я даю возможность вставить любой. В том числе и "no-op". Было быстранно, если бы не разрешал.


                                  У меня map так не делает, только forEach.

                                  А я вот в дотнете видел .Select(x => {Console.WriteLine(x); return x*2;}) не раз.

                                    –3
                                    В том числе и "no-op"

                                    Это софистика сейчас, если no-op это эффект, то тогда не-монадический код имеет эффект — тот самый no-op.


                                    А я вот в дотнете видел .Select(x => {Console.WriteLine(x); return x*2;}) не раз.

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

                                      +5
                                      Это софистика сейчас, если no-op это эффект, то тогда не-монадический код имеет эффект — тот самый no-op.

                                      Монада дает возможность встроить эффект, но-оп это тоже эффект. Точно так же как ФВП дают возможность передавать функцию, которая может быть () -> {} — нооп. От этого возможность выполнить произвольную функцию про которую мы ничего не знаем — не перестала быть ценной.


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

                                      Нет, это минус, у меня так эластик умер. А заодно и БД, когда орм не смогла это странслировать в SQL и попыталась выгрузить всю таблицу в память.

                                        –1
                                        Нет, это минус, у меня так эластик умер. А заодно и БД, когда орм не смогла это странслировать в SQL и попыталась выгрузить всю таблицу в память.

                                        Ну вот это к монадам и ФП точно отношения не имеет. Проблема-то не в Console.WriteLine, а в отсутствии маппинга.

                                          0

                                          Проблема в том, что без ссылочной прозрачности изменение сигнатуры CommentId -> Comment на CommentId -> IO Comment не является ломающим изменением.

                                            +1

                                            Ну так при чём тут ссылочная прозрачность-то?


                                            ORM для трансляции нужна не она

                                              0

                                              Без неё нет смысла в IO, потому что это просто маркер "здесь значение может поменяться"

                                                0

                                                Но при чём тут IO?

                        0
                        и доказали, что выполняются некоторые правила (типа наличия единицы и ассоциативности)

                        Дело в том, что в реальности они не выполняются. В ИРЛ-программировании не существует "настоящих" монад, по-этому любое утверждение о свойствах может неожиданно и нетривиально обломаться. И если в хаскеле это еще можно как-то подшаманить — то в энергичном языке нет. Да и некоторые монады просто в логике своей работы противоречат "монадичности".
                        Но свойства эти бесполезные и на практике неприменимые (за очень редким исключением) — по-этому никто обычно и не парится тем, что монада на самом деле не монада (вон для Nullable в c# тупо не пишется джойн, но менее монадой Nullable от этого не становится).
                        С другой стороны, выполняются "слабые конструктивные" утверждения — навроде того, что если у вас есть монада, то почти наверняка она определенным образом выражается через call/cc-монаду.

                          +1
                          А можно чуть пояснить что такое ИРЛ-программирование и почему там нет настоящих монад? Либо ссылкой кинуть.
                            –1

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

                              +1
                              Все абстракции текут, тут ничего не поделать. С другой стороны если нам не нужен какой то из законов, то почему бы его не выкинуть? Если выяснили что для композиции достаточно быть просто моноидом и не нужно ограничивать функтор до эндо, то давайте его выкинем вместе с ненужными нам свойствами типа join. Мат законы кажутся ненужными, так как мы начинаем думать о них как о само собой разумеющимся и не требующим доказательств. А потом когда встречаем случай где он не соблюдается и вся наша логика ломается — то сильно удивляемся.
                                –1
                                С другой стороны если нам не нужен какой то из законов, то почему бы его не выкинуть?

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


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


                                Что у нас в реальности? В реальности мы отдаем наверх некоторый объект x: a и продолжение cont: b -> c. Потом мы делаем f(x, cont) — это та самая общая конструкция, которая определяет, каким образом нам следует применить продолжение к тому, что мы реально туда засунули. Если здесь теперь a = m a', b = a', c = m c', то cont: a' -> m c', и f — это бинд. Общий тип f будет a -> (b -> c) -> d. Ограничение на типы при этом возникает из требования универсальности бинда: тип d должен быть связан с типом c, и тип a с типом b, собственно, a = m b, c = d в случае монады. Но вы можете выбрать другие варианты, какие захотите, все будет работать. Ну и никто, конечно, не требует ни законов ни даже функториальности тут.

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

                                  Не совсем все выкинуть, а просто не думать о тех из них, про которые думает компилятор. Пока о них думает компилятор например проверяя типы — все хорошо. Ну вот допустим написали хитрый fold, а потом выяснилось что с ним ассоциативность не работает за которой компилятор не следит — что результат будет зависеть от того в каком порядке фолдить. И узнаем об этом на проде где много ядер и потоков, где возникли кейсы, в которых он стал меняться.
                                    –1
                                    Не совсем все выкинуть, а просто не думать о тех из них, про которые думает компилятор.

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


                                    Ну вот допустим написали хитрый fold

                                    А при чем тут fold? Мы конкретно про монады. Ну и операция для folda в принципе не должна быть ассоциативна (часто это и не возможно), от нее это не требуется.

                                +4
                                Можете подробнее объяснить почему ReaderT, например, не настоящая монада? Или на примере — что настоящая монада, а что нет.
                                Спрашиваю из интереса, не ради спора. Если можно — простое объяснение, пожалуйста.
                                  0

                                  ReaderT это трансформер. Монадой будет ReaderT m, где m-монада.

                                    0

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

                                      0

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

                                        0
                                        Спасибо за ответ. Мне тоже непонятно что такое «ненастоящая монада». Или что имелось в виду.
                                        Просто не раз уже подобные утверждения слышал, хотел узнать подробнее.
                                          +1

                                          Пример ненастоящей монады — это Promise в Javascript.


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


                                          Также некоторая "ненастоящесть" наблюдается в монаде Promsie/Task/Future/… во многих нефункциональных языках — "настоящая"-то монада должна быть ленивой, чего обычно не наблюдается.

                                            0

                                            А вот про ленивость я не помню требований. Если говорить про категорию, то там важно только чтобы стрелки коммутировали. "Ленивость" — внешнее свойство по отношению к тому что теоркат моделирует. Если вспомнить, то мы в япах нас интересует категория Set. А отображения между множествами просто "есть", никто не "вычисляет" функции, соответственно и ленивости/жадности никакой тоже нет.

                                              0

                                              Тут противоречие не столько с теоркатом, сколько со стандартной интерпретацией теорката в программировании. Условный Task<int> — это не просто некоторое значение, а фоновой процесс, имеющий наблюдаемые сторонние эффекты.


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

                                                0

                                                Почему наблюдаемые? На уровне абстракции тасков вы их не наблюдаете.

                                                  +1

                                                  Ну мы же всегда в модели оцениваем. А то так получится что fmap id != id, потому что разное количество памяти выделяем, а это тоже можно пронаблюдать, и сделать вывод что функторы не работают. Или замерять количество выделяемого процессором тепла, для двух фмапов будет больше, чем для одного. Ну и так далее.


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

                      +3

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


                      Nullable<string> s; // ошибка
                      Nullable<Nullable<int>> i; // снова ошибка

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

                      Это верно только для языков, в которых у значений нет "врождённых" свойств. Для C# это не так:


                      public extension BrokenFunctor of List : Functor<List>
                      {
                          public static List<B> Map<A, B>(List<A> source, Func<A, B> map) =>
                              typeof(A) == typeof(B) ? source.Select(map).ToList() : new List<B>();
                      }



                      Теперь для Applicative


                      К сожалению, реализовать один и тот же интерфейс для одного типа двумя различными способами нельзя

                      Вроде бы именно для shape в C# это как раз можно? Не просто же так каждой реализации даётся своё имя...


                      // вообще тут должен быть бесконечный генератор элемента 'a'

                      Вообще необходимость генерации именно бесконечной последовательности надо бы вывести из каких-нибудь аксиом Applicative.


                      А почему функтор? Имея функции LiftA2 и Pure легко реализовать Map:

                      Я бы всё-таки обошелся без "мусорного" нуля и без замыкания:


                      static T<B> MapAnyFunctor<T, A, B>(T<A> source, Func<A, B> map) where T : Applicative =>
                          LiftA2(source, Pure(map), (a, f) => f(a));
                        0

                        На самом деле тут ведь вопрос как определить категорию. Если мы возьмем категорию структур (то есть Hask / ReferenceTypes) то всё снова работать будет.


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


                        Это верно только для языков, в которых у значений нет "врождённых" свойств. Для C# это не так:

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


                        Вроде бы именно для shape в C# это как раз можно? Не просто же так каждой реализации даётся своё имя...

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


                        Вообще необходимость генерации именно бесконечной последовательности надо бы вывести из каких-нибудь аксиом Applicative.

                        Это очевидно из тех соображений, что LiftA2(Pure(a), bs, SomeFunc) должно быть эквивалентно bs.Map(b => SomeFunc(a, b)). А теперь вспомним, что ZipList по своей семантики LiftA2 усекает список до самого короткого. Отсюда вывод, что результат Pure(a) должен быть длиннее любого наперед заданного списка bs. По индукции приходим к тому, что результат должен быть бесконечной длины.


                        Я бы всё-таки обошелся без "мусорного" нуля и без замыкания:

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

                          +1

                          Нет, Nullable не будет функтором даже в категории структур из-за запрета на рекурсию.

                            0

                            Не припомню в определении функтора никакого требования на произвольную вложенность. Nullable<T> не является обычной структурой в принципе. Map будет работать с любой структурой, а сам Nullable мы в категорию (оригинальных объектов) не включаем, если мап работает, значит — функтор.

                              +1

                              Он же, в данном случае, эндофунктор. Тогда если мы Nullable в кодомен не включили, то и в домене его нет. Тогда куда функтор?

                                +1

                                Так функтор живет в другой категории) Если посмотрим на картинку из милевского:



                                То можно увидеть, что функтор отображает категорию С в D. Соответственно, мы можем сказать, что в С нет нуллейбла, а в D он есть.

                                  +2

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

                                    0

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


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

                                      0

                                      А куда вы этот where T : struct допишете? Ограничение на домен — это не свойство отдельной функции, а свойство функтора, и дописывать его надо к вот этой строчке:


                                      public extension NullableFunctor of Nullable: Functor<Nullable>
                                        0

                                        Ну да, у нас будет какой-то StructFunctor, а обычный будет его расширять.

                                          0

                                          Что-то мне кажется, что этот StructFunctor вы планируете ввести только ради одного Nullable.

                                            +1

                                            Да, вы правы. Соглашусь с доводами.

                                    0

                                    Круто. А тогда у join'a для Nullable какой тип? :)

                                      0

                                      Хм, видимо действительно где-то законы нарушаются. Хороший вопрос, видимо нельзя просто так вырвать F из самой категории C. Стоит над этим подумать.

                                        +2
                                        Хм, видимо действительно где-то законы нарушаются.

                                        И это какбе намекает на то, что, по факту — хрен с ними, с законами :)


                                        видимо нельзя просто так вырвать F из самой категории C.

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

                                          0

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

                                            0
                                            Если я правильно понял всю эту ветку: Nullable выполняет законы моноида, но не эндофунктора и следовательно не монады, так как конструктор типа Nullable принимает только значимые типы, а возращает только ссылочные, поэтому не допускает вложенность контейнеров. Но возможно ограничение эндо- не такое и нужное и никак не мешает использовать композицию. То есть 1 уровень вложенности хватит для большинства кейсов. А так сложилось просто потому что в Haskell проще не ограничивать, а в C# оказалось проще ограничить.
                                              +1

                                              Верно. Соответственно, нуллейбл неплохой пример аппликатива, который не является монадой. Обычно все интересные аппликативы являются монадами, а тут хороший контрпример.


                                              А так сложилось просто потому что в Haskell проще не ограничивать, а в C# оказалось проще ограничить.

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


                                              Или, если в двух словах: сначала не подумали, а потом на голову свалилась обратная совместимость.

                                                0
                                                Мне кажется null ввели не потому что так в джаве, а потому что так во всех императивных языках. Мы заранее объявили переменную, хранящую какое то состояние, но использовать будем где то в другом месте, но пока неизвестно когда и где — типичный императивный кейс. А раз так, то ее нужно заполнить чем то дефолтным. Для маленького значимого типа так уж и быть выделим немного памяти для дефолта. А для большого класса с неизвестным заранее размером как то хочется отложить на потом этот вопрос, поэтому пусть будет null. Хотя тут целая статья habr.com/ru/post/309462
                                                А в ФП мире у тебя все состояние в параметрах, поэтому нет необходимости забивать что то по дефолту на будущее.
                                                  +2

                                                  Раст например императивный некуда, императивнее сишарпа, но там нуллов нет, а всё что надо завернуто в Option.

                                                    0

                                                    Но там есть mem::uninitiated или что то такое. хотя я думаю это все можно завернуть тоже в типы.

                                                      +1
                                                        0

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

                                                          +1

                                                          Ну, вы можете, но не в расте.


                                                          Все же нужно баланс соблюдать. Я не думаю, что кто-то хочет доказывать 2+2=4 в каждой программе.


                                                          По сути если у вас есть инстанс типа, значит он правильно заполнен. Воспользовать "неправильным" заполнением можно только через ансейф, а заполнение некорректным типом это инста-УБ (что по ссылке кстати и написано). Поэтому как раз-таки вынесли на уровень типов, что любой T всегда well-formed, если нигде УБ нет.

                                                            +1

                                                            ATS так умеет.

                                                    –1
                                                    Обычно все интересные аппликативы являются монадами, а тут хороший контрпример.

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

                                                      0
                                                      Это как раз хороший пример того, что чтобы быть монадой в смысле «интерфейс блаблабла» и нормально это в качестве монады использовать — совсем не требуется быть монадой математически

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

                                                        Вы не совсем понимаете, что там за ассоциативность. На самом деле прямой ассоциативностью она становится только для объектов-множеств, в случае эндофункторов ее нельзя так трактовать. Это свойство, бкуквально, значит, что
                                                        join(join(T^2)*T) = join(T*join(T^2))
                                                        в случае категории Set Т — обычное множество, а join — некоторая бинарная операция на нем и, с-но, данное свойство обозначает ассоциативность join как бинарной операции. В случае эндофункторов join не является бинарной операцией, по-этому об ассоциативности говорить просто смысла нет.


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

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

                                                            Ассоциативна композиция Клейсли, т.е. ф-я >=>: (a -> m b) -> (b -> m c) -> a -> m c.


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

                                                            Зависит от того, что понимать под "чистой монадой" и "схлопыванием с середины" :)

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

                                Теперь про вторую часть.


                                Первым пунктом, следующим из предыдущего абзаца, стоит выделить упрощение языка. Посмотрите, сколько мусора натащил сишарп, чтобы выразить простую идею "Сделай что-нибудь, а затем сделай кто-нибудь еще". И асинк-авейт, и LINQ, и null propagation являются частными случаями общей идеи.

                                Вот только асинк-авейт и LINQ под общий тайпкласс не затащить никак пока в языке есть мутабельность.


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


                                В случае LINQ в функцию Bind/SelectMany передаётся функция, которая может быть вызвана любое число раз и которая не должна иметь побочных эффектов.


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


                                И это всё — не просто недоработки в языке, а именно что следствие мутабельности. Тот же Rust, как вы говорите, "прошелся по граблям" не из мазохизма разработчиков, а потому что альтернатива тут — стать Хаскелем. Каким бы замечательным языком ни был Haskell, второй такой же язык никому не нужен, ибо первый уже есть.


                                Сишарп тут в абсолютно схожей ситуации. Посмотрите на вот этот пакет System.Linq.Async. Разработчики из майкрософта в нём занимаются буквально тем, что копипастят реализацию LINQ из corefx, расставляя где надо async-await.

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


                                Тут лучше бы подошел пример библиотеки Akka.NET Streams, а именно файлов FlowOperations.cs и SubFlowOperations.cs, где в одном и том же проекте разработчики оказались вынуждены написать полную копию своего же API...

                                  +1

                                  Ну, мутабельность это серьезная пробелема, да. Сходу даже сказать что надо с делать чтобы её победить сказать не готов. Если не предлагать конечно всем писать в иммутабельном стиле с явным State<T> где надо.


                                  И это всё — не просто недоработки в языке, а именно что следствие мутабельности. Тот же Rust, как вы говорите, "прошелся по граблям" не из мазохизма разработчиков, а потому что альтернатива тут — стать Хаскелем. Каким бы замечательным языком ни был Haskell, второй такой же язык никому не нужен.

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


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

                                  Ага, особенно функции Min/Max/Average/..., которые просто вываливаются списком из 30 перегрузок)


                                  Тут лучше бы подошел пример библиотеки Akka.NET Streams, а именно файлов FlowOperations.cs и SubFlowOperations.cs, где в одном и том же проекте разработчики оказались вынуждены написать полную копию своего же API...

                                  Хороший пример, спасибо, если не возражаете, добавлю в статью.

                                    +5
                                    Мне кажется они просто недостаточно этот момент продумали. Не боги горшки обжигают, поэтому несовершенство языка вполне объяснимо.

                                    Да нет, тут в другом дело. У них язык с линейными типами, притом полноценными, а из этого сразу же следует два вида функций. А если в языке два вида функций — то и тайпкласс монад тоже распадается на два разных тайпкласса с разными свойствами.


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

                                    То, что сейчас делается — то и надо делать. Можно обобщить немного.


                                    У нас есть два вида монад: "ветвящаяся" монада и "линейная" монада. Им нужен разный синтаксис, "ветвящейся" — функциональный LINQ, "линейной" — структурный async-await.


                                    Монада IEnumerable будет строго "ветвящейся", монада Task будет строго "линейной". Монаду Option можно записать в оба тайпкласса.


                                    Ага, особенно функции Min/Max/Average/..., которые просто вываливаются списком из 30 перегрузок)

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

                                      0

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

                                        0
                                        У них язык с линейными типами, притом полноценными

                                        Ах, если бы, если бы.

                                          +1

                                          Какие-то странные там претензии...


                                          Например, автор того текста пытается запретить "building a reference counted cycle which will leak into infinity", но при этом считает нормальным mem::forget.

                                            0

                                            Я просто хотел сказать, что в Rust не линейные типы, а аффинные. Гарантий на вызов деструкторов нет, любое значение можно забыть через std::mem::forget, зафорсить вызов некоторой поглощающей функции вместо деструктора нельзя (ну, чтобы это в компил-тайме проверялось, естесственно).

                                              +3

                                              С практической точки зрения разницы никакой. Представьте, что переменная некоторого типа будет уничтожена через день/неделю/месяц/год/10 лет… Всё это время ситуация остаётся корректной с точки зрения линейности типа. Что принципиально меняется когда это время становится равно бесконечности?


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


                                              Для случаев, когда деструктора недостаточно, всегда есть #[must_use]

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

                                                Меняется: аргументы передать нельзя, об ошибке сообщить нельзя.


                                                Для случаев, когда деструктора недостаточно, всегда есть #[must_use]

                                                Не поможет, всегда можно сделать let _ = value;

                                                  0

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


                                                  Так-то и в языках с termination checker'ами никто не мешает вычислять Аккермана на каждой итерации, но любим мы их не за это.

                                            +1
                                            У них язык с линейными типами, притом полноценными, а из этого сразу же следует два вида функций. А если в языке два вида функций — то и тайпкласс монад тоже распадается на два разных тайпкласса с разными свойствами.

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

                                              0

                                              Полиморфизм по линейности будет полезен в какой-нибудь хитрой библиотеке. Но он не сможет сделать синтаксис async-await подобным linq.

                                          –2
                                          В случае LINQ функция <...> не должна иметь побочных эффектов

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

                                            +1
                                            В случае асинк-авейт в функцию Bind передаётся функция, которая должна быть вызвана не более 1 раза и которая может иметь побочные эффекты.

                                            Формально, побочные эффекты имеет сам Bind. А аргумент бинда просто отдает Task. Проблемой можно было бы тут назвать не мутабельность, а то, что два await на один и тот же таск и на две копии одного — это разные вещи, но на самом деле это не проблема. С-но, написать linq-реализацию для таска никто не мешает, можно и обратное сделать, вон в f# все прекрасно работает. Никаких "линейных монад" и "ветвящихся монад" тут нет, одинаковые они.

                                              0

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

                                                0

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

                                                  0

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

                                                    –1

                                                    Ну так вот async/await прекрасно выглядит.

                                                      0

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

                                                        0

                                                        Он не то чтобы чужероден — но он написан в функциональном стиле. В языке C# должна быть возможность написать структурный код.

                                                          0

                                                          Мне нравится концепция 3 layer cake, поэтому не вижу ничего дурного в функциональном стиле. Каждый слой отвечает за свой аспект, но бизнес-логику (а именно там больше всего кода, причем самого важного кода) и для неё обычно как раз функциональный вариант подходит лучше, если, конечно, язык позволяет.


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

                                                          0
                                                          Но так писать не принято.

                                                          Я так писал. Брат жив :)
                                                          Есть, кстати, важный плюс — логику не связанную с-но с конкретным монадическим вычислением приходится вытаскивать в отдельные чистые ф-и (т.к. внутрь linq вкорячить затруднительно), что в итоге делает код приятнее.

                                                –3
                                                Опять поверхностный наброс.

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

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

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

                                                  Эмм, информация об исполнении — это один из инстанов монады, в частности монада IO. Есть еще Par, есть Async, есть еще разные.


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


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

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


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

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


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

                                                  Кучерявый способ — это тот, который используется практически во всех современных языках? Это и then в жсе (бог с ним, что промисы в жс ненастоящие монады), это and_then в расте, это ContinueWith в сишарпе, это thenCompose в Java… Все они кучерявые, оказывается. А где не кучерявые? Могу ли я предположить, что в го?

                                                    +1

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


                                                    Насколько я понимаю для полноценных монад нужен уровень абстракции выше. Собственно интуитивно понять отличие от привычных дженериков легко, если посмотреть на сигнатуру: вместо List<T> мы пишем M<Data>. Т.е. параметризуем не по внутренностям а по внешней обертке.


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

                                                      –1
                                                      >> Насколько я понимаю для полноценных монад нужен уровень абстракции выше

                                                      Там всё сложнее.

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

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

                                                      То есть мы просто и тупо копируем паттерн «монада» из любви к искусству. Точнее — фанаты ФП копируют. А пользы от копирования — только, так сказать, понты в разговорах на хабре.

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

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

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

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

                                                      Ну а ссылки на повышенный уровень абстракции здесь, в общем-то, опять ничего не объясняют и лишь добавляют ещё больше тумана. Разработчику нужно знать, зачем нужны абстракции. Например — абстракция «список» — очень понятная, простая, с массой практических приложений. А вот абстракция «монада» — здесь я уступаю дорогу сторонникам ФП, может когда-нибудь и до них дойдёт, что их идеи без понимания практического смысла никто не поддержит.

                                                      Хотя это всё в итоге уйдёт в обсуждение темы «зачем нужна теория категорий», а потому даже в стандарте хаскеля пишут примерно так — теория категорий здесь не при делах, просто вот есть у нас такие вычурные названия и паттерны, и всё, пользуйте (если хотите).
                                                        +4
                                                        Странные рассуждения. В монадах ничего сложного нет. Плюс позволяют они гораздо больше, чем Вы описываете. А именно — это отличный синтаксис для DSL. Мне, например, в Java очень нехватает чего-то вроде скаловского Slick, который устроен гораздо проще и понятнее чем тот-же hibernate. Так что где тут overengineering — я бы поспорил.
                                                        Понимание практического смысла давно есть — всякие вещи вроде корутин, теперь можно реализовывать не на уровне компилятора, а описывать непосредственно на языке программирования.
                                                          +1
                                                          Возможно, при моделировании чего-то столь же абстрактного, как и сама теория категорий, монады в хаскеле действительно дают какие-то бонусы благодаря соответствию определениям из теорий групп и категорий. Но вот в практической деятельности пользы от соответствия абстрактным теориям я не встречал. По частям, например для теории групп, могу себе представить пользу, но вот где все эти определения нужны в сумме — не знаю.

                                                          Ходить по HTTP в другие сервисы, взаиомдействовать с БД, читать данные из конфига, писать данные в лог — это всё "абстрактные и оторванные от практики вещи"? Или "я привык считать, что монады — это что-то сложное для математиков, поэтому буду игнорировать все факты, которые этому противоречат"?


                                                          Ну а ссылки на повышенный уровень абстракции здесь, в общем-то, опять ничего не объясняют и лишь добавляют ещё больше тумана. Разработчику нужно знать, зачем нужны абстракции. Например — абстракция «список» — очень понятная, простая, с массой практических приложений. А вот абстракция «монада» — здесь я уступаю дорогу сторонникам ФП, может когда-нибудь и до них дойдёт, что их идеи без понимания практического смысла никто не поддержит.

                                                          То есть "хочу двух наследников одного интерфейса, один из которых работает синхронно, а другой — асинхронно" — это туманная непонятная фигня? Как по мне, куда уж практичнее. Конечно, есть любители взять асинхронный интерфейс как набольший общий делитель, и обмазаться Task.FromResult/Promise.resolve/... в синхронном варианте, но мне это едва ли кажется хорошим и надежным решением. Которое ещё и не работает, стоит чуть-чуть усложнить пример.


                                                          Другой пример — хотим читать данные из конфига, соответственно в ДТО для чтения из конфига все поля — опциональные:


                                                          struct Config {
                                                             workers_count: Option<u32>,
                                                             latency: Option<u32>,
                                                             app_url: Option<String>,
                                                             app_port: Option<Port>,
                                                             ...
                                                          }

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


                                                          struct AppConfig {
                                                             workers_count: u32,
                                                             latency: u32,
                                                             app_url: String,
                                                             app_port: Port,
                                                             ...
                                                          }
                                                          
                                                          fn get_config(config: Config) -> AppConfig {
                                                            AppConfig {
                                                              workers_count = config.workers_count.unwrap_or(10),
                                                              latency = config.latency.unwrap_or(100),
                                                              app_url = config.app_url.unwrap_or("localhost".into()),
                                                              app_port = config.app_port.unwrap_or(Port::new(8080)),
                                                              ...
                                                            }
                                                          }

                                                          Вопрос — как не писать функцию get_config и не дублировать структуру AppConfig?


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

                                                            +1
                                                            >> Ходить по HTTP в другие сервисы, взаиомдействовать с БД, читать данные из конфига, писать данные в лог — это всё «абстрактные и оторванные от практики вещи»?

                                                            Как эти конкретные задачи решаются при помощи монад? Если это опять будет стандартное указание на один из способов работы с асинхронностью, то я выше уже говорил — есть и другие способы. Самый простой пример — просто отдаём listener некой функции и забываем о проблеме. Этот шаблон реализован уже с пол века назад (до появления ФП), и вот теперь его же реализовали «с монадами», то есть наворотили много абстрактных понятий вокруг тривиального обработчика результата запроса. Но зачем эта вся абстракция? Что она меняет? Ведь мы всё тот же listener передаём (да, и в монаду тоже).

                                                            Ну и далее логично следует очередная простая проблемка — зачем нам выполнять определения из теорий групп и категорий для listener-а? Что это даёт? Где тут нужны ассоциативность, коммутативность и прочее?

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

                                                            >> То есть «хочу двух наследников одного интерфейса, один из которых работает синхронно, а другой — асинхронно» — это туманная непонятная фигня?

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

                                                            >> Вопрос — как не писать функцию get_config и не дублировать структуру AppConfig?

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

                                                            ЗЫ.

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

                                                              Монада это и есть listener, все верно. Просто к самому паттерну добавляются некоторые встроенные средства для его использования — вам не надо выполнять руками cps-преобразование кода (которое требуется для того, чтобы пробрасывать коллбеки) и огребать от callback hell, за вас все делает либо do-нотация, либо набор монадических комбинаторов, либо (на худой конец) жесткое структурирование через bind.
                                                              Иными словами, с точки зрения чисто практического использования, монады — это listener, который сделан так, чтобы снять с программиста необходимость закатывать солнце вручную.
                                                              При том данный набор минимален — т.е. если вы попробуете сделать такой "удобный listener", вы в любом случае получите эквивалентную монадам конструкцию.
                                                              Фактически, вся польза монад — она идет от того, что монады это специализации call/cc.


                                                              Ну и далее логично следует очередная простая проблемка — зачем нам выполнять определения из теорий групп и категорий для listener-а?

                                                              После того как вы фиксировали набор комбинаторов для конкретной монады и не выходите за его пределы — незачем. Более того — на практике и не выполняют.

                                                                0

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


                                                                В случае lister, а елси надо скомбинировать два listner-а, то как это сделать? Где гарантии что типы будут совместимы? Почему ни в С# ни в Rust нельзя определить этот концепт тогда, Может все такие нужна теория для типов чтобы понимать что мы делаем?

                                                                  +2
                                                                  Т.е. в итоге толку от математики ноль — условие теорем нарушены и их выводами мы не пользуемся?

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


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


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


                                                                  В случае lister, а елси надо скомбинировать два listner-а, то как это сделать?

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

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

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

                                                                      +2
                                                                      при рефакторинге кода (при условии доказательства теорем) значительно проще пользоваться законами ассоциативности т.е. a*b + a*c ~= a*(b+c). Или пользоваться определением морфизмов из теории категорий (ака простой шаблон манипулирования почти всем кодом), например: a * 5 ~= (a << 2) + a
                                                                        0

                                                                        В случае с динамической типизацией гарантий что там с каллбэками будет никаких нету в любом случае.


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

                                                                          0
                                                                          В случае с динамической типизацией гарантий что там с каллбэками будет никаких нету в любом случае.

                                                                          Гарантии есть в том, что интерфейс будет "хорошим". Т.е. через него точно будет можно выразить (причем достаточно удобным способом) все, что мне потребуется.


                                                                          А вообще математика так не работает

                                                                          Именно так она и работает, вы ее плохо знаете просто.


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

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

                                                                            0

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


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

                                                                      0
                                                                      Просто к самому паттерну добавляются некоторые встроенные средства для его использования — вам не надо выполнять руками cps-преобразование кода (которое требуется для того, чтобы пробрасывать коллбеки) и огребать от callback hell, за вас все делает либо do-нотация, либо набор монадических комбинаторов, либо (на худой конец) жесткое структурирование через bind.

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

                                                                      Собственно всё сводится к тому, что ваши «некоторые встроенные средства» прекрасно реализуются на любом языке, и без монад. Хотя, скажем, вложенная типизация (типа T<A<B<C...>>>) пока что мало распространена, но это опять же не монады, а встроенный в компилятор отдельный механизм, отслеживающий проблемы типизации. Это я к ответу участника «PsyHaSTe», там у него типизированные инстансы выдаются за пользу от монад. Может вы что-то другое имели в виду, но ваши жаргонизмы я не понял.
                                                                      При том данный набор минимален — т.е. если вы попробуете сделать такой «удобный listener», вы в любом случае получите эквивалентную монадам конструкцию.

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

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

                                                                      Далее отвечу участнику «PsyHaSTe» (ибо меня тут ограничивают).

                                                                      >> Никакой копипасты. И какая тут асинхронность?

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

                                                                      >> программа не дедлочится, и не ддосит шлюз

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

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

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

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


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

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


                                                                        там у него типизированные инстансы выдаются за пользу от монад. Может вы что-то другое имели в виду

                                                                        Я сказал ровно то, что хотел — монады избавляют вас от необходимости делать руками cps-преобразование.


                                                                        То есть я пока не вижу «минимальности» набора.

                                                                        Ну так вы попробуйте сделать какой-то другой набор.


                                                                        Можно хотя бы на примере — что я не смогу сделать на императивном языке без монад?

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


                                                                        Давайте так, вот у меня есть код:


                                                                        async function yoba() {
                                                                            const x1 = await query1();
                                                                        
                                                                            if (f(x1)) {
                                                                                for (const x2 of x1) {
                                                                                    if(g(x2)) {
                                                                                        await query31(x2);
                                                                                        await query32(x2); 
                                                                                    } else {
                                                                                        await query33(x2);
                                                                                    }
                                                                                    await query 34(x2);
                                                                                }
                                                                                await query41(x1);
                                                                            } else {
                                                                                await query42(x1);
                                                                            }
                                                                        
                                                                            return await query5(x1);
                                                                        }

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


                                                                        ЗЫ: вот я знаю монады, по-этому сразу знаю как реализовать аналог конструкции for из примера выше, даже в языке, у которого поддержки монад в том или ином виде не будет (т.е. не будет async/await или чего-то вроде того). Мне даже думать не надо — я просто сразу знаю. А вы знаете?

                                                                          0
                                                                          >> запишите плиз как у вас там будет на listener'ах

                                                                          Да так же и будет. Но без монад. Точнее — без необходимости привлекать понятие «монада».

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

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

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

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

                                                                          Ну и «совсем в итоге» — так зачем же нам монады, если я обошёлся без них, а к тому же они ещё и негибкие? Я вот не хочу, что бы меня нагибали на использование кем-то придуманного шаблона, даже если при этом уверяют, что «всё построено на ужасно умной математике».
                                                                            +1
                                                                            Да так же и будет. Но без монад

                                                                            Как "так же"? Код приведите, пожалуйста.


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

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


                                                                            Ну и «совсем в итоге» — так зачем же нам монады, если я обошёлся без них

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

                                                                              +1
                                                                              Привожу код:
                                                                              public class Service<T2,T1 extends Iterable<T2>,T3> extends Synchronizer
                                                                              {
                                                                              	public T3 yoba(Behavior<T1,T2,T3> b) throws InterruptedException, ExecutionException
                                                                              	{
                                                                              		T1 x1 = get(()->b.query1());
                                                                              		if (test(()->b.f(x1)))
                                                                              		{
                                                                              			for (T2 x2:x1)
                                                                              			{
                                                                              				if (test(()->b.g(x2)))
                                                                              				{
                                                                              					act(()->b.query31(x2));
                                                                              					act(()->b.query32(x2));
                                                                              				}
                                                                              				else act(()->b.query33(x2));
                                                                              				act(()->b.query34(x2));
                                                                              			}
                                                                              			act(()->b.query41(x1));
                                                                              		}
                                                                              		else act(()->b.query42(x1));
                                                                              		return get(()->b.query5(x1));
                                                                              	}
                                                                              }
                                                                              

                                                                              Если непонятен входной параметр, то вот он:
                                                                              public interface Behavior<T1,T2,T3>
                                                                              {
                                                                              	public T1 query1();
                                                                              	public boolean f(T1 t1);
                                                                              	public boolean g(T2 t2);
                                                                              	public void query31(T2 x2);
                                                                              	public void query32(T2 x2);
                                                                              	public void query33(T2 x2);
                                                                              	public void query34(T2 x2);
                                                                              	public void query41(T1 x1);
                                                                              	public void query42(T1 x1);
                                                                              	public T3 query5(T1 x1);
                                                                              }
                                                                              

                                                                              Ну и примитивнейшая библиотека, реализацию которой осилит даже ещё не дописавший до конца Hello World начинающий разработчик:
                                                                              public class Synchronizer
                                                                              {
                                                                              	private ExecutorService executorService;
                                                                              	
                                                                              	protected <T> T get(Supplier<T> bs) throws InterruptedException, ExecutionException
                                                                              	{ return executorService.submit(()->bs.get()).get(); }
                                                                              	
                                                                              	protected boolean test(BooleanSupplier bs) throws InterruptedException, ExecutionException
                                                                              	{ return executorService.submit(()->bs.getAsBoolean()).get(); }
                                                                              
                                                                              	protected void act(Runnable r) throws InterruptedException, ExecutionException
                                                                              	{ executorService.submit(r).get(); }
                                                                              }


                                                                              Собственно выше дана полная реализация вашего «ТЗ». Реализуя приведённый Behavior вы в нём можете хоть с марсом связываться, общая логика сервиса от этого не изменится.

                                                                              Теперь очевидные преимущества:

                                                                              1) Нет монад. То есть нет всего того ненужного множества смыслов, которыми грузят мир сторонники ФП. Все эти смыслы с точки зрения реализации ТЗ — просто мусор. Может в каких-то других очень редких случаях эти смыслы полезны, но в вами же предложенном задании — от них толку = абсолютный ноль.

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

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

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

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

                                                                              Если сказать короче — в угоду следованию функциональной модели ФП приводит к совершенно ненужной сложности, из которой далее следуют выше показанные пункты (и наверняка не только они). Ну а ваше заявление про некую минимальность решения на основе ФП явно не выдерживает критики.
                                                                                +4
                                                                                Если непонятен входной параметр, то вот он:
                                                                                public interface Behavior<T1,T2,T3>
                                                                                {
                                                                                public T1 query1();
                                                                                public boolean f(T1 t1);
                                                                                public boolean g(T2 t2);
                                                                                public void query31(T2 x2);
                                                                                public void query32(T2 x2);
                                                                                public void query33(T2 x2);
                                                                                public void query34(T2 x2);
                                                                                public void query41(T1 x1);
                                                                                public void query42(T1 x1);
                                                                                public T3 query5(T1 x1);
                                                                                }

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

                                                                                  0

                                                                                  В чём вообще смысл писать executorService.submit(()->foo()).get(); и чем это отличается от простого вызова foo() кроме повышенного расхода ресурсов?

                                                                                    0
                                                                                    Что-то уровень вопросов меня смущает.

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

                                                                                    Выше комментарий точно так же вытекает из непонимания. Правда уровни непонимания, похоже, разные.

                                                                                    Вот эти уровни:
                                                                                    1) Непонимание сути обсуждения.
                                                                                    2) Непонимание сути «ТЗ».
                                                                                    3) Непонимание базового API Java.

                                                                                    С первыми двумя, я надеялся, со стороны сторонников ФП не будет сложностей, но я оказался неправ…

                                                                                    Если же сложность с пунктом №3, то вот здесь всё написано.

                                                                                    Ну и по оптимальности.

                                                                                    Действительно, я «по быстрому» выбрал те интерфейсы, которые даны в пакете function, а вот Callable находится в concurrent. Но разумеется, можно было напрямую использовать Callable и тогда в Synchronizer-е не нужны были бы лямбды.

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

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

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

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


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

                                                                                    +1
                                                                                    Реализуя приведённый Behavior вы в нём можете хоть с марсом связываться, общая логика сервиса от этого не изменится.

                                                                                    Вы, видимо, не поняли. Behavior как раз меняться не должен. Он имеет одну единственную реализацию, которая используется во всех случаях. Именно в этом смысл. Ф-и query не содержат никакой логики о том, как запрос отправляется. Они возвращают сам запрос (не ответ, запрос).


                                                                                    Ну и да — что-то я колбеков с листенерами у вас не увидел, вы просто сделали код синхронным, понаставив блоков. Так что в любом случае не засчитывается, давайте вторую попытку.

                                                                                      –1

                                                                                      Ну любая асинк функция это стейт машина, которую коментатор вышк вам расписал явно. Монады это не совсем про это. Асинк в жаваскрипте как я понимаю это не общая абстракция а просто один конкретный трюк компилятора.

                                                                                        0

                                                                                        В том-то и проблема, что user_man не написал никакого конечного автомата.

                                                                                          +1
                                                                                          Ну любая асинк функция это стейт машина, которую коментатор вышк вам расписал явно. Монады это не совсем про это

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


                                                                                          Асинк в жаваскрипте как я понимаю это не общая абстракция а просто один конкретный трюк компилятора.

                                                                                          Можно async/await поменять на function*/yield — будет общая абстракция.

                                                                                          –2
                                                                                          >> Так что в любом случае не засчитывается

                                                                                          Нет, так дело не пойдёт. Навели тумана и теперь троллите. Нехорошо.

                                                                                          Предлагаю следующий план дискуссии:
                                                                                          1) Вы честно признаёте, что показанная реализация соответствует вашему «ТЗ».
                                                                                          2) Вы соглашаетесь с тем, что не указали полной информации в своём ТЗ, а потому там осталось место для различных вариантов реализации.
                                                                                          3) Вы, наконец, указываете, каким же на самом деле должно быть ТЗ, благо моя реализация вам явно показывает, где и что вы недоговорили.
                                                                                          4) Ну и было бы неплохо привести вашу версию кода. Это устранит неоднозначность, свойственную любому ТЗ, а так же не даст вам отвертеться в случае, когда я приведу аналогичный по функционалу и более простой код без монад.

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

                                                                                          ЗЫ.

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

                                                                                            А зачем форкать, а потом сразу делать get (join)? Где асинхронность? Я то сначала подумал вы там стейт-машину сделали, а вы просто лямбда скобочек понатаскали.

                                                                                              +2
                                                                                              1) Вы честно признаёте, что показанная реализация соответствует вашему «ТЗ».

                                                                                              Но она не соответствует. Ваш код:


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

                                                                                              2) Вы соглашаетесь с тем, что не указали полной информации в своём ТЗ

                                                                                              Я же вам привел пример кода. Он и является основным ТЗ, все прочее — просто пояснение. Очевидно, что вы должны написать код, который работает также. hint: оригинальный код мог быть асинхронным. hint2: а мог быть и нет, предполагалось что синхронность/асинхронность можно выбирать на call site


                                                                                              Ну и было бы неплохо привести вашу версию кода.

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


                                                                                              function* yoba() {
                                                                                                  const x1 = yield query1();
                                                                                              
                                                                                                  if (f(x1)) {
                                                                                                      for (const x2 of x1) {
                                                                                                          if(g(x2)) {
                                                                                                              yield query31(x2);
                                                                                                              yield query32(x2); 
                                                                                                          } else {
                                                                                                              yield query33(x2);
                                                                                                          }
                                                                                                          yield query 34(x2);
                                                                                                      }
                                                                                                      yield query41(x1);
                                                                                                  } else {
                                                                                                      yield query42(x1);
                                                                                                  }
                                                                                              
                                                                                                  return query5(x1);
                                                                                              }

                                                                                              Ну и потом мы делаем просто runInHTTP(yoba) или runInWebsocket(yoba) или runInMarsDeliverNetwork(yoba).

                                                                                                –7
                                                                                                Ну что-ж, настало время удивительных историй…

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

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

                                                                                                Далее придётся пояснять всем отметившимся «знатокам», почему они все неправы. Да, обидно, ЧСВ страдает, но надо. Умные примут этот урок, ну а остальным уже ничем не поможешь.

                                                                                                Итак, массово расставленные в коде предыдущей версии ТЗ ключевые слова await означают, что первоначальный код (внезапно) точно такой же синхронный, как и предоставленный в ответ на ТЗ. Поясню коротко, как работает конструкция await («знатоки», не ухмыляйтесь, а прочитайте внимательно). Сначала создаётся обещание. В момент его создания в параллельном потоке запускается выполнение его задачи. А в момент встречи ключевого слова await текущий поток (то есть не тот, в котором выполняет свою задачу обещание) останавливается и ждёт завершения работы обещания. Так вот, густо расставленные await-ы как раз и останавливают текущий поток именно в тех местах, в которых он бы остановился, если бы все эти queryXY запускались строго последовательно. И это превращает выполнение запросов в синхронный процесс. Зачем это надо? Понятия не имею. Автор предложил мне показать код вместо дальнейших пояснений. Поэтому я и не стал думать о таких вещах, а тупо сделал копию предложенного. Да, у меня вызов тоже синхронный, да, непонятно, зачем это нужно, но здесь важно одно — такая реализация соответствует ТЗ. А если ТЗ «не очень», то какие претензии к реализации? Trash in, trash out.

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

                                                                                                В общем, не повторяя акты из марлезонского балета, сразу приведу код, который (как требуют «знатоки») является реально асинхронным. Вот он:

                                                                                                public class Service<T2,T1 extends Iterable<T2>,T3> extends Synchronizer
                                                                                                {
                                                                                                	public T3 yoba(Behavior<T1,T2,T3> b) throws InterruptedException, 
                                                                                                
                                                                                                ExecutionException
                                                                                                	{
                                                                                                		T1 x1 = b.query1();
                                                                                                		reset();
                                                                                                		if (b.f(x1))
                                                                                                		{
                                                                                                			for (T2 x2:x1)
                                                                                                			{
                                                                                                				if (b.g(x2))
                                                                                                				{
                                                                                                					async(()->b.query31(x2));
                                                                                                					async(()->b.query32(x2));
                                                                                                				}
                                                                                                				else async(()->b.query33(x2));
                                                                                                				async(()->b.query34(x2));
                                                                                                			}
                                                                                                			async(()->b.query41(x1));
                                                                                                		}
                                                                                                		else async(()->b.query42(x1));
                                                                                                		Future<T3> f=async(()->b.query5(x1));
                                                                                                		complete();
                                                                                                		return f.get();
                                                                                                	}
                                                                                                }
                                                                                                

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

                                                                                                public class Synchronizer
                                                                                                {
                                                                                                	private ExecutorService executorService;
                                                                                                	private List<Future<?>> futures = new ArrayList<Future<?>>();
                                                                                                
                                                                                                	public void complete() throws InterruptedException, ExecutionException
                                                                                                	{
                                                                                                		for (Future<?> f:futures)
                                                                                                			f.get();
                                                                                                		futures.clear();
                                                                                                	}
                                                                                                	
                                                                                                	public void reset()
                                                                                                	{
                                                                                                		for (Future<?> f:futures)
                                                                                                			f.cancel(true);
                                                                                                		futures.clear();
                                                                                                	}
                                                                                                	
                                                                                                	public Future<?> async(Runnable r) throws InterruptedException, ExecutionException
                                                                                                	{ return add(executorService.submit(r)); }
                                                                                                	
                                                                                                	public <T> Future<T> async(Callable<T> c) throws InterruptedException, 
                                                                                                
                                                                                                ExecutionException
                                                                                                	{ return add(executorService.submit(c)); }
                                                                                                	
                                                                                                	private <T> Future<T> add(Future<T> f)
                                                                                                	{
                                                                                                		futures.add(f);
                                                                                                		return f;
                                                                                                	}
                                                                                                }
                                                                                                

                                                                                                Собственно, здесь наглядно видно, что код из ТЗ (теперь уже из двух версий) практически в неизменном виде присутствует как в последнем, так и в первом решении. Код крайне простой.

                                                                                                Код последовательный и понятный. Никаких монад. Никаких лишних приседаний (лифты да разнообразные переопределённые стрелки в смеси с адским деревом математических типов).

                                                                                                Но кроме собственно очевидной из кода простоты, имеем ещё и очевидную простоту получившейся системы в целом. Почему? Потому что все «знатоки» в голос заявляли мне, что мой код синхронный (правда после указания на Java API), а вот автору ТЗ никто не сказал, что и в его коде «что-то не так». Почему не сказал? Да потому что все эти промисы реально сложнее предложенного решения. Ну и оказалось, что никто из «знатоков» не разбирается в том, как работают промисы. А вот в моём коде — разобрались моментально. А всё почему? Да потому, что «делают на монадах не задумываясь» ((с) не моё). А когда видят вменяемое описание происходящего (наследие создателей Java), то привычка делать не задумываясь их не подводит. Потому что наследие стоящее.

                                                                                                Монады (как и JavaScript) прячут от разработчика суть происходящего. Пояснения к функционированию как монад, так и скриптовых конструкций — убогие. Хоть и развелась куча учебников по скрипту, а всё равно подробную цепочку всего происходящего они не дают. Ну а про ФП и говорить нечего — только разбираться с исходным кодом компилятора, иначе — никак. Точнее — иначе опять будет «делают на монадах не задумываясь».

                                                                                                Так в чём же польза от монад? Повторюсь — это всего лишь костыли, делающие программирование на ФЯП более «элегантным» (с некоторых точек зрения). И всё. Более в них смысла нет, одна морока (как и с любыми костылями).

                                                                                                Ну а фанатам ФП остаётся сказать одно — не зазнавайтесь. Ибо молоды вы ещё приводить аргументы в противостотянии ФП — императив. Может лет вам и много, но знаний (и особенно — опыта) у вас очень мало. Императивом занимались реальные монстры, с которыми вы ни в какое сравнение не идёте. Культура разработки на императиве даже не снилась тем университетским лаборантам, которые создали, например, хаскель (и чисто для справки — они не зазнавались). Ну а последовавшие за ними любители и вовсе отметились лишь работами уровня «курсовая» да «дипломная», так откуда у них возьмётся культура разработки? Учитесь у отцов-основателей, забейте своё ЧСВ подальше и изучайте, например, Java, просто потому, что те, кто её создавал, как раз были причастны к той мощной культуре разработки, о которой вы, к сожалению, понятия не имеете. И вот эти отцы никогда бы не стали заявлять что-то вроде «делаю на монадах не задумываясь», потому что, например, синхронизация — это непростая задача, а решать непростые задачи «не задумываясь» — ну вы только что видели, к чему это приводит.

                                                                                                В общем пока всё, умные поймут, дураки разольют нечистоты, ну да к этим «выбросам» мне не привыкать :)
                                                                                                  +5
                                                                                                  А в момент встречи ключевого слова await текущий поток (то есть не тот, в котором выполняет свою задачу обещание) останавливается и ждёт завершения работы

                                                                                                  Нет, не ждет. Если бы это было так, то при любом await код на жсе бы вставал колом, т.к. жс — однопоточный и блокировка потока это блокировка всего жса. Т.е. вы просто не понимаете как работает await. Вы верно сказали — создается промис, вот только никакой блокировки не происходит, а просто в этот промис передается колбек на оставшееся вычисление, иными словами f(await x) = x.then(f). Вы же говорили про колбеки с листенерами? Вот я вам и написал пример про колбеки с листенерами.


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

                                                                                                  Вызывающая сторона-то как раз все и знает. Как делается асинхронный код на жсовских генераторах, можете почитать: https://hackernoon.com/async-await-generators-promises-51f1a6ceede2


                                                                                                  В общем, не повторяя акты из марлезонского балета, сразу приведу код, который (как требуют «знатоки») является реально асинхронным

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

                                                                                                    –6
                                                                                                    Если бы это было так, то при любом await код на жсе бы вставал колом, т.к. жс — однопоточный и блокировка потока это блокировка всего жса

                                                                                                    А как же Worker-ы?

                                                                                                    В общем так — в скрипте вы выдали строго последовательный код, блокировку на IO и параллельное выполнение чего-то другого вы тоже не продемонстрировали. Я же первый раз повторил ваше творчество, ну а второй — уже сделал как надо.

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

                                                                                                    По изменению второго варианта моего кода. Главный вопрос — зачем? Городить какой-то неэффективный шаблон исключительно потому, что спрашивающий привык видеть именно этот шаблон? Это глупо. Приведённый код эффективен, распараллеливает задачу по максимуму (для большего вы не дали информации в своём ТЗ). Ухудшать код ради удовлетворения лично вас у меня желания нет.

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

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

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

                                                                                                    Ладно, надоело что-то доказывать тем, кому просто неинтересно ничего, кроме любимых шаблонов.
                                                                                                      +5
                                                                                                      А как же Worker-ы?

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


                                                                                                      В общем так — в скрипте вы выдали строго последовательный код, блокировку на IO и параллельное выполнение чего-то другого вы тоже не продемонстрировали

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


                                                                                                      Я же первый раз повторил ваше творчество, ну а второй — уже сделал как надо.

                                                                                                      Вы оба раза написали код, который работает не так, как исходный. И сделали это потому, что не знаете как работает async/await. Почему вы с прошлого поста не ознакомились с принципом работы этой конструкции?


                                                                                                      Давайте я вам попробую объяснить, но это последний раз. если у вас есть код async f() { await x(); z = await y(); return z; } то этот код эквивалентен коду f() { return x().then(() => y()).then((z) => z) }. Оно же эквивалентно f(callback) { x(() => y((z) => callback(z))); }
                                                                                                      кто здесь что блокирует? Никто и нигде. Это обычный асинхронный код. Его от вас и требовали. Точнее — от вас требовали код который может быть а может и не быть асинхронным будучи запущенным в разных контекстах.


                                                                                                      Приведённый код эффективен, распараллеливает задачу по максимуму

                                                                                                      Задачу как раз параллелить не надо. Все await должны выполняться строго друг за другом. Но при этом текущий поток не должен блокироваться — т.е. выполнение должно быть асинхронным. Погуглите concurrency vs parallelizm и прекратите позориться.

                                                                                                        –2
                                                                                                        Ваш шаблон называется — хочу как в браузере, и только так, как я привык.

                                                                                                        Объясняю:

                                                                                                        Вы не дали никакой информации по вызывающей ваш код стороне. Даже более — навели туману про HTTP и ещё чего-то там. Значит это может быть что угодно. Но тогда с чего вы взяли, что «что угодно» — это ваш любимый браузер?

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

                                                                                                        Вот цитата из ECMA-262:
                                                                                                        8.4.1 EnqueueJob
                                                                                                        8. Perform any implementation or host environment defined processing of pending.

                                                                                                        Затем вы опять шаблонно (для фронтэнда) описываете используемые понятия, такие как блокировка, синхронность, асинхронность и т.д.:

                                                                                                        >>Так там нету блокировки, и параллельного выполнения нету. Там асинхронный код.

                                                                                                        Объясняю:

                                                                                                        Блокировка, это когда задача останавливается и чего-то ждёт (задача заблокирована). Отсутствие блокировки — это когда задача продолжает выполняться после неблокирующего вызова.

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

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

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

                                                                                                        Далее вы опять настаиваете на своём привычном шаблоне:

                                                                                                        >> Вы оба раза написали код, который работает не так, как исходный. И сделали это потому, что не знаете как работает async/await.

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

                                                                                                        Теперь более высокий уровень:

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

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

                                                                                                        Что нужно для реализации вашего ТЗ в вранианте «инверсия контроля»? Как минимум — нужно знать контекст. То есть когда и в зависимости от чего менять поведение yob-ы. Вы такой информации не привели. Поэтому, опять же, мне остаётся лишь один вариант — менять поведение через переданные мне вызываемые функции, что я и сделал. Не попал в вами любимый шаблон? Ну что-ж, не я же такое ТЗ составил.

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

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

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

                                                                                                        Вот пример:
                                                                                                        f(()=>g(()=>h()));

                                                                                                        В однопоточной среде эквивалентно:
                                                                                                        f();
                                                                                                        g();
                                                                                                        h();
                                                                                                        

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

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

                                                                                                        ЗЫ.

                                                                                                        Я подчеркну — с асинхронностью, в отличии от вас, я привык работать не в браузерах, а в реальных приложениях, требующих высокой производительности, а потому ваш однопоточный подход для меня ничем неинтересен (и да, я его не изучал до уровня эксперта). Но тем не менее, я хоть пытаюсь понять, что вы там себе придумали (какие шаблоны ждёте), ну а вы же даже и не собирались ничего понимать, просто повторяете одно и то же — дай мне как я привык. И как с таким подходом можно работать?
                                                                                                          +4

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

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

                                                                                                            А как вы собираетесь распараллелить зависимые по данным функции?
                                                                                                            Утрированный пример:


                                                                                                            1. сделать запрос на сайт.
                                                                                                            2. обработать ответ.
                                                                                                            3. На основе результата сделать ещё один запрос.

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

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

                                                                                                              Интересно узнать, где вы нашли подобные заявления с моей стороны?

                                                                                                              Посмотрите на функцию из ТЗ и мою реализацию внимательнее. Там есть зависимость от результата первого вызова. И там нет предложенного вами подхода.
                                                                                                                +1

                                                                                                                У вас все вызовы query31 параллельно друг другу выполняются, если executorService это разрешает, что противоречит исходному ТЗ.


                                                                                                                А если executorService однопоточный, то вызовы query31 из разных yoba параллельно выполняться не могут, что тоже противоречит исходному ТЗ.


                                                                                                                Есть, конечно, ещё третий вариант — свой executorService на каждый вызов yoba — но в этом случае всё умрёт под нагрузкой. Конечно, в ТЗ про "не умирать под нагрузкой" ничего сказано не было, но обычно такие вещи подразумеваются.

                                                                                                              +3
                                                                                                              Значит это может быть что угодно.

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


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


                                                                                                              что выполнение задачи может быть исключительно однопоточным.

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


                                                                                                              Это означает, что ваш код синхронизируется на точке await.

                                                                                                              На await ничего не синхронизируется. Я не понимаю, я же вам указал, что именно происходит при await, как он рассахаривается. Зачем вы чушь свою повторяете? Забудьте про await если для вас это чересчур сложная конструкции, вот код, эквивалентный коду с await: f(callback) { x(() => y((z) => callback(z))); }
                                                                                                              Что здесь с чем синхронизируется?


                                                                                                              Мой вариант будет выполнен быстрее на многоядерной платформе

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


                                                                                                              Как минимум — нужно знать контекст.

                                                                                                              Моя функция с yield работает без знания контекста. Благодаря монадам.
                                                                                                              Напоминаю, вы сказали, что монады не нужны — вот и покажите мне аналогичный код без монад.

                                                                                                                –4
                                                                                                                Я всё же повторю ещё раз — попробуйте отказаться от привычных шаблонов, вы в них реально погрязи.

                                                                                                                >> код должен работать с любой вызывающей стороной без изменений самого кода.

                                                                                                                Он и работает. Никаких изменений не требуется.

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

                                                                                                                А вот слово «скажет» — это передача информации. О передаче дополнительных параметров в ТЗ слов нет, так что опять вы в плену шаблона.

                                                                                                                >> Оно, конечно, может быть многопоточным. Никто вас не ограничивал. Главное — оно не должно быть параллельным. Каждый из query выполняется только после того, как предыдущий query вернул результат.

                                                                                                                Для последовательного исполнения задач нужно просто написать последовательность их вызовов. Монады при этом не нужны.

                                                                                                                >> На await ничего не синхронизируется. Я не понимаю, я же вам указал, что именно происходит при await, как он рассахаривается. Зачем вы чушь свою повторяете? Забудьте про await если для вас это чересчур сложная конструкции, вот код, эквивалентный коду с await: f(callback) { x(() => y((z) => callback(z))); }
                                                                                                                Что здесь с чем синхронизируется?

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

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

                                                                                                                Пишите ТЗ понятно и будет вам счастье. Ну а про последовательное исполнение — см. выше.

                                                                                                                >> Моя функция с yield работает без знания контекста. Благодаря монадам.
                                                                                                                >> Напоминаю, вы сказали, что монады не нужны — вот и покажите мне аналогичный код без монад.

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

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

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

                                                                                                                  Ну так сделайте инверсию контроля в своем коде, тогда будет ближе к тому как работает async. Зачем пустая болтовня?

                                                                                                                    –1
                                                                                                                    Там есть вопросы к ТЗ. Что возвращать каждый раз? У автора await-ы стоят перед функциями, которые ничего не возвращают. Ну и вообще я пока не полностью понимаю, чего он хочет. Движок JavaScript ему написать? Ну нет, пусть сам такими играми занимается. Но иначе ведь он опять скажет, что «не так работает».

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

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

                                                                                                                        +2

                                                                                                                        Не видел никого, кому было бы понятно, что вы там написали. У вас получилось написать f(); g(); в запутанном виде с кучей ненужных классов, но у вас так и не получилось сделать работу асинхронной.

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

                                                                                                          Ни на одном из известных мне языков (C#, Python, Javascript, Rust, C++) оператор await не работает описанным вами образом.


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

                                                                                                            +3

                                                                                                            Как в том анекдоте, Вы не читатель, вы писатель.
                                                                                                            Узнайте зачем нужен await и event-loop.

                                                                                                          +3

                                                                                                          Дело не в "ТЗ". Дело в том что Вы не поняли код, который принялись критиковать. Это говорит о том, что Вы понятия не имеете что такое монады, для чего они нужны и как работают.

                                                                                            +1
                                                                                            Как эти конкретные задачи решаются при помощи монад?

                                                                                            Да, решаются. Пример с конфигом например решается по принцифу TF:


                                                                                            struct AnyConfig<M> where M : <> {
                                                                                               workers_count: M<u32>,
                                                                                               latency: M<u32>,
                                                                                               app_url: M<String>,
                                                                                               app_port: M<Port>,
                                                                                               ...
                                                                                            }

                                                                                            тогда тривиально:


                                                                                            type Config = AnyConfig<Option>;
                                                                                            type AppConfig = AnyConfig<Id>;

                                                                                            Никакой копипасты. И какая тут асинхронность?


                                                                                            Ну и далее логично следует очередная простая проблемка — зачем нам выполнять определения из теорий групп и категорий для listener-а? Что это даёт? Где тут нужны ассоциативность, коммутативность и прочее?

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


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

                                                                                            Люди зачем-то учат все эти солиды, паттерны, и прочие акки. Интересно, зачем, ведь это куда сложнее чем 2+2, и то, что в статье.


                                                                                            Представим себе стандартное сложение в любом языке программирования — вместо 2+2 нам пришлось бы работать с чем-то вроде «секвенторов» (должно же звучать «заумно»?), которым пришлось бы передавать «инкрементируемые типы» выполняющие законы ассоциативности, коммутативности и т.д. И да, сторонники такого подхода очень быстро переопределили бы операцию "+"

                                                                                            За аналогиями опять ничего непонятно. Где кого кто нагибает, какая лишняя сложность? Вам дают интерфейс, который позволяет вот например конфиги не копипастить, вы говорите что это сложно. А искать ошибку в 10 "отнеследованных" в ущербно-ориентированном стиле коде — это просто? Вместо ошибки компилятора дебажить многопоточную мутабельную багу — это просто?


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

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


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

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

                                                                                              0

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

                                                                                                +1

                                                                                                Да, но без fmap на M<> почти ничего интересного с такой структурой не сделать.


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

                                                                                                Асинк авейт изобрели 20 лет назад, когда ду нотацию придумали. А потом спустя 10 лет её переизобрел майкросот для IO и так он пошло в мир.

                                                                                                  0

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

                                                                                                    0

                                                                                                    С ней почти всё что надо можно сделать. Что не надо — можно дополнить ручными вызовами к функциям предоставляемым бифунктором, который дает второй мап (для ошибок). А анврап в 99% случаев делать не нужно.

                                                                                                      0

                                                                                                      Какие ошибки если там Id? Вообщем я не знаю о чем вы, пример бы помог.

                                                                                                        0

                                                                                                        Unwrap в смысле .Value? Ну да, он нужен, но только на верхнем уровне который знает про то как достать значение. А в остальном монадичекий интерфейс 100% возможностей типа дает.


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

                                                                                                          0

                                                                                                          Да про unwrap я имел ввиду достать значение из контейнара, в случае Id это действительно просто через .Value что в целом не сложно понаписать его в коде который принимает AppConfig.

                                                                                          0

                                                                                          Практический смысл можно найти, например, в filterM. Хотя и несколько синтетический.

                                                                                      +1
                                                                                      Вы не могли бы языком комутативных диаграм объяснить и функтор, и аппликативный функтор, и все остальное?
                                                                                        0

                                                                                        Интересный вопрос, для функтора могу ответить картинкой которую выше кидал:


                                                                                        image


                                                                                        Придется поверхностно коснуться теории категорий, но постараюсь сделать это нестрашно. Итак, у нас есть две категории, в С объекты a и b, а в D Fa, Fb. В языках программирования единственной категорией которая используется является категория Set. В ней объекты — это множества (говоря иначе — типы), а стрелки между ними — отображения между множествами (говоря иначе — функции из одного типа в другой).


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


                                                                                        Теперь давайте возьмем, что a — это int, b — double, F — IEnumerable.
                                                                                        Тогда эта диагрмма говорит нам о том, что есть некая функция createListA :: a -> IEnumerable<A> и Ff :: IEnumerable<A> -> IEnumerable<B>.


                                                                                        С другой стороны у нас есть функция f :: A -> B и createListB :: b -> IEnumerable<B>.


                                                                                        Диаграмма коммутирует, а это означает, что неважно, по какому пути мы пойдем, мы придем в одну итоговую точку. То есть


                                                                                        createList(new A()).Map(f) === createList(f(new A())


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


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


                                                                                        static IEnumerable<T> CreateList<T>(T x) => new[] { x, x, x };

                                                                                        По типам мы сошлись, эти законы будут выполняться, но это — не pure.


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

                                                                                          +1

                                                                                          Вижу картинку и чую руку Бартоша Милевского. У него на сайте взяли?


                                                                                          Я по образованию математик, а по опыту работы — разработчик С++. Поэтому мне понятны либо коммутативные диаграммы, либо С++ код. С# код разумею с трудом.


                                                                                          Функтор — просто морфизм между категориями, переводящий обьекты в обьекты, морфизмы между ними — в морфизмы в образе класса морфизмов. Что, собственно, ваша (или таки Бартоша?) картинка и показывает.


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


                                                                                          Аппликативный функтор что делает?

                                                                                            0
                                                                                            Вижу картинку и чую руку Бартоша Милевского. У него на сайте взяли?

                                                                                            Его статьи в книжку скомпилировали, по моему скромному мнению весьма неплохую: https://github.com/hmemcpy/milewski-ctfp-pdf


                                                                                            Я по образованию математик, а по опыту работы — разработчик С++. Поэтому мне понятны либо коммутативные диаграммы, либо С++ код. С# код разумею с трудом.

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


                                                                                            Монада:
                                                                                            image


                                                                                            image
                                                                                            Где ню — это pure, а мю — это джоин:


                                                                                            join :: (Monad m) => m (m a) -> m a
                                                                                            join x = x >>= id

                                                                                            Для аппликатива такой нет, но если бы меня попросили нарисовать, это выглядело бы как-то так:



                                                                                            Вот тут можно почитать что сам Бартош пишет. TLDR:


                                                                                            We can define the categorical version of the Haskell’s applicative functor as a lax closed functor going from a closed category C to Set. It’s a functor equipped with a natural transformation:

                                                                                            f (a => b) -> (f a -> f b)
                                                                                            where a=>b is the internal hom-object in C (the second arrow is a function type in Set), and a function:

                                                                                            1 -> f i
                                                                                            where 1 is the singleton set and i is the unit object in C.

                                                                                            (всех кто читает это просьба не пугаться, это объяснение не для программистов :) )

                                                                                              0

                                                                                              Спасибо.


                                                                                              Бартоша я начал читать, но что-то он мудрит. Почитаю-ка я Ротмана про моноидные категории, оттуда ноги растут.


                                                                                              А что понимается под Т^2 и Т^3 в диаграммах?

                                                                                                0

                                                                                                Бартош на мой взгляд максимально понятно объясняет (с примерами на плюсах в том числе, кстати). Остальных пробовал — нифига не понял.


                                                                                                T^2 T^3 — это вложенные генерики, T^3 int === T<T<T<int>>> например

                                                                                            –1
                                                                                            Всегда когда читаю про морфизмы из A в B чувствую затруднения, но в тоже время я знаю что, бинарный сдвиг в право на 1 разряд эквивалентен делению на 2 и не нужно не каких коробок, облаков и стрелок. Виртуальный функтор)
                                                                                            зы а основание логарифма это не функтор?
                                                                                          +1
                                                                                          Посмотрите, сколько мусора натащил сишарп

                                                                                          Возможно сказывается мой длительный разрыв с C#, но мне кажется, что с конструкциями типа
                                                                                          public extension IdMonad of Id : Monad<Id>
                                                                                          {
                                                                                              static IdMonad<A> Pure<A>(A a) => new Id<A>(a); // просто создаем обертку
                                                                                              static IdMonad<B> Bind<A, B>(IdMonad<A> ta, Func<A, IdMonad<B>> mapInner)  =>
                                                                                                  mapInner(ta.Value);
                                                                                          }
                                                                                          

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