Pull to refresh

Comments 33

Пример на TypeScript, показанный выше, это демонстрация реализации более старой версии предложения

Насколько я знаю, для стандарта никогда не было такого предложения как это реализовали в ts. На момент появления декораторов в ts для js предлагался вариант с initializer-ом.

Не понимаю для чего была создана конструкция switch. Более бесполезной конструкция в Javascript наверно не существует. Нет ни одной ситуации, где она была бы лучше или компактнее, чем if / else
Классическое применение — проход по enum'у, через свитч выглядит на порядок аккуратнее. Ну и при нетривиальном использовании break и default можно всякие штуки творить, которые вы через if писать задолбаетесь просто, а вместо аккуратного списка может получиться вложенное ветвистое нечто.
Добавлю к вышеперечисленному — стейт машины.
Если писать классический бекенд, то маловероятно что это будет нужно. Но если это телеграм-бот какой-нибудь — уже более. А если какой-нибудь биржевой торговый робот — уже вполне себе так. Ну и всякие сервисные штуки что раз в какое-то время что-то делают и могут иметь стейты. В общем применений больше чем может казаться.
// стейт

Рекомендую прием «банк стратегий»

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 конструкциях

UFO just landed and posted this here

Когда увидел "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 не отражают того, зачем это нужно, а библиотечными функциями это сделать невозможно по ряду причин:


  1. эти функции должны вызываться исключительно из блока do. Синтаксической ошибкой сделать вызов функции не в том месте нельзя, плюс функции оборачиваются, переименовываются, переопределяются и т.д. Это плохо.
  2. эти функции должны получать доступ к контексту do. Это подразумевает введение магических аттрибутов в scope, и это плохо.
  3. эти функции должны иметь возможность изменять control flow вызывающей стороны, что делает их очень сильно магическими — т.е. как в примере с List, они должны возвращать управление несколько раз подряд.
  4. если использовать 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 немного помог поубавить градус этому, но опять все катится в одно место. Надеюсь они откажутся от этой идеи

тож самое, что и с iife.
Как только захотелось его использовать, лучше сделать extract method

Такими темпами может когда-то даже добавят метод для удаления единичного элемента из массива (не через ужасный splice) и итератор по объектам с получением ключа и значения (не через отдельно болтающийся многословный Object.entities и не через for… in, который даёт только ключ и требует отсекать собственные свойства). Ну и заодно deep copy на уровне языка.

Революционные возможности? Не сказал бы. Декораторы и контексты для асихронных задач давно есть в питоне, do-выражения были бы не нужны, если бы язык был больше ориентирован на выражения (а современные языки: Kotlin, Scala, Rust такими и делают), да и паттерн-матчингом уже никого не удивишь.

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

> все проблемы при работе с js так или иначе ведут к отсутствию строгой типизации
>> все проблемы

Прямо вот все до единой?
Теоретически, Вы можете получить некоторое подобие с помощью контрактов, брендированных типов и глубоких проверок зависимостей. Практически — это будет не очень хорошо работать.
Зачем строгая типизация в Js? Как я и написал, практически — это будет не очень хорошо работать.
Непонятно, почему по-сути всего-то новый синтаксический сахар назван «революционными возможностями». В JS этого сахара уже в излишке…
Как разработчика, однажды унаследовавшего от предыдущей команды кодовую базу на Scala, меня сильно огорчит, если pattern matching появится в прочих языках. Дело в том, что разработчики, имея такой инструмент на руках, начинают использовать его где надо и где не надо. По итогу, у вас вся кодовая база усеяна case'ами, которые очень сложно анализировать. Например, вы видите иерархию классов без единого использования полиморфизма и не понимаете, зачем она вообще нужна. Оказывается, вместо определения полиморфного метода, кто-то написал case по всем подклассам иерархии. Или, допустим, вы хотите добавить еще один подкласс и видите, что сделать это очень легко, т.к. в реализации нуждается лишь один-два метода. Но потом ничего не работает, потому что в коде миллион case'ов с default-реализацией, которые ваш подкласс, само собой, не учитывают. И найти их тяжело. Возможно, дело в привычке, но мне идея не нравится.
Only those users with full accounts are able to leave comments. Log in, please.