|>
) для JavaScript (В этом документе %
используется в качестве маркера-заполнителя для ссылки на тему. Почти наверняка это не будет окончательный выбор ; подробности см. в обсуждении токенов для велосипедистов.)
В опросе State of JS 2020 четвертый по популярности ответ на вопрос «Чего, по вашему мнению, сейчас не хватает в JavaScript?» был трубопроводчиком . Почему?
Когда мы выполняем последовательные операции (например, вызовы функций) над значением в JavaScript, в настоящее время существует два основных стиля:
То есть three(two(one(value)))
против value.one().two().three()
. Однако эти стили сильно различаются по читабельности, беглости и применимости.
Первый стиль, вложение , обычно применим — он работает для любой последовательности операций: вызовы функций, арифметика, литералы массива/объекта, await
и yield
и т. д.
Однако вложенность трудно читать, когда она становится глубокой: поток выполнения движется справа налево , а не при чтении слева направо, как в обычном коде. Если на некоторых уровнях имеется несколько аргументов , чтение даже прыгает вперед и назад : наши глаза должны прыгнуть влево, чтобы найти имя функции, а затем они должны прыгнуть вправо, чтобы найти дополнительные аргументы. Кроме того, последующее редактирование кода может оказаться затруднительным: нам необходимо найти правильное место для вставки новых аргументов среди множества вложенных круглых скобок .
Рассмотрим этот реальный код из React.
console . log (
chalk . dim (
`$ ${ Object . keys ( envars )
. map ( envar =>
` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
} ` ,
'node' ,
args . join ( ' ' ) ) ) ;
Этот реальный код состоит из глубоко вложенных выражений . Чтобы прочитать поток данных, глаза человека сначала должны:
Найдите исходные данные (самое внутреннее выражение, envars
).
А затем многократно сканируйте взад и вперед изнутри наружу для каждого преобразования данных, каждое из которых либо легко пропускаемый префиксный оператор слева, либо суффиксный оператор справа:
Object.keys()
(левая сторона),.map()
(правая сторона),.join()
(правая сторона),chalk.dim()
(левая сторона), затемconsole.log()
(левая сторона).В результате глубокой вложенности многих выражений (некоторые из которых используют префиксные операторы, некоторые — постфиксные операторы, а некоторые — операторы циркумфикса ) мы должны проверять как левую, так и правую часть, чтобы найти заголовок каждого выражения .
Второй стиль, цепочка методов , можно использовать только в том случае, если значение имеет функции, обозначенные как методы для его класса. Это ограничивает его применимость. Но когда он применяется, благодаря своей постфиксной структуре он, как правило, более удобен и его легче читать и писать. Выполнение кода происходит слева направо . Глубоко вложенные выражения распутываются . Все аргументы вызова функции группируются по имени функции. А редактирование кода позже для вставки или удаления дополнительных вызовов методов тривиально, поскольку нам просто нужно поместить курсор в одно место, а затем начать вводить или удалять одну непрерывную серию символов.
Действительно, преимущества цепочки методов настолько привлекательны , что некоторые популярные библиотеки специально искажают структуру своего кода, чтобы обеспечить большее сцепление методов . Самый яркий пример — jQuery , которая до сих пор остается самой популярной JS-библиотекой в мире. Основная конструкция jQuery представляет собой единый сверхобъект с десятками его методов, каждый из которых возвращает объект одного и того же типа, так что мы можем продолжить связывание . У этого стиля программирования даже есть название: Fluent Interfaces .
К сожалению, при всей своей гибкости, цепочка методов сама по себе не может вместить другие синтаксисы JavaScript: вызовы функций, арифметику, литералы массива/объекта, await
и yield
и т. д. Таким образом, цепочка методов остается ограниченной в своей применимости .
Оператор канала пытается объединить удобство и простоту цепочки методов с широкой применимостью вложенности выражений .
Общая структура всех операторов канала имеет value |>
e1 |>
e2 |>
e3 , где e1 , e2 , e3 — все выражения, которые принимают последовательные значения в качестве параметров. Затем оператор |>
творит некую магию, «перенаправляя» value
из левой части в правую.
Продолжение этого глубоко вложенного реального кода из React:
console . log (
chalk . dim (
`$ ${ Object . keys ( envars )
. map ( envar =>
` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
} ` ,
'node' ,
args . join ( ' ' ) ) ) ;
… мы можем распутать это как таковое, используя оператор канала и токен-заполнитель ( %
), заменяющий значение предыдущей операции:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
Теперь читатель может быстро найти исходные данные (то, что раньше было самым сокровенным выражением, envars
), а затем линейно прочитать слева направо каждое преобразование данных.
Можно утверждать, что использование временных переменных должно быть единственным способом распутать глубоко вложенный код. Явное именование переменной каждого шага приводит к чему-то похожему на цепочку методов, с преимуществами, аналогичными чтению и написанию кода.
Например, используя наш предыдущий модифицированный реальный пример из React:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
…версия с использованием временных переменных будет выглядеть так:
const envarString = Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' ) ;
const consoleText = `$ ${ envarString } ` ;
const coloredConsoleText = chalk . dim ( consoleText , 'node' , args . join ( ' ' ) ) ;
console . log ( coloredConsoleText ) ;
Но есть причины, по которым в реальном мире мы постоянно сталкиваемся с глубоко вложенными выражениями в коде друг друга, а не со строками временных переменных. И есть причины, по которым беглые интерфейсы jQuery, Mocha и т. д. на основе цепочек методов все еще популярны .
Часто писать код с длинной последовательностью временных одноразовых переменных слишком утомительно и многословно . Возможно, человеку читать даже утомительно и визуально шумно.
Если присвоение имен является одной из самых трудных задач в программировании, то программисты неизбежно будут избегать присвоения имен переменным, если считают, что их польза от этого относительно невелика.
Можно возразить, что использование одной изменяемой переменной с коротким именем уменьшит многословность временных переменных, достигая тех же результатов, что и в случае с оператором канала.
Например, наш предыдущий модифицированный реальный пример из React можно было бы переписать следующим образом:
let _ ;
_ = Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' ) ;
_ = `$ ${ _ } ` ;
_ = chalk . dim ( _ , 'node' , args . join ( ' ' ) ) ;
_ = console . log ( _ ) ;
Но такой код не распространен в реальном коде. Одна из причин этого заключается в том, что изменяемые переменные могут неожиданно измениться , что приведет к скрытым ошибкам, которые трудно обнаружить. Например, переменная может быть случайно упомянута в замыкании. Или оно может быть ошибочно переназначено внутри выражения.
// setup
function one ( ) { return 1 ; }
function double ( x ) { return x * 2 ; }
let _ ;
_ = one ( ) ; // _ is now 1.
_ = double ( _ ) ; // _ is now 2.
_ = Promise . resolve ( ) . then ( ( ) =>
// This does *not* print 2!
// It prints 1, because `_` is reassigned downstream.
console . log ( _ ) ) ;
// _ becomes 1 before the promise callback.
_ = one ( _ ) ;
Эта проблема не возникнет с оператором трубопровода. Токен темы нельзя переназначить, а код вне каждого шага не может изменить его привязку.
let _ ;
_ = one ( )
| > double ( % )
| > Promise . resolve ( ) . then ( ( ) =>
// This prints 2, as intended.
console . log ( % ) ) ;
_ = one ( ) ;
По этой причине код с изменяемыми переменными труднее читать. Чтобы определить, что представляет собой переменная в любой заданной точке, вы должны просмотреть всю предыдущую область видимости в поисках мест, где она переназначается .
Ссылка на тему конвейера, с другой стороны, имеет ограниченную лексическую область, и ее привязка неизменна в пределах ее области. Его нельзя случайно переназначить, и его можно безопасно использовать в замыканиях.
Хотя значение темы также меняется на каждом этапе конвейера, мы просматриваем только предыдущий этап конвейера, чтобы разобраться в нем, что приводит к созданию кода, который легче читать.
Еще одним преимуществом оператора канала перед последовательностями операторов присваивания (как с изменяемыми, так и с неизменяемыми временными переменными) является то, что они являются выражениями .
Выражения канала — это выражения, которые можно возвращать напрямую, присваивать переменной или использовать в таких контекстах, как выражения JSX.
С другой стороны, использование временных переменных требует последовательностей операторов.
Трубопроводы | Временные переменные |
---|---|
const envVarFormat = vars =>
Object . keys ( vars )
. map ( var => ` ${ var } = ${ vars [ var ] } ` )
. join ( ' ' )
| > chalk . dim ( % , 'node' , args . join ( ' ' ) ) ; | const envVarFormat = ( vars ) => {
let _ = Object . keys ( vars ) ;
_ = _ . map ( var => ` ${ var } = ${ vars [ var ] } ` ) ;
_ = _ . join ( ' ' ) ;
return chalk . dim ( _ , 'node' , args . join ( ' ' ) ) ;
} |
// This example uses JSX.
return (
< ul >
{
values
| > Object . keys ( % )
| > [ ... Array . from ( new Set ( % ) ) ]
| > % . map ( envar => (
< li onClick = {
( ) => doStuff ( values )
} > { envar } < / li >
) )
}
< / ul >
) ; | // This example uses JSX.
let _ = values ;
_ = Object . keys ( _ ) ;
_ = [ ... Array . from ( new Set ( _ ) ) ] ;
_ = _ . map ( envar => (
< li onClick = {
( ) => doStuff ( values )
} > { envar } < / li >
) ) ;
return (
< ul > { _ } < / ul >
) ; |
Оператору трубы было два конкурирующих предложения : трубы Hack и трубы F#. (До этого было третье предложение «умного сочетания» первых двух предложений, но оно было отозвано, поскольку его синтаксис является строго расширенным набором одного из предложений.)
Эти два предложения канала немного различаются в том, что такое «магия», когда мы записываем наш код при использовании |>
.
Оба предложения повторно используют существующие концепции языка: каналы Hack основаны на концепции выражения , а каналы F# основаны на концепции унарной функции .
Выражения конвейеризации и конвейерные унарные функции соответственно имеют небольшие и почти симметричные компромиссы .
В синтаксисе канала языка Hack правая часть канала представляет собой выражение , содержащее специальный заполнитель , который вычисляется с помощью заполнителя, привязанного к результату вычисления выражения левой части. То есть мы записываем value |> one(%) |> two(%) |> three(%)
чтобы передать value
через три функции.
Плюсы: правая часть может быть любым выражением , а заполнитель может располагаться в любом месте, где может находиться любой обычный идентификатор переменной, поэтому мы можем передавать по каналу любой код без каких-либо специальных правил :
value |> foo(%)
для вызовов унарных функций,value |> foo(1, %)
для вызовов n-арных функций,value |> %.foo()
для вызовов методов,value |> % + 1
для арифметики,value |> [%, 0]
для литералов массива,value |> {foo: %}
для литералов объекта,value |> `${%}`
для литералов шаблона,value |> new Foo(%)
для построения объектов,value |> await %
для ожидающих обещаний,value |> (yield %)
для получения значений генератора,value |> import(%)
для вызова ключевых слов, подобных функциям, Минусы: передача унарных функций в каналах Hack немного более подробна , чем в каналах F#. Сюда входят унарные функции, созданные библиотеками каррирования функций, такими как Ramda, а также унарные стрелочные функции, которые выполняют сложную деструктуризацию своих аргументов: каналы взлома будут немного более подробными с явным суффиксом вызова функции (%)
.
(Сложная деструктуризация значения темы будет проще по мере выполнения выражений, поскольку тогда вы сможете выполнять назначение/деструктуризацию переменных внутри тела канала.)
В синтаксисе канала языка F# правая часть канала представляет собой выражение, результатом которого должна быть унарная функция , которая затем неявно вызывается со значением левой части в качестве единственного аргумента . То есть мы пишем value |> one |> two |> three
чтобы передать value
через три функции. left |> right
становится right(left)
. Это называется неявным программированием или бесточечным стилем.
Например, используя наш предыдущий модифицированный реальный пример из React:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
…версия, использующая каналы F# вместо каналов Hack, будет выглядеть так:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > x => `$ ${ x } `
| > x => chalk . dim ( x , 'node' , args . join ( ' ' ) )
| > console . log ;
Плюсы: ограничение, согласно которому правая часть должна разрешаться в унарную функцию, позволяет нам писать очень краткие каналы , когда операция, которую мы хотим выполнить, является вызовом унарной функции :
value |> foo
для вызовов унарных функций. Сюда входят унарные функции, созданные библиотеками каррирования функций , такими как Ramda, а также унарные стрелочные функции, которые выполняют сложную деструктуризацию своих аргументов: каналы F# были бы немного менее многословными с неявным вызовом функции (no (%)
).
Против: Ограничение означает, что любые операции , выполняемые с другим синтаксисом, должны быть немного более подробными, если обернуть операцию в унарную стрелочную функцию :
value |> x=> x.foo()
для вызовов методов,value |> x=> x + 1
для арифметики,value |> x=> [x, 0]
для литералов массива,value |> x=> ({foo: x})
для объектных литералов,value |> x=> `${x}`
для литералов шаблона,value |> x=> new Foo(x)
для создания объектов,value |> x=> import(x)
для вызова ключевых слов, подобных функциям,Даже вызов именованных функций требует переноса , когда нам нужно передать более одного аргумента :
value |> x=> foo(1, x)
для вызовов n-арных функций. Против: операции await
и yield
ограничены содержащей их функцией и, следовательно, не могут обрабатываться только унарными функциями . Если мы хотим интегрировать их в выражение канала, await
и yield
должны обрабатываться как особые случаи синтаксиса :
value |> await
для ожидающих обещаний иvalue |> yield
для получения значений генератора. И каналы Hack, и каналы F# соответственно налагают небольшой синтаксический налог на различные выражения:
Хакерские каналы слегка облагают налогом только унарные вызовы функций , и
Каналы F# слегка облагают налогом все выражения, кроме вызовов унарных функций.
В обоих предложениях синтаксический налог на каждое облагаемое выражение невелик ( оба (%)
и x=>
состоят всего из трех символов ). Однако налог умножается на распространенность его соответственно облагаемых налогом выражений. Поэтому, возможно, имеет смысл ввести налог на те выражения, которые менее распространены , и провести оптимизацию в пользу тех выражений, которые являются более распространенными .
Вызовы унарных функций, как правило, встречаются реже , чем все выражения, кроме унарных функций. В частности, вызов методов и вызов n-арных функций всегда будут популярны ; В целом частота вызова унарной функции равна или превосходит только эти два случая, не говоря уже о других распространенных синтаксисах, таких как литералы массива , литералы объектов и арифметические операции . Это объяснение содержит несколько реальных примеров этой разницы в распространенности.
Более того, некоторые другие предлагаемые новые синтаксисы , такие как вызов расширений , выражения do и литералы записи/кортежа , также, вероятно, станут широко распространенными в будущем . Аналогично, арифметические операции станут еще более распространенными, если TC39 стандартизирует перегрузку операторов . Распутывание выражений этих будущих синтаксисов будет более простым с помощью каналов Hack по сравнению с каналами F #.
Синтаксический налог Hack-каналов на вызовы унарных функций (т.е. (%)
для вызова унарной функции в правой части) не является особым случаем : это просто явное написание обычного кода , как мы обычно это делали бы без канала.
С другой стороны, каналы F# требуют от нас различать «код, который разрешается в унарную функцию» и «любое другое выражение» — и не забывать добавлять оболочку стрелочной функции вокруг последнего случая.
Например, для каналов Hack value |> someFunction + 1
является недопустимым синтаксисом и приведет к преждевременному сбою . Нет необходимости признавать, что someFunction + 1
не будет считаться унарной функцией. Но в каналах F# value |> someFunction + 1
по-прежнему является допустимым синтаксисом — оно просто завершится ошибкой во время выполнения , поскольку someFunction + 1
не может быть вызвана.
Группа чемпионов по трубкам дважды представляла трубки F# для Этапа 2 TC39. Оба раза перейти на второй этап не удалось . Оба канала F# (и приложение с частичной функцией (PFA)) столкнулись с резким сопротивлением со стороны многих других представителей TC39 из-за различных проблем. К ним относятся:
await
.Это сопротивление произошло за пределами группы чемпионов по трубке. Дополнительную информацию смотрите на сайте HISTORY.md.
Группа сторонников конвейера убеждена, что любой оператор канала лучше, чем ничего, поскольку он позволяет легко линеаризовать глубоко вложенные выражения, не прибегая к именованным переменным. Многие члены группы чемпионов считают, что каналы Hack немного лучше, чем каналы F#, а некоторые члены группы чемпионов считают, что каналы F# немного лучше, чем каналы Hack. Но все в группе чемпионов согласны с тем, что трубы F# встретили слишком сильное сопротивление, чтобы пройти TC39 в обозримом будущем.
Подчеркнем: вполне вероятно, что попытка переключиться с каналов Hack обратно на каналы F# приведет к тому, что TC39 вообще никогда не согласится ни на какие каналы. Синтаксис PFA также сталкивается с тяжелыми трудностями в TC39 (см. HISTORY.md). Многие члены группы чемпионов по пайпу считают, что это неудачно, и они готовы позже снова побороться за разделенную смесь F#-пайпа и синтаксис PFA. Но есть довольно много представителей (включая разработчиков браузерных движков) за пределами Pipe Champion Group, которые обычно против поощрения неявного программирования (и синтаксиса PFA), независимо от Hack Pipes.
(Официальный проект спецификации доступен.)
Ссылка на тему %
является нулевым оператором . Он действует как заполнитель для значения темы , имеет лексическую область действия и неизменяем .
%
не является окончательным выбором (Точный токен для ссылки на тему не является окончательным . %
вместо этого может быть ^
или многими другими токенами. Мы планируем решить , какой фактический токен использовать, прежде чем перейти к этапу 3. Однако %
кажется наименее синтаксически проблематичным, и он также напоминает заполнители строк формата printf и функциональные литералы Clojure #(%)
.)
Оператор канала |>
— это инфиксный оператор , который формирует выражение канала (также называемое конвейером ). Он оценивает свою левую часть ( заголовок канала или ввод канала ), неизменяемо привязывает полученное значение ( значение темы ) к ссылке на тему , затем оценивает свою правую часть ( тело канала ) с помощью этой привязки. Результирующее значение правой части становится окончательным значением выражения всего канала ( выходным значением канала ).
Приоритет оператора канала такой же , как:
=>
;=
, +=
и т. д.;yield
и yield *
; Это сложнее , чем только оператор запятая ,
.
Он более свободный, чем у всех остальных операторов.
Например, v => v |> % == null |> foo(%, 0)
будет группироваться в v => (v |> (% == null) |> foo(%, 0))
,
что, в свою очередь, эквивалентно v => foo(v == null, 0)
.
Тело канала должно использовать значение темы хотя бы один раз . Например, value |> foo + 1
является недопустимым синтаксисом , поскольку его тело не содержит ссылки на тему. Такая конструкция обусловлена тем, что отсутствие ссылки на тему в теле выражения канала почти наверняка является случайной ошибкой программиста.
Аналогично, ссылка на тему должна содержаться в теле канала. Использование ссылки на тему вне тела канала также является недопустимым синтаксисом .
Чтобы избежать путаницы при группировании, использование других операторов с аналогичным приоритетом (т. е. стрелки =>
, тернарного условного оператора ?
:
, операторов присваивания и оператора yield
) в качестве заголовка или тела канала является недопустимым . При использовании |>
с этими операторами мы должны использовать круглые скобки , чтобы явно указать, какая группировка правильная. Например, a |> b ? % : c |> %.d
— неверный синтаксис; его следует исправить либо на a |> (b ? % : c) |> %.d
, либо на a |> (b ? % : c |> %.d)
.
Наконец, привязки тем внутри динамически скомпилированного кода (например, с помощью eval
или new Function
) не могут использоваться вне этого кода. Например, v |> eval('% + 1')
выдаст синтаксическую ошибку, когда выражение eval
вычисляется во время выполнения.
Других особых правил нет .
Естественным результатом этих правил является то, что если нам нужно вставить побочный эффект в середину цепочки выражений конвейера, не изменяя передаваемые по конвейеру данные, то мы можем использовать выражение с запятой , например, со value |> (sideEffect(), %)
. Как обычно, выражение с запятой будет оценивать свою правую часть %
, по существу проходя через значение темы, не изменяя его. Это особенно полезно для быстрой отладки: value |> (console.log(%), %)
.
Единственными изменениями в исходных примерах были удаление зубов и удаление комментариев.
Из jquery/build/tasks/sourceMap.js:
// Status quo
var minLoc = Object . keys ( grunt . config ( "uglify.all.files" ) ) [ 0 ] ;
// With pipes
var minLoc = grunt . config ( 'uglify.all.files' ) | > Object . keys ( % ) [ 0 ] ;
Из узла/deps/npm/lib/unpublish.js:
// Status quo
const json = await npmFetch . json ( npa ( pkgs [ 0 ] ) . escapedName , opts ) ;
// With pipes
const json = pkgs [ 0 ] | > npa ( % ) . escapedName | > await npmFetch . json ( % , opts ) ;
Из underscore.js:
// Status quo
return filter ( obj , negate ( cb ( predicate ) ) , context ) ;
// With pipes
return cb ( predicate ) | > _ . negate ( % ) | > _ . filter ( obj , % , context ) ;
Из ramda.js.