В релизе babel@7.0.0-beta52 появился новый обязательный флаг конфига для плагина @babel/plugin-proposal-pipeline-operator
, что ломает обратную совместимость для предыдущих версий плагина. Из этой статьи вы узнаете, что такое оператор pipeline
и зачем ему нужна конфигурация.
Текущий статус
Gilbert Garza, изначально предложивший оператор pipeline
, ставил целью получить простой синтаксис для «упорядоченных цепочек вызовов функций в удобочитаемом функциональном стиле». Оператор pipeline берёт своё начало в таких языках, как F#, Hack, Elm, Elixir и других, а при добавлении его в JavaScript возникают два спорных момента:
- Где и как использовать плейсхолдеры?
- Как должны работать
async / await
в пайплайне?
Плейсхолдеры
Первый вопрос был поднят Кевин Смит в этом тикете, где он предложил использовать стиль пайплайнов из языка Hack. В Hack плейсхолдеры обязательны для любой правой части пайплайна, например:
namespace Hack\UserDocumentation\Operators\Pipe\Examples\MapFilterCountPiped;
function piped_example(array<int> $arr): int {
return $arr
|> \array_map($x ==> $x * $x, $$)
|> \array_filter($$, $x ==> $x % 2 == 0)
|> \count($$);
}
var_dump(piped_example(range(1, 10)));
Мы взяли за основу то, что плейсхолдер может быть использован в любом выражении и содержит значение с прошлого шага пайплайна. Такой подход даёт нам гибкость и широкие возможности для составления выражений.
Обратная сторона медали — усложнение языка из-за добавления нового токена. Пока что мы выбрали хеш (#
), и, хотя дискуссия по-прежнему открыта, любой токен потенциально будет иметь пересечения с другими использованиями. Например хеш также используется приватными полями класса, как и любые другие варианты токена используются так или иначе.
Async / Await
Первый вариант оператора pipeline
содержал такой синтаксис для await
:
x |> await f
что может быть развёрнуто в:
await f(x)
К сожалению, пользователи вполне могут ожидать и такого разворачивания:
(await f)(x)
Пока буксовала сама идея добавления async
в пайплайн, члены комитета высказались против оператора pipeline
, который не поддерживает async / await
. Да, есть варианты, как без явного синтаксиса работать с функциями, возвращающими Promise, но все эти варианты слишком громоздкие или требуют вспомогательных функций.
Предлагаемые решения
В результате дискуссий сформировались два предложения (вдобавок к минимальному варианту): использовать F#-пайплайны и Smart-пайплайны. Давайте посмотрим на эти предложения.
Минимальный вариант оператора
Это предложение касается только базовой функциональности. В минимальном варианте убрана поддержка асинхронности и нет плейсхолдеров. Такой вариант соответствует поведению babel-плагина предыдущих версий, до появления конфигурации, и соответствует текущей спецификации по оператору pipeline
в репозитории. Он используется больше как черновик-пробник, чтобы выявить преимущества и недостатки других предложений, и вряд ли будет принят без кардинальных изменений, которые есть в альтернативных предложениях.
F# пайплайны
Плейсхолдеры для F#-пайплайнов вообще не нужны. В базовом варианте стрелочные функции закрывают потребность в плейсхолдерах, требуя меньше писанины, да и основываются на привычном для всех синтаксисе ES2015.
На текущий момент (по спецификации F#-пайплайнов) стрелочные функции должны быть обёрнуты в скобки:
let person = { score: 25 };
let newScore = person.score
|> double
|> (_ => add(7, _))
|> (_ => boundScore(0, 100, _));
Полным ходом ведутся изыскания, чтобы определить, осуществимо ли использовать стрелочные функции без скобок, которые здесь выглядят синтаксически излишними.
Что касается асинхронности, в F#- пайплайнах await
работает как унарная функция:
promise |> await
Что разворачивается в:
await promise
и поэтому await
может быть использован посреди длинной цепочки асинхронных вызовов:
promise
|> await
|> (x => doubleSay(x, ', '))
|> capitalize
|> (x => x + '!')
|> (x => new User.Message(x))
|> (x => stream.write(x))
|> await
|> console.log;
Такая специальная обработка await
потенциально может открыть возможность похожим образом использовать другие унарные операторы (например, typeof
), но исходная спецификация F#-пайплайнов не содержит их.
Smart-пайплайны
Smar-пайплайны доводят идею плейсхолдеров до логического завершения, разрешая в пайплайнах как частичное применение, так и произвольные выражения. Предыдущая длинная цепочка может быть записана так:
promise
|> await #
|> doubleSay(#, ', ')
|> # || throw new TypeError()
|> capitalize
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log;
Правила использования плейсхолдеров в Smart-пайплайнах довольно просты. Если одиночный идентификатор передан в шаг пайплайна, то никакой дополнительный токен (плейсхолдера) не требуется, это называется «минималистским стилем» ("bare style"):
x |> a;
x |> f.b;
В отличие от Hack, унарные функции не требуют токена плейсхолдера.
Для других выражений плейсхолдер (называемый "lexical topic token" — «лексема тематического стиля») обязателен, а пайплайн считается работающим в рамках «тематического стиля» — "topic style". Отсутствие токена плейсхолдера в таком случае вызывает раннюю ошибку SyntaxError:
10 |> # + 1;
promise |> await #;
Если имеются какие-либо операторы, скобки (в том числе для вызова метода), кавычки, или вообще что угодно, кроме идентификатора и точки, то токен плейсхолдера обязателен. Это поможет избежать выстрела себе в ногу и устранить неопределённость.
Smart-пайплайны решают проблему поддержки асинхронности в более общем виде, что разрешает использовать в пайплайнах все возможные выражения, не только await
, но и typeof
, yield
и любые другие операторы.
На сцену выходит Babel
Как только все три предложения были конкретизированы, мы пришли к выводу, что такие обсуждения не приведут к разрешению глубоких противоречий между предложениями. Мы решили, что лучший способ — собрать отзывы разработчиков, использующих предложения в реальном коде. С учётом роли Babel в сообществе разработчиков, мы решили добавить все три варианта в плагин оператора pipeline
.
Поскольку парсинг для всех трёх предложений незначительно, но отличается, их поддержка должна быть сначала добавлена в @babel/parser
(который babylon
), причём парсер должен знать, какое предложение нужно сейчас поддерживать. Таким образом плагин оператора pipeline
требует опции "proposal"
, как для конфигурирования babylon для парсинга, так и для последующей трансформации.
Мы работали над этим в оперативном режиме, потому что нам надо сделать все изменения, ломающие обратную совместимость, до того, как babel@7 перестанет быть бетой. Мы бы хотели в итоге сделать один из вариантов пайплайнов дефолтным для плагина, чтобы избавиться от необходимости в конфигурационной опции.
Учитывая эти ограничения, мы решили добавить опцию в конфигурацию плагина и сделать её обязательной, принуждая пользователей решать, какое из предложений они хотят использовать в своём проекте. Как только конкретное предложение будет выбрано как каноническое поведение оператора, мы пометим опцию "proposal"
как устаревшую, а канонический вариант станет работать по-умолчанию. Поддержка отменённых предложений будет работать до следующей мажорной версии.
Принять участие
Если хотите участвовать в обсуждении предложения, то все обсуждения публичны и вы можете найти их в репозитории предложения оператора pipeline. К вашим услугам также презентация со встречи TC39. В конце концов, вы можете обратиться к James DiGioia, J. S. Choi или к Daniel Ehrenberg в твиттере.
Но что гораздо важнее, как только работа над pipeline
будет завершена, попробуйте его в своих проектах! Мы также работаем над добавлением новых возможностей в repl, так что вы сможете проверить свой код и в интерактивном режиме. Нам нужна обратная связь, и использование в реальном коде очень поможет её собрать. Отправляйте твиты на @babeljs.
Комментарии (9)
k12th
25.07.2018 14:40+1Я обычно очень положительно отношусь ко всем нововведениям в JS, но новый причудливый оператор для очень узкого юзкейса вместо улучшения стандартной библиотеки кажется мне сомнительным.
dagen Автор
25.07.2018 16:17+2У вас есть в публикациях перевод о миксинах в javascript. У пайплайн оператора набор возможных юзкейзов шире, чем может показаться на первый взгляд, например те же примеси:
// Before: class Comment extends Sharable(Editable(Model)) { // ... } // After: class Comment extends Model |> Editable |> Sharable { // ... }
Дальше — больше.
Да, в случае классов нам вполне можно использовать и декораторы для примешивания, но для функциональных компонентов так просто всё не получится (декораторы не применимы просто к функциям). Поэтому обычно в recompose/recompact используют обычный функциональный compose/pipe (который надо импортить из какой-либо библиотеки). Пайплайн позволяет собирать энхансер (higher-order component) из своих хоков (или рекомпоузовских) используя нативный javascript-синтаксис.
Этот оператор (как и стагнирующее предложение bind-оператора) вводится как раз ради появления нативного синтаксиса для обработки цепочек/пайплайнов, а они очень часто используются, это много юзкейзов. Пакетами, реализующими пайплайны и цепочки, завален весь npm. Это примерно то же самое, как ранее, до появления ключевого слова class в es6, каждый начинающий javascript-ер был обязан сделать свою реализацию "полноценного ООП" в js
(и написать об этом на Хабр).k12th
25.07.2018 16:44Спасибо за объяснения.
Честно говоря, меня вполне устраивает
Sharable(Editable(Model))
(да это даже меньше символов занимает), но это вкусовщина.
Что касается recompose и HOC'ов, то и так было понятно, что пайплайны затеваются почти и исключительно под них, поэтому я и ворчу про узкий юзкейс.
Цепочек я не писал уже очень давно, а когда писал, то мне вполне хватало Array#reduce.
Впрочем, заткнусь, пожалуй, со своим старческим брюзжанием и постараюсь принять дивный новый мир.
Slowz
25.07.2018 16:45+1Интересно мнение сообщества. Какой из предложенных вариантов вам больше нравится?
// С именованием переменных function getRandomColor() { const base = Math.random() * 0xFFFFFF; const floored = Math.floor(base); const hexString = floored.toString(16); return `#${hexString}`; }
// Просто последовательные вызовы function getRandomColor() { return `#${Math.floor(Math.random() * 0xFFFFFF).toString(16)}`; }
// С пайплайн оператором function getRandomColor() { return Math.random() |> base => base * 0xFFFFFF |> Math.floor |> floored => floored.toString(16) |> hex => `#${hex}` }
faiwer
25.07.2018 19:06На мой взгляд третий… и первый. Это сильно зависит от конкретного кода. Чем сложнее и замысловатее задача, тем сложнее вам будет написать первый вариант, но проще третий. По сути они идентичны, но в третьем:
- код выровнен, а оттого проще воспринимается
- за '|>' цепляется глаз, проще воспринимать саму последовательность действий
- нет проблемы с именованием
Собственно по этой причине те, кто часто имеет дело с чем-то около-функциональным и не только, предпочитают использовать цепочки трансформаций. Даже без '|>' они позволяют многое упростить и избежать дубляжа. Но, без '|>' — шаг влево, шаг в право — цепочка порвалась.
dagen Автор
26.07.2018 17:00+1На самом деле не очень честное сравнение. Каждый инструмент в языке предназначен для чего-нибудь, и pipeline operator не исключение: это инструмент для функционального подхода,
и он лучше себя покажет в таком сравнении, например:
// (1) Просто вызов функций с промежуточными значениями const doEverything = val => { const junk1 = doSomething(val); const junk2 = doSomethingElse(junk1); return doSomeMore(junk2); };
// (2) Вложенные вызовы тех же функций const doEverything = val => doSomething(doSomethingElse(doSomeMore(val)));
// (3) Бесточечный подход (с импортом pipe откуда-нибудь) const doEverything = pipe( doSomething, doSomethingElse, doSomeMore, );
// (4) Новый модный молодёжный оператор const doEverything = val => val |> doSomething |> doSomethingElse |> doSomeMore
Собственно самые читабельные варианты — два последних, но точку останова можно нормально (без доп ухищрений, без изменения кода, без захода в блок извне) поставить только в первом и в последнем.
Получается, что pipeline operator — это самый удобный для чтения и для отладки инструмент из тех, которые на текущий момент существуют в языке.
gnaeus
А можно какой-нибудь пример использования pipeline operator c async / await из реальной жизни?
С синхронным вариантом все понятно: его можно использовать для преобразования коллекций.
Например, написать
map
,filter
,reduce
дляObject, Map, Set
без использования Lodash.А что насчет асинхронного?
dagen Автор
Лично мне кажется что многие привычные асинхронные сценарии в генераторах redux-saga будут выглядеть намного удобнее с пайплайном (пока что запись саг доступна только через Smart-пайплайны, так как в F#-like пайплайнах нет yield). Как и любая запись сценария в функциональном стиле будет удобней, чем в императивном (с моей точки зрения, само собой).
Вот сейчас у меня перед глазами функция без всяких redux-saga, которая содержит сценарий отправки отзыва, она должна сделать три асинхронные операции: если пользователь аноним, то создаётся пользователь, затем собственно отправляется отзыв, затем перезапрашиваются отзывы к текущей странице. Между этими тремя вызовами есть синхронные операции, и было бы неудобно часть действий писать в пайплайне, но иногда разрывать цепочку, чтобы использовать await. В конце концов надо ещё вернуть промис всей этой катавасии, и функциональная запись пайплайна тут как нельзя кстати: вся функция выглядела бы как несколько шагов пайплайна.
Если вы хотите больше точек зрения, то вот тут идут баталии по вашему вопросу: github.com/tc39/proposal-pipeline-operator/issues/86