Как стать автором
Обновить

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

of: <A>(a: A): Task<A> => async () => a

Кажется, этот код поломается в случае когда a — Thenable (т.е. имеет метод then)


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

Ваша правда. Но смысл использования thenable в функциональном коде вместе с тасками для меня неясен. Подразумевается, что весь нечистый код (в т.ч. все сторонние промисы/thenables) остается на границе домена приложения.
Можете привести пример, где бы могло быть полезным в функциональных программах?

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

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

Вариант 1: обернуть значение внутреннего промиза.


type Task<A> = () => Promise<{ value: A }>;

of: <A>(a: A): Task<A> => () => Promise.resolve({ value: a});

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

Ах да, вот вспомнил вариант 3. Главное достоинство ленивых задач — в том, что они выполняются всегда ровно с 1 ожидающим подписчиком. Соответственно, делать ленивую задачу через обычную — избыточно, и проще всего её сделать вот так:


type Task<T> = (callback: (value: T) => void) => void;

const URI = 'SimpleTask';
type URI = typeof URI;

declare module 'fp-ts/HKT' {
  interface URItoKind<A> {
    [URI]: Task<A>;
  }
}

const monadSeq: Monad1<URI> = {
    URI,
    of: <A>(value: A): Task<A> => cb => cb(value),
    map: <A, B>(
        taskA: Task<A>, 
        transform: (a: A) => B
    ): Task<B> => cb => taskA(value => cb(transform(value))),
    chain: <A, B>(
        taskA: Task<A>, 
        transform: (a: A) => Task<B>,
    ): Task<B> => cb => taskA(value => transform(value)(cb)),
    ap: <A, B>(
        taskAB: Task<(a: A) => B>, 
        taskA: Task<A>
    ): Task<B> => cb => taskA(valueA => taskAB(valueAB => cb(valueAB(valueA)))),
};

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


const monadPar: Monad1<URI> = {
    …,
    ap: <A, B>(
        taskAB: Task<(a: A) => B>, 
        taskA: Task<A>
    ): Task<B> => cb => {
        let valueA : A;
        let valueAB : (a: A) => B;
        let state = 0;

        taskA(x => { valueA = x; advance(); })
        taskAB(x => { valueAB = x; advance(); })

        function advance() {
            if (++state == 2) {
                cb(valueAB(valueA));
            }
        }
    },
};
НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь
У монад же весьма ограниченное полезное применение — убрать под общий контекст множество { функций }. Даже список [фунций] с общим контекстом сложно использовать как locator, а только как жесткий pipe.

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

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

Вместо TaskEither не проще ли было бы объявить трансформер EitherT и применить его к Task, чтобы получить автоматически и функтор TaskEither, и аппликатив, и монаду?

Многого от трансформеров не выиграть из-за слабости системы типов Typescript.


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


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

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

Promises and compromises. Инженерные задачи, в отличие от чисто математических и общегуманитарных, всегда решаются в ограничениях. Было бы намного больше пользы, если б поклонники FP сели и написали свой правильный монадический JS с тайпклассами и эндофункторами, вместо того чтобы хейтить по мелочам и лепить костыли. С 2013 на то была ведь куча времени. Интересно, почему до сих пор нет?


Насколько я понимаю все упирается в производительность и память. Для монадических промисов нужно аккуратно трекать весь стек вызовов (в энергичном языке за ленивость приходится платить), либо отказаться от стека совсем в пользу чего-то более легковесного типа call/cc. Обычные же промисы можно спокойно схлопывать — это открыло возможности для оптимизаций https://v8.dev/blog/fast-async

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


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

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


Например, сейчас Promise.resolve(Promise.resolve(42)) == Promise.resolve(42) какой-бы вложенность у них не была. Для поддержки видимости монадических законов в малом нужно сохранять всю цепочку. Причем без каких-либо гарантий выполнения этих законов в большем, ведь возможность мутаций в JS ни кто не отменял.


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

Было бы намного больше пользы, если б поклонники FP сели и написали свой правильный монадический JS с тайпклассами и эндофункторами, вместо того чтобы хейтить по мелочам и лепить костыли. С 2013 на то была ведь куча времени. Интересно, почему до сих пор нет?

PureScript?


Да и Haskell, например, спокойно компилируется в JS. Вот только недавно на хабре статья была.

Использование JS как универсаного ассемблера для специализированных языков это нормальная практика. Попробовать другой подход как разминку для ума, потрогать граничные кейсы, чтобы лучше понять дизайн языка, prod/cons — это все здорово. Мне не понятно зачем тащить в продакшн или стандарт такие "улучшения" — с той стороны, для которой этот язык явно не предназначен. Напишите свой язык с синтаксисом максимально близким к JS, если важна именно привычная внешняя атрибутика.

Я хоть и за ФП обеими руками, но вот не понимаю зачем тянуть из Haskella понятия совершенно чуждые для TS?


Kоторая позволит сделать промис законопослушным

Зачем? Эти законы в TS не существуют, они только у вас в голове. Нет там такого понятия как чистые и не чистые функции. С точки зрения Haskell и фундаментального ФП, в TS, Scala, Elm, F#, etc не существует такого понятия как чистые функции на уровне языка. Следовательно, это все излишества.

В хаскеле можно сделать так:


babah :: Int
babah = const 42 $! unsafePerformIO $ putStrLn "babah"

Значит ли это, что в хаскеле чистые функции также не существуют на уровне языка?


Второй момент. Что в в скала-сообществе, что в TS-сообществе распространена практика использования безопасного подмножества языка — т.е. такого подмножества, которое минимизирует негативные побочные эффекты. Для скалы это была инициатива Scalazzi, сейчас Typelevel stack и ZIO stack. Для TS — fp-ts и схожие с ним проекты (тот же Effect-TS). Использование абстракций из них позволяет писать безопасный, производительный, просто рефакторящийся код. Я не могу согласиться, что это излишества.

В хаскеле можно сделать так

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


Значит ли это, что в хаскеле чистые функции так же не существуют на уровне языка?

Я же вам об обратном и говорю


Для скалы это была инициатива Scalazzi

Категорически с вами не согласен, инициатива Scalaz или Cats зиждиться на восполнение пробелов Scala 2, а именно функ. композиции, union types, и прицепом монад. Честно вам скажу, я на Scala пилю примерно с 2009, контор которые в продакшене используют Scalaz я не встречал. Я бы даже сказал что народ от Scalaz лица воротит что черт от ладана, из-за этого очень сложно продвигать такие няшные и нужные библиотеки как cats. На мой взгляд Scalaz у большинства вызывает негодование нежели желание им пользоваться.


тот же Effect-TS

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


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

Совершенно не согласен с таким термином, в оригинале pure/unpure functions не имеет ничего общего с безопасностью. ФП это не про это. Вас просто люди не поймут если будете подменять понятия. На мой взгляд.

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

Что в них негативного и относительно чего?

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

Я считаю, что да, обязательно. Если говорить про ФП, то понимание, как под капотом реализованы те или иные абстракции, даёт уверенность в их использовании и простоту передачи знаний. Поэтому я люблю давать упражнения на написание своих Option/Either/Task и в дальнейшем ZIO-like конструкций своим ученикам из проектов, на которых я работаю. К сожалению, за время работы как на бэке, так и во фронте я видел очень много условных «верстальщиков на реакте», которые не понимают, как работает на базовом уровне язык, и во что превращаются в рантайме те или иные абстракции. Мне думается, что такого стоит избегать, и что умение разобрать досконально в той или иной технологии это то, что отличает инженера-программиста от кодера.

Пользуясь случаем, спрошу: продолжения (continuation) правда без setjmp/longjmp в Си не делаются, или мне ещё подумать?)

Если вы пишете на Scala или F# то их байт/ИЛ код можно лего декомпилировать в Java/C# и смотреть что там за творчество выдает компилятор. На Scala так вообще инструментарий побогаче будет (GenBCode + --Xprint:cleanup). С Clojure тоже, вроде бы все ОК в этом плане.

JS не в си «разворачивается»
Более того, как он «разворачивается» — зависит от движка и от его версии
Вопрос был не про JS (haskell, lisp, prolog), а про необходимость в принципе уметь представить или переписать высокоуровневые абстракции на более низком уровне.

Переписать или "как будет выглядеть" ?

Хотя бы раз стоит переписать, а то получится как у меня в начале фриланса — в голове и на бумажке всё просто, а руками писать неделю вместо суток :(
Мы в теме TS, так что )
Я тему понимаю больше как «принесём побольше ФП туда, где его изначально особо не планировалось» :)
Вопрос был не про JS

Мы в теме TS, так что )

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