Комментарии 33
Пример на TypeScript, показанный выше, это демонстрация реализации более старой версии предложения
Насколько я знаю, для стандарта никогда не было такого предложения как это реализовали в ts. На момент появления декораторов в ts для js предлагался вариант с initializer-ом.
case можно группировать.
Если писать классический бекенд, то маловероятно что это будет нужно. Но если это телеграм-бот какой-нибудь — уже более. А если какой-нибудь биржевой торговый робот — уже вполне себе так. Ну и всякие сервисные штуки что раз в какое-то время что-то делают и могут иметь стейты. В общем применений больше чем может казаться.
Рекомендую прием «банк стратегий»
const STRATEGY = {
'sum' : (a,b)=>a+b,
'dif': (a,b)=>a-b,
'mul': (a,b)=>a*b,
'div': (a,b)=>a/b
}
// какая-то переменная, определяющая выбор
let type = 'div';
const a= 15;
const b = 3;
// вместо switch-case - одна строка
const strategy = STRATEGY[type];
const result = strategy(a,b);
Банк стратегий выносит многострочность кода из места применения в место определения. Можно оформить отдельной библиотекой. Можно спокойно расширять.
Я сам часто использую switch-case, но когда эта конструкция разрастается — рефакторинг в любом случае потребуется.
PS: чем еще мне не нравится switch… — в JS нельзя завести let, const внутри отдельного case.
А вот по поводу let, const — всё же можно:
const a = 0;
switch (true) {
case true: {
const a = 1;
console.log(a);
break;
}
case false: {
const a = 2;
}
}
console.log(a);
Блоки нам в этом помогут.
PS: чем еще мне не нравится switch… — в JS нельзя завести let, const внутри отдельного case.
Можно. Достаточно отдельный блок объявить.
function test(val) {
const b = 1;
let r = 0;
switch (val) {
case 1: {
let a = 1;
const b = 2;
r = a + b;
break;
}
case 2: {
let a = 2;
const b = 3;
r = a + b;
break;
}
default: {
let a = 3;
const b = 4;
r = a + b;
break;
}
}
return r;
}
С радостью бы посмотрел на вашу реализацию этого кода из react-dom на if/else.
Вот ещё пример из vue или пример из angular на ts. Такая конструкция есть в практически любом языке, так что рекомендую вам так же написать вот этот switch из игры VVVVVV на if/else конструкциях
Вспоминается 10-е правило Гринспена.
Когда увидел "do expressions" в душе проснулась наивная надежда увидеть first class monads. Ха. Ага. Уже, ща.
Увы.
А приведите пример на языке знакомом вам, а также как бы вы представили это на javascript?
Никогда не поздно написать свой велосипед и скормить его babel.
Вкратце, под этот юзкейс попадают любые генераторы, которые используются для более точного контроля исполнения, чем тот, который могут дать async-функции. Из того, что приходит в голову:
- отмена выполнения
- синхронизационные примитивы, барьеры
- автоматическая параллелизация и построение графа зависимостей
- ограниченная параллелизация, троттлинг, дебаунс
- software transactional memory
- автоматический повтор неудачных команд в контексте cqrs command handler
- аудит трейс выполнения
- реактивное программирование
- всякие эффекты из библиотек типа effectful.js
- работа со значениями Observable напрямую
- всякие "обычные" монады типа
Maybe
,Either A
,Reader
,Writer
,State
,IO
, и т.д.
Увы, так как генераторы, это, по сути, лишь умный хак, а не полноценная do
-нотация, все монады и часть перечисленных выше кейсов на чистых генераторах сделать нельзя. Например, недетерминированные вычисления (монада List
) на генераторах могли бы выглядеть как-то так (пример намеренно усложненный, чтобы показать варианты использования):
// чистая функция
const str = (x: number) => x.toString();
// монадическая функция над абстрактной монадой
const addMult = (xm: Monad<number>,
ym: Monad<number>,
zm: Monad<number>): Monad<number | null> => Monad.do(function*() {
const x: number = yield xm;
if (x === 1) {
return pure(null);
}
const y: number = yield ym;
const z: number = yield zm;
const temp1: number = x + y;
const temp2: Monad<number> = pure(temp1 * z);
return temp2;
});
// монадическое выражение над конкретной монадой
const result: List<string> = List.do(function*() {
const value: number | null = yield addMult(
[1, 2, 3],
[4, 5, 6],
pure(10),
);
if (value === null) {
return [];
}
return pure(str(value));
});
expect(result).to.equal(['60', '70', '70', '80', '80', '90']);
Увы, чтобы такой генератор сконвертировался в монаду, удовлетворяющую всем монадическим законам, рантайм должен дать возможность сериализовать состояние генераторов в произвольной точке, клонировать их, перезапускать с произвольной точки и т.д. Без этого не выйдет сделать так, чтобы x = yield [1, 2, 3]
вернул управление в функцию три раза, а y = yield [4, 5, 6]
, соответственно, шесть раз (т.к. в трех случаях до него выполнение не дойдет). С этим есть практические сложности.
Полноценная do-нотация избавила бы рантайм от ограничений, накладываемых на генераторы спецификацией, и позволила бы control flow, невозможный в генераторах, без нарушения обратной совместимости. Возможно, это также позволило бы оптимизации, специфичные для монадического кода. Асинхронные функции и генераторы можно было бы также выразить через фреймворк монад, попутно исправив ошибку дизайна и сделав Promise
полноценной монадой.
Как бы это на мой взгляд выглядело? Наверное, если рассматривать нечто идиоматически близкое к JavaScript, а не к Haskell, и учитывая в языке отсутствие тайпклассов, то, я думаю, как-то так:
// чистая функция
const str = (x: number) => x.toString();
// монадическая функция над абстрактной монадой
const addMult = (xm: Monad<number>,
ym: Monad<number>,
zm: Monad<number>): Monad<number | null> => do (Monad) {
const x: number = run xm;
if (x === 1) {
return pure null;
}
const y: number = run ym;
const z: number = run zm;
const temp1: number = x + y;
const temp2: Monad<number> = pure (temp1 * z);
return temp2;
});
// монадическое выражение над конкретной монадой
const result: List<string> = do (List) {
const value: number | null = run addMult(
[1, 2, 3],
[4, 5, 6],
pure 10,
);
if (value === null) {
return [];
}
return pure str(value);
});
expect(result).to.equal(['60', '70', '70', '80', '80', '90']);
Новые ключевые слова — do
, run
и pure
. Кейворд do
синтаксически выражается аналогично while
или with
, а run
и pure
синтаксически похожи на yield
или await
. Пример выше рассахаривался бы в следующий код:
// чистая функция
const str = (x: number) => x.toString();
// монадическая функция над абстрактной монадой
const addMult = (xm: Monad<number>,
ym: Monad<number>,
zm: Monad<number>): Monad<number | null> => {
return Monad.bind(xm, (x: number) => {
if (x === 1) {
return Monad.pure(null);
}
return Monad.bind(ym, (y: number) => {
return Monad.bind(zm, (z: number) => {
const temp1: number = x + y;
const temp2: Monad<number> = Monad.pure(temp1 * z);
return temp2;
});
});
});
};
// монадическое выражение над конкретной монадой
const result: List<string> = List.bind(addMult(
[1, 2, 3],
[4, 5, 6],
List.pure(10),
), (value: number | null) => {
if (value === null) {
return [];
}
return List.pure(str(value));
});
expect(result).to.equal(['60', '70', '70', '80', '80', '90']);
В рантайме Monad.bind
использовало бы .bind
от того класса монады, экземпляром которого является аргумент. Monad.pure
возращало бы абстрактный контейнер, который бы автоматически разворачивался вызывающей стороной и приводился к List.pure
.
В do (List) {}
, идентификатор List
— это обычное JS-выражение, которое бы эвалюировалось в значение, имеющее методы bind
, pure
и опционально catch
(для перехвата throw
).
Асинхронные do-выражения, следовательно, могли бы выглядеть как
const whatever: Promise<Data> = do (Promise) {
let foo: Foo = run fetch("/some/foo");
let bar: Bar = run fetch("/some/bar");
return pure synchronouslyCombineStuff(foo, bar);
}
и были бы аналогичны
const whatever: Promise<Data> = (async () => {
let foo: Foo = await fetch("/some/foo");
let bar: Bar = await fetch("/some/bar");
return synchronouslyCombineStuff(foo, bar);
})();
за тем исключением, что заменив Promise
на какой-нибудь DepGraphParallelPromise
, можно было бы заставить foo
и bar
скачиваться параллельно, т.к. монада могла бы проанализировать дерево зависимостей используемых идентификаторов.
Что еще обычно из примеров показывают, Maybe
? В таком случае, для сниппета
const result = do (Maybe) {
const x = run foo();
const y = run bar(x);
return pure y;
}
Если foo
вернет Nothing
, то до bar
управление даже не дойдет — т.е. рассахаренный код просто не вызовет то, что будет передано в Maybe.bind(foo(), x => { /* вот этот код не будет вызван */ })
. Если же foo
вернет Just<что-нибудь>
, то это что-нибудь
будет присвоено x
.
В общем-то, наверное, кроме, на мой взгляд, богатых возможностей по организации кода, такая do
-нотация была бы еще и тайпчекер-френдли, т.к. можно было бы выводить типы используемых в run
-выражениях монад по контексту do
и ругаться, если кто-то пытается run
ить List
в контексте Maybe
или наоборот; или пытается вернуть из do-выражения не-монадическое значение — например, случайный голый return x
вместо return pure x
, если x
— не монада.
Конечно, это на данный момент лишь поверхностные фантазии, но я надеюсь, что это дало небольшое понимание того, как я бы это видел. Если вдруг у вас есть вопросы, идеи, пожелания, то я постараюсь ответить.
Был бы благодарен за аргументацию минусов.
Скорее всего, не осилили многа букф. Держи плюс за труд.
Во-первых, монада — не тип. Соответственно, писать в одной и той же конструкции do (List) {
и do (Monad) {
очень странно.
Во-вторых, не слишком ли много хитрых ключевых слов?
В-третьих, точно ли имитация императивного кода на монадах является хорошей идеей? В императивном коде можно сделать много странных вещей, которые в ФП не предусмотрены.
В-четвертых, у нас всё-таки язык со слабой динамической типизацией. Что будет, если пропустить pure? Вот мы пишем — return synchronouslyCombineStuff(foo, bar);
, как это должно отработать?
В-пятых, Promise — не совсем монада.
Во-первых, монада — не тип. Соответственно, писать в одной и той же конструкции do (List) { и do (Monad) { очень странно.
Почему же не тип? Это тайпкласс — т.е. интерфейс, в переводе на "общечеловеческий". В конкретно этой идее, аргументом do
передается какой-то объект, который содержит реализации bind
, return
и fail
, которые для уменьшения терминологического разрыва можно назвать bind
, pure
и catch
. bind
вообще можно было бы назвать then
, если бы не существующий then
в Promise
.
Т.е. запись do
вполне могла бы выглядеть как что-то эдакое:
do ({
bind(xs, f) { return xs.map(f).flat(); },
pure(x) { return [x]; },
[Symbol.hasInstance](x) { return Array.isArray(x) }
}) {
...
}
Но передавать готовый инстанс куда удобнее, как мне кажется.
Если же вас смущает слово Monad
— представьте, что это Free
. Мне показалось, что если я назову это Free
, то из-за смысловой перегруженности термина "free" и ассоциациями с управлением памятью это вызовет путаницу. Тогда как Monad<T>
это вроде как "экземпляр какой-то монады", т.е. "что-то, что реализует типокласс монады", т.е. "что-то, что имеет методы bind
и pure
". Подставьте вместо Monad любую free-монаду, которую может реинтерпретировать вызывающая сторона в своем контексте, и это будет то, что я хотел сказать.
Во-вторых, не слишком ли много хитрых ключевых слов?
Ну, ни yield
, ни await
не отражают того, зачем это нужно, а библиотечными функциями это сделать невозможно по ряду причин:
- эти функции должны вызываться исключительно из блока
do
. Синтаксической ошибкой сделать вызов функции не в том месте нельзя, плюс функции оборачиваются, переименовываются, переопределяются и т.д. Это плохо. - эти функции должны получать доступ к контексту
do
. Это подразумевает введение магических аттрибутов в scope, и это плохо. - эти функции должны иметь возможность изменять control flow вызывающей стороны, что делает их очень сильно магическими — т.е. как в примере с List, они должны возвращать управление несколько раз подряд.
- если использовать
yield
илиawait
как ключевое слово вместоrun
, это не даст возможности заключатьdo
-блоки в генераторы и асинхронные функции.
Т.е. run
не может быть функцией по той же причине, по которой не могут быть функциями await
и yield
. Если есть лучшие кандидаты на переиспользование ключевых слов — милости прошу, вопрос именования, на мой взгляд, как раз не является самым важным.
Если же ваше опасение вызвано тем, что кейворды будут мешать совместимости с существующим кодом — эта проблема уже была решена с введением async
и await
, и они могут использоваться как обычные идентификаторы, названия методов и т.д., проблемой это не является.
В-третьих, точно ли имитация императивного кода на монадах является хорошей идеей? В императивном коде можно сделать много странных вещей, которые в ФП не предусмотрены.
Это не имитация императивного кода. Это императивный код является неполным, ограниченным приближением монадических вычислений. Поэтому, например, описанные выше недетерминированные вычисления не выйдет сделать так же просто и лаконично, как это было бы с do-нотацией. Аналогично, в императивном коде не выйдет работать со значениями, извлеченными из Observable
, тоже по той причине, что на один условный yield
приходится много значений. То, что с монадами можно сделать много странных вещей, которые невозможны для простого императивного кода — это неизбежное следствие введения более мощного инструмента моделирования.
Я привел выше пример того, во что рассахариваются указанные конструкции, и на мой взгляд, конвертация достаточно однозначна и лаконична, чтобы исключить возможные недопонимания и выстреливание себе в ногу.
Если у вас есть обратные примеры, я был бы рад их вместе рассмотреть. Но в общем случае — если вам не нужны монады, вы не используете монады. Если вам нужны монады, вы их используете без цепочки уродливых биндов, в синтаксическом шуме которых теряется суть выполняемых действий.
В-четвертых, у нас всё-таки язык со слабой динамической типизацией. Что будет, если пропустить pure? Вот мы пишем — return synchronouslyCombineStuff(foo, bar);
, как это должно отработать?
Так как bind()
любой монады ожидает, что его коллбэк вернет значение, уже обернутое в монаду того же типа, то если мы напишем что-то типа do (MONAD) { return VALUE; }
, рантайм должен бы выполнить аналог VALUE instanceof MONAD
, который, в свою очередь, вызовет, возможно, MONAD[Symbol.hasInstance](VALUE)
, который сможет, например, отличить List
от ZipList
. Если проверка будет провалена, было бы логичным выбросить TypeError
, как это происходит в любых других ситуациях, где ожидаемый тип отличается от фактического.
Тайпчекеры типа TypeScript смогут статически вывести несовместимость типов и дать диагностику еще на стадии компиляции.
В-пятых, Promise — не совсем монада.
Так и есть, именно поэтому я написал про "попутно исправив ошибку дизайна и сделав Promise полноценной монадой". Нет никаких препятствий для того, чтобы сделать Promise монадой с минимальными изменениями. Если вам интересно, я могу написать реализацию условного MonadPromise
, который бы сохранял совместимость с экземплярами ES6 Promise
и при этом удовлетврял монадическим законам.
Почему же не тип? Это тайпкласс — т.е. интерфейс, в переводе на "общечеловеческий".
Во-первых, потому что у монад kind не *
, а * → *
. Во-вторых, интерфейсы в Javascript не представлены, они существуют только "в уме" и в документации.
Это не имитация императивного кода. Это императивный код является неполным, ограниченным приближением монадических вычислений [...]
Не важно чем является императивный код. Важно что он может неаккуратной строчкой поломать все монады.
Если проверка будет провалена, было бы логичным выбросить TypeError, как это происходит в любых других ситуациях, где ожидаемый тип отличается от фактического.
Было бы логичным придерживаться существующих в стандартной библиотеке JavaScript соглашений.
Так и есть, именно поэтому я написал про "попутно исправив ошибку дизайна и сделав Promise полноценной монадой". Нет никаких препятствий для того, чтобы сделать Promise монадой с минимальными изменениями.
Такое препятствие есть: обратная совместимость.
Во-первых, потому что у монад kind не , а → *.
Ну как вы верно подметили, в JS "настоящие" типы существуют только в уме. Я не думаю, что у тайпчекеров даже возникнет необходимость поддерживать HKT в полном объеме, потому что речь выше не идет о полностью абстрактной do-нотации, а о do-нотации с указанием конкретного экземпляра монады (пусть даже это будет Free
) и о правилах реинтерпретации Free.
Не важно чем является императивный код. Важно что он может неаккуратной строчкой поломать все монады.
Ну так и в Haskell можно объявить инстансом монады нечто, что не выполняет монадические законы. Можно и в записи биндами замкнуться на внешнюю переменную и мутировать ее. Можно и геттеры с сайдэффектами делать. Компилятор — не нянька, всего не предусмотришь. Чтобы подобных вещей недопустить, и нужно участие программиста.
Было бы логичным придерживаться существующих в стандартной библиотеке JavaScript соглашений.
Они в таких случаях предусматривают выброс TypeError
, насколько мне известно. Если это не так, исправьте меня, пожалуйста.
Такое препятствие есть: обратная совместимость.
Приведите пример, как введение нового типа MonadPromise, способного биндиться от Thenable (или расширение существующего встроенного Promise методами из интерфейса монад), нарушит обратную совместимость существующего кода.
let x = do { if (foo()) { f(); } else if (bar()) { g(); } else { h(); } };
Кошмар какой-то. На что только не идут люди, чтобы не писать много мелких функций с логичными названиями ...
Что мешает сделать отдельную функцию и передать ее аналогично в функцию высшего порядка? А с типизацией в ts в таком случае что делать? Ладно там допустим лямбда выражения пропихнули, в них хоть смысл есть, за счет захвата окружающего контекста, но тут? В js и так достаточно проблем с читаемостью кода, ts немного помог поубавить градус этому, но опять все катится в одно место. Надеюсь они откажутся от этой идеи
Революционные возможности? Не сказал бы. Декораторы и контексты для асихронных задач давно есть в питоне, do-выражения были бы не нужны, если бы язык был больше ориентирован на выражения (а современные языки: Kotlin, Scala, Rust такими и делают), да и паттерн-матчингом уже никого не удивишь.
а строгая типизация будет?
ну типа все проблемы при работе с js так или иначе ведут к отсутствию строгой типизации — система типов сделала бы гораздо удобнее и intellisense и тестирование и обмен данными. мне лично этого больше всего не хватает в js
из-за беспорядочного определения классов среда не может подхватить подсказки, и ты никогда не знаешь какие методы у объекта. может сделать чтобы это было можно включать/выключать хотябы?
4 революционных возможности JavaScript из будущего