company_banner

4 революционных возможности JavaScript из будущего

Автор оригинала: Arek Nawo
  • Перевод
JavaScript, с момента выхода стандарта ECMAScript 6 (ES6), быстро и динамично развивается. Благодаря тому, что теперь новые версии стандарта ECMA-262 выходят ежегодно, и благодаря титаническому труду всех производителей браузеров, JS стал одним из самых популярных языков программирования в мире.

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



Но это не значит, что нечто исключительно новое в итоге в языке не появится. Собственно говоря, об этом я и хочу сегодня рассказать. Мне хотелось бы поговорить о 4 возможностях, которые, в перспективе, можно будет назвать революционными. Они находятся сейчас на разных стадиях процесса согласования предложений. Это, с одной стороны, означает, что мы можем никогда их в JavaScript и не увидеть, а с другой стороны — наличие подобных предложений даёт нам надежду на то, что мы их, всё же, когда-нибудь встретим в языке.

Декораторы (decorators)


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

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

Взгляните на следующий пример:

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

Тут я решил действовать осторожно и привёл пример использования TypeScript-декораторов. Сделал я это, преимущественно, для того, чтобы показать общую идею. В вышеприведённом фрагменте кода мы создали декоратор sealed и применили его к классу Greeter. Как несложно заметить, декоратор — это просто функция, у которой есть доступ к конструктору класса, к которому она применена (это — цель). Мы используем ссылку на конструктор, а так же — метод Object.seal() для того чтобы сделать класс нерасширяемым.

Для того чтобы применить декоратор к классу, мы записываем имя декоратора со значком @ сразу перед объявлением класса. В результате получается, что перед объявлением класса появляется конструкция вида @[name], что в нашем случае выглядит как @sealed.

Проверить работоспособность декоратора можно, скомпилировав этот TS-код с включённой опцией experimentalDecorators и попробовав изменить прототип класса:

Greeter.prototype.test = "test"; // ERROR

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

Я решил использовать TypeScript для демонстрации декораторов не без причины. Дело в том, что предложение, касающееся внедрения декораторов в JavaScript, появилось уже несколько лет назад. И оно сейчас «всего лишь» на 2 стадии согласования из 4. И в синтаксис, и в возможности декораторов из этого предложения постоянно вносят изменения. Но это не останавливает JS-сообщество от использования данной концепции. Для того чтобы в этом убедиться, достаточно взглянуть на огромные опенсорсные проекты вроде TypeScript или Angular v2+.

Правда, всё это, с течением времени, и по мере того, как предложение развивается, ведёт к проблеме, связанной с несовместимостью спецификаций. А именно, спецификация декораторов сильно развилась с момента появления соответствующего предложения, но многие проекты всё ещё её не реализовали. Пример на TypeScript, показанный выше, это демонстрация реализации более старой версии предложения. То же самое можно сказать и об Angular, и даже о Babel (хотя, в случае с Babel, можно говорить о том, что сейчас ведётся работа над реализацией более новой версии этой спецификации). В целом же, более новая версия предложения, в которой используется ключевое слово decorator и синтаксис, пригодный для компоновки, пока ещё не нашла хоть какого-то заметного применения.

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

Области (realms)


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

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

Сейчас решение этой проблемы в браузере заключается в использовании элементов <iframe>, а в некоторых особых случаях — в использовании веб-воркеров. В среде Node.js ту же проблему решают с помощью модуля vm или с помощью дочерних процессов. Для решения подобных проблем и предназначен API Realms.

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

var x = 39;
const realm = new Realm();

realm.globalThis.x; // undefined
realm.globalThis.x = 42; // 42
realm.globalThis.x; // 42

x; // 39

Здесь мы создаём новый объект Realm, используя соответствующий конструктор. С этого момента у нас есть полный доступ к новой области и к её глобальным объектам через свойство globalThis (введённое в ES2020). Тут можно видеть, что переменные основного «инкубатора» отделены от переменных в области, которую мы создали.

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

Предложение по API Realms сейчас находится на 2 стадии согласования. Когда оно, в итоге, попадёт в стандарт, можно будет ожидать того, что оно будет использоваться в «тяжёлых» библиотеках, зависящих от глобальной области видимости, в онлайн-редакторах кода, оформленных в виде «песочниц», в различных приложениях, ориентированных на тестирование.

Do-выражения (do expressions)


Синтаксис JavaScript, как и синтаксис большинства языков программирования, включает в себя инструкции (statements) и выражения (expressions). Самая заметная разница между этими конструкциями заключается в том, что выражения могут быть использованы как значения (то есть — их можно назначать переменным, передавать функциям и так далее), в то время как инструкции в таком качестве использовать нельзя.

Из-за этого различия именно выражениям чаще всего отдают предпочтение, стремясь к написанию более чистого и компактного кода. В JavaScript это можно увидеть, если проанализировать популярность функциональных выражений (function expression), куда входят и стрелочные функции, в сравнении с объявлениями функций (function declaration, function statement). Та же ситуация видна, если сравнить методы для перебора массивов (вроде forEach()) с циклами. То же самое, в случае с более продвинутыми программистами, справедливо и при сравнении тернарного оператора и инструкции if.

Предложение по do-выражениям, находящееся на 1 стадии согласования, нацелено на то, чтобы ещё сильнее расширить возможности JS-выражений. И, кстати, не надо путать понятие «do-выражение» с циклами do…while, так как это — совершенно разные вещи.

Вот пример:

let x = do {
  if (foo()) {
    f();
  } else if (bar()) {
    g();
  } else {
    h();
  }
};

Здесь представлен синтаксис из предложения по do-выражениям. В целом, перед нами — фрагмент JS-кода, обёрнутый в конструкцию do {}. Последнее выражение в этой конструкции «возвращается» в качестве итогового значения всего do-выражения.

Похожий (но не идентичный) эффект достижим с использованием IIFE (Immediately Invoked Function Expression, немедленно вызываемое функциональное выражение). Но в случае с do-выражениями весьма привлекательным кажется их компактный синтаксис. Тут, при схожем функционале, не нужно ни return, ни каких-то некрасивых вспомогательных конструкций вроде (() => {})(). Именно поэтому я полагаю, что после того, как do-выражения попадут в стандарт, их воздействие на JavaScript будет сравнимо с воздействием на язык стрелочных функций. Удобство выражений и дружелюбный синтаксис, так сказать, в одной упаковке, выглядят весьма заманчиво.

Сопоставление с образцом (pattern matching)


Эта возможность последняя в данном материале, но она далеко не последняя по значимости. Речь идёт о предложении, направленном на внедрении в язык механизма сопоставления с образцом.

Возможно, вы знакомы с JS-инструкцией switch. Она похожа на if-else, но её возможности немного сильнее ограничены, и она, безусловно, лучше подходит для организации выбора одной из множества альтернатив. Вот как она выглядит:

switch (value) {
  case 1:
    // ...
    break;
  case 2:
    // ...
    break;
  case 3:
    // ...
    break;
  default:
    // ...
    break;
}

Лично я считаю инструкцию switch слабее инструкции if, так как switch умеет сравнивать то, что ей передано, только с конкретными значениями. Данное ограничение можно обойти, но я не могу придумать — зачем. Кроме того, инструкция switch перегружена вспомогательными элементами. В частности, речь идёт об инструкциях break.

Механизм сопоставления с образцом можно рассматривать как более функциональную, основанную на выражениях, и потенциально более универсальную версию инструкции switch. Вместо простого сравнения значений, механизм сопоставления с образцом позволяет разработчику сравнивать значения со специальными образцами (шаблонами), которые поддаются глубокой настройке. Вот фрагмент кода, демонстрирующий пример предложенного API:

const getLength = vector => case (vector) {
  when { x, y, z } -> Math.hypot(x, y, z)
  when { x, y } -> Math.hypot(x, y)
  when [...etc] -> vector.length
}
getLength({x: 1, y: 2, z: 3})

Тут используется довольно-таки необычный для JavaScript синтаксис (хотя он основан на синтаксисе, встречающемся в таких языках, как Rust или Scala), у которого есть некоторые общие черты с уже известной нам инструкцией switch. Здесь, вместо ключевого слова switch, используется слово case, которое отмечает начало блока, в котором выполняется проверка значения. Затем, внутри блока, используя ключевое слово when, мы описываем шаблоны, с помощью которых хотим проверять значения. Синтаксис шаблонов напоминает синтаксис уже имеющегося в языке механизма деструктурирования объектов. Сравнивать значения можно с объектами, содержащими выбранные свойства, их можно сравнивать со значениями свойств объектов и со многими другими сущностями. Для того чтобы узнать подробности о данном механизме — взгляните на этот документ.

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

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

Итоги


На этом я завершаю рассказ о революционных возможностях JavaScript, которые, возможно, мы увидим в языке в будущем. Есть и другие подобные возможности, например — предложения о внешней стандартной библиотеке и о конвейерном операторе. Но я выбрал для этого материала только то, что показалось мне самым интересным. Учитывайте, что эти возможности всё ещё находятся на стадии предложений. Со временем они могут измениться. А может случиться так, что в стандарт они так никогда и не попадут. Но если вы, в любом случае, хотите быть в числе тех, кто раньше других пользуется подобными возможностями, я советую вам присмотреться к таким проектам, как Babel. Эти проекты дают путёвку в жизнь многим JS-предложениям (в особенности тем, которые связаны с синтаксисом). Это позволяет всем желающим экспериментировать с новейшими возможностями задолго до того, как они появятся в реализациях языка.

Каких возможностей вам больше всего не хватает в JavaScript?

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

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

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

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

      –7
      Не понимаю для чего была создана конструкция switch. Более бесполезной конструкция в Javascript наверно не существует. Нет ни одной ситуации, где она была бы лучше или компактнее, чем if / else
        +7

        case можно группировать.

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

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

              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.
                +3
                Спасибо за пример.
                А вот по поводу let, const — всё же можно:

                const a = 0;
                
                switch (true) {
                    case true: {
                        const a = 1;
                
                        console.log(a);
                        break;
                    }
                    case false: {
                        const a = 2;
                    }
                }
                
                console.log(a);
                
                


                Блоки нам в этом помогут.
                  +1
                  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;
                  }
                  
                0

                С радостью бы посмотрел на вашу реализацию этого кода из react-dom на if/else.
                Вот ещё пример из vue или пример из angular на ts. Такая конструкция есть в практически любом языке, так что рекомендую вам так же написать вот этот switch из игры VVVVVV на if/else конструкциях

                +2

                Вспоминается 10-е правило Гринспена.

                  +1

                  Удивительно разумная статья. Согласен с каждым предложением.

                    +4

                    Когда увидел "do expressions" в душе проснулась наивная надежда увидеть first class monads. Ха. Ага. Уже, ща.


                    Увы.

                      +4

                      А приведите пример на языке знакомом вам, а также как бы вы представили это на javascript?
                      Никогда не поздно написать свой велосипед и скормить его babel.

                        +7

                        Вкратце, под этот юзкейс попадают любые генераторы, которые используются для более точного контроля исполнения, чем тот, который могут дать 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 — не монада.


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

                          +2

                          Был бы благодарен за аргументацию минусов.

                            +1

                            Скорее всего, не осилили многа букф. Держи плюс за труд.

                              –1

                              Во-первых, монада — не тип. Соответственно, писать в одной и той же конструкции do (List) { и do (Monad) { очень странно.


                              Во-вторых, не слишком ли много хитрых ключевых слов?


                              В-третьих, точно ли имитация императивного кода на монадах является хорошей идеей? В императивном коде можно сделать много странных вещей, которые в ФП не предусмотрены.


                              В-четвертых, у нас всё-таки язык со слабой динамической типизацией. Что будет, если пропустить pure? Вот мы пишем — return synchronouslyCombineStuff(foo, bar);, как это должно отработать?


                              В-пятых, Promise — не совсем монада.

                                0
                                Во-первых, монада — не тип. Соответственно, писать в одной и той же конструкции 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 и при этом удовлетврял монадическим законам.

                                  –1
                                  Почему же не тип? Это тайпкласс — т.е. интерфейс, в переводе на "общечеловеческий".

                                  Во-первых, потому что у монад kind не *, а * → *. Во-вторых, интерфейсы в Javascript не представлены, они существуют только "в уме" и в документации.


                                  Это не имитация императивного кода. Это императивный код является неполным, ограниченным приближением монадических вычислений [...]

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


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

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


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

                                  Такое препятствие есть: обратная совместимость.

                                    0
                                    Во-первых, потому что у монад kind не , а → *.

                                    Ну как вы верно подметили, в JS "настоящие" типы существуют только в уме. Я не думаю, что у тайпчекеров даже возникнет необходимость поддерживать HKT в полном объеме, потому что речь выше не идет о полностью абстрактной do-нотации, а о do-нотации с указанием конкретного экземпляра монады (пусть даже это будет Free) и о правилах реинтерпретации Free.


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

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


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

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


                                    Такое препятствие есть: обратная совместимость.

                                    Приведите пример, как введение нового типа MonadPromise, способного биндиться от Thenable (или расширение существующего встроенного Promise методами из интерфейса монад), нарушит обратную совместимость существующего кода.

                        +2
                        let x = do {
                        if (foo()) {
                        f();
                        } else if (bar()) {
                        g();
                        } else {
                        h();
                        }
                        };

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


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

                          0

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

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

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

                              +1

                              а строгая типизация будет?

                                0
                                А зачем?
                                  0

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

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

                                    Прямо вот все до единой?
                                  0
                                  Теоретически, Вы можете получить некоторое подобие с помощью контрактов, брендированных типов и глубоких проверок зависимостей. Практически — это будет не очень хорошо работать.
                                    0
                                    А зачем?
                                      –2
                                      Зачем строгая типизация в Js? Как я и написал, практически — это будет не очень хорошо работать.
                                    +1
                                    Непонятно, почему по-сути всего-то новый синтаксический сахар назван «революционными возможностями». В JS этого сахара уже в излишке…
                                      0
                                      Как разработчика, однажды унаследовавшего от предыдущей команды кодовую базу на Scala, меня сильно огорчит, если pattern matching появится в прочих языках. Дело в том, что разработчики, имея такой инструмент на руках, начинают использовать его где надо и где не надо. По итогу, у вас вся кодовая база усеяна case'ами, которые очень сложно анализировать. Например, вы видите иерархию классов без единого использования полиморфизма и не понимаете, зачем она вообще нужна. Оказывается, вместо определения полиморфного метода, кто-то написал case по всем подклассам иерархии. Или, допустим, вы хотите добавить еще один подкласс и видите, что сделать это очень легко, т.к. в реализации нуждается лишь один-два метода. Но потом ничего не работает, потому что в коде миллион case'ов с default-реализацией, которые ваш подкласс, само собой, не учитывают. И найти их тяжело. Возможно, дело в привычке, но мне идея не нравится.

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

                                      Самое читаемое