|>
) para JavaScript (Este documento usa %
como token de espaço reservado para a referência do tópico. Esta quase certamente não será a escolha final ; consulte a discussão sobre token bikeshedding para obter detalhes.)
Na pesquisa State of JS 2020, a quarta resposta principal para “O que você acha que está faltando no JavaScript?” era operador de tubos . Por que?
Quando realizamos operações consecutivas (por exemplo, chamadas de função) em um valor em JavaScript, existem atualmente dois estilos fundamentais:
Ou seja, three(two(one(value)))
versus value.one().two().three()
. No entanto, esses estilos diferem muito em legibilidade, fluência e aplicabilidade.
O primeiro estilo, nesting , é geralmente aplicável – funciona para qualquer sequência de operações: chamadas de função, aritmética, literais de array/objeto, await
e yield
, etc.
No entanto, o aninhamento é difícil de ler quando se torna profundo: o fluxo de execução se move da direita para a esquerda , em vez da leitura da esquerda para a direita do código normal. Se houver vários argumentos em alguns níveis, a leitura ainda oscila para frente e para trás : nossos olhos devem pular para a esquerda para encontrar o nome de uma função e, em seguida, devem pular para a direita para encontrar argumentos adicionais. Além disso, editar o código posteriormente pode ser complicado: devemos encontrar o local correto para inserir novos argumentos entre muitos parênteses aninhados .
Considere este código do mundo real do React.
console . log (
chalk . dim (
`$ ${ Object . keys ( envars )
. map ( envar =>
` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
} ` ,
'node' ,
args . join ( ' ' ) ) ) ;
Este código do mundo real é feito de expressões profundamente aninhadas . Para ler o fluxo de dados, os olhos humanos devem primeiro:
Encontre os dados iniciais (a expressão mais interna, envars
).
E, em seguida, verifique repetidamente de dentro para fora cada transformação de dados, cada uma com um operador de prefixo facilmente esquecido à esquerda ou um operador de sufixo à direita:
Object.keys()
(lado esquerdo),.map()
(lado direito),.join()
(lado direito),chalk.dim()
(lado esquerdo), entãoconsole.log()
(lado esquerdo).Como resultado do aninhamento profundo de muitas expressões (algumas das quais usam operadores de prefixo , algumas das quais usam operadores pós-fixados e algumas das quais usam operadores circunfixos ), devemos verificar os lados esquerdo e direito para encontrar o cabeçalho de cada expressão .
O segundo estilo, encadeamento de métodos , só é utilizável se o valor possuir as funções designadas como métodos para sua classe. Isto limita a sua aplicabilidade. Mas quando se aplica, graças à sua estrutura postfix, é geralmente mais utilizável e mais fácil de ler e escrever. A execução do código flui da esquerda para a direita . Expressões profundamente aninhadas são desembaraçadas . Todos os argumentos para uma chamada de função são agrupados com o nome da função. E editar o código posteriormente para inserir ou excluir mais chamadas de método é trivial, já que teríamos apenas que colocar o cursor em um local e começar a digitar ou excluir uma série contígua de caracteres.
Na verdade, os benefícios do encadeamento de métodos são tão atraentes que algumas bibliotecas populares distorcem sua estrutura de código especificamente para permitir mais encadeamento de métodos . O exemplo mais proeminente é o jQuery , que ainda continua sendo a biblioteca JS mais popular do mundo. O design principal do jQuery é um único superobjeto com dezenas de métodos, todos retornando o mesmo tipo de objeto para que possamos continuar encadeando . Existe até um nome para esse estilo de programação: interfaces fluentes .
Infelizmente, apesar de toda a sua fluência, o encadeamento de métodos por si só não pode acomodar outras sintaxes do JavaScript: chamadas de função, aritmética, literais de array/objeto, await
e yield
, etc.
O operador pipe tenta casar a conveniência e facilidade do encadeamento de métodos com a ampla aplicabilidade do aninhamento de expressões .
A estrutura geral de todos os operadores de pipe é value |>
e1 |>
e2 |>
e3 , onde e1 , e2 , e3 são todas expressões que tomam valores consecutivos como parâmetros. O operador |>
então faz algum grau de mágica para “canalizar” value
do lado esquerdo para o lado direito.
Continuando este código do mundo real profundamente aninhado do React:
console . log (
chalk . dim (
`$ ${ Object . keys ( envars )
. map ( envar =>
` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
} ` ,
'node' ,
args . join ( ' ' ) ) ) ;
…podemos desembaraçá- lo usando um operador de pipe e um token de espaço reservado ( %
) substituindo o valor da operação anterior:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
Agora, o leitor humano pode encontrar rapidamente os dados iniciais (o que era a expressão mais interna, envars
) e depois ler linearmente , da esquerda para a direita , cada transformação nos dados.
Alguém poderia argumentar que o uso de variáveis temporárias deveria ser a única maneira de desembaraçar código profundamente aninhado. Nomear explicitamente a variável de cada etapa faz com que algo semelhante ao encadeamento de métodos aconteça, com benefícios semelhantes à leitura e escrita de código.
Por exemplo, usando nosso exemplo anterior modificado do mundo real do React:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
…uma versão usando variáveis temporárias ficaria assim:
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 ) ;
Mas há razões pelas quais encontramos expressões profundamente aninhadas no código umas das outras no mundo real o tempo todo , em vez de linhas de variáveis temporárias. E há razões pelas quais as interfaces fluentes baseadas em cadeia de métodos de jQuery, Mocha e assim por diante ainda são populares .
Muitas vezes é muito tedioso e prolixo escrever código com uma longa sequência de variáveis temporárias e de uso único. É indiscutivelmente até tedioso e visualmente barulhento para um ser humano ler também.
Se nomear é uma das tarefas mais difíceis na programação, então os programadores inevitavelmente evitarão nomear variáveis quando perceberem que seu benefício é relativamente pequeno.
Pode-se argumentar que o uso de uma única variável mutável com um nome curto reduziria o prolixo das variáveis temporárias, alcançando resultados semelhantes aos do operador pipe.
Por exemplo, nosso exemplo anterior modificado do mundo real do React poderia ser reescrito assim:
let _ ;
_ = Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' ) ;
_ = `$ ${ _ } ` ;
_ = chalk . dim ( _ , 'node' , args . join ( ' ' ) ) ;
_ = console . log ( _ ) ;
Mas códigos como esse não são comuns em códigos do mundo real. Uma razão para isso é que variáveis mutáveis podem mudar inesperadamente , causando bugs silenciosos que são difíceis de encontrar. Por exemplo, a variável pode ser referenciada acidentalmente em um encerramento. Ou pode ser reatribuído por engano dentro de uma expressão.
// 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 ( _ ) ;
Esse problema não aconteceria com o operador de pipe. O token do tópico não pode ser reatribuído e o código fora de cada etapa não pode alterar sua ligação.
let _ ;
_ = one ( )
| > double ( % )
| > Promise . resolve ( ) . then ( ( ) =>
// This prints 2, as intended.
console . log ( % ) ) ;
_ = one ( ) ;
Por esse motivo, o código com variáveis mutáveis também é mais difícil de ler. Para determinar o que a variável representa em qualquer ponto, você deve pesquisar em todo o escopo anterior os locais onde ela foi reatribuída .
A referência de tópico de um pipeline, por outro lado, tem um escopo lexical limitado e sua ligação é imutável dentro do seu escopo. Não pode ser reatribuído acidentalmente e pode ser usado com segurança em fechamentos.
Embora o valor do tópico também mude a cada etapa do pipeline, apenas verificamos a etapa anterior do pipeline para entendê-la, resultando em um código mais fácil de ler.
Outro benefício do operador pipe sobre sequências de instruções de atribuição (sejam com variáveis temporárias mutáveis ou imutáveis) é que elas são expressões .
Expressões de pipe são expressões que podem ser retornadas diretamente, atribuídas a uma variável ou usadas em contextos como expressões JSX.
O uso de variáveis temporárias, por outro lado, requer sequências de instruções.
Gasodutos | Variáveis Temporárias |
---|---|
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 >
) ; |
Havia duas propostas concorrentes para o operador de pipe: Hack pipes e F# pipes. (Antes disso, havia uma terceira proposta para uma “combinação inteligente” das duas primeiras propostas, mas ela foi retirada, uma vez que a sua sintaxe é estritamente um superconjunto de uma das propostas.)
As duas propostas de pipe diferem ligeiramente sobre qual é a “mágica”, quando soletramos nosso código ao usar |>
.
Ambas as propostas reutilizam conceitos de linguagem existentes: Hack pipes são baseados no conceito da expressão , enquanto F# pipes são baseados no conceito da função unária .
Expressões de tubulação e funções unárias de tubulação têm compensações pequenas e quase simétricas.
Na sintaxe de pipe da linguagem Hack , o lado direito do pipe é uma expressão que contém um espaço reservado especial, que é avaliado com o espaço reservado vinculado ao resultado da avaliação da expressão do lado esquerdo. Ou seja, escrevemos value |> one(%) |> two(%) |> three(%)
para canalizar value
através das três funções.
Pró: O lado direito pode ser qualquer expressão , e o espaço reservado pode ir para qualquer lugar que qualquer identificador de variável normal possa ir, para que possamos canalizar para qualquer código que desejarmos, sem quaisquer regras especiais :
value |> foo(%)
para chamadas de função unárias,value |> foo(1, %)
para chamadas de função n-árias,value |> %.foo()
para chamadas de método,value |> % + 1
para aritmética,value |> [%, 0]
para literais de array,value |> {foo: %}
para literais de objeto,value |> `${%}`
para literais de modelo,value |> new Foo(%)
para construção de objetos,value |> await %
para aguardar promessas,value |> (yield %)
para gerar valores do gerador,value |> import(%)
para chamar palavras-chave semelhantes a funções, Contra: A tubulação através de funções unárias é um pouco mais detalhada com canais Hack do que com canais F#. Isso inclui funções unárias que foram criadas por bibliotecas de currying de funções como Ramda, bem como funções de seta unárias que executam desestruturações complexas em seus argumentos: Hack pipes seriam um pouco mais detalhados com um sufixo de chamada de função explícito (%)
.
(A desestruturação complexa do valor do tópico será mais fácil quando as expressões progredirem, pois você poderá fazer atribuição/desestruturação de variáveis dentro de um corpo de tubo.)
Na sintaxe de pipe da linguagem F# , o lado direito do pipe é uma expressão que deve ser avaliada como uma função unária , que é então chamada tacitamente com o valor do lado esquerdo como seu único argumento . Ou seja, escrevemos value |> one |> two |> three
para canalizar value
através das três funções. left |> right
torna-se right(left)
. Isso é chamado de programação tácita ou estilo sem pontos.
Por exemplo, usando nosso exemplo anterior modificado do mundo real do React:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
…uma versão usando pipes F# em vez de pipes Hack ficaria assim:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > x => `$ ${ x } `
| > x => chalk . dim ( x , 'node' , args . join ( ' ' ) )
| > console . log ;
Pró: A restrição que o lado direito deve resolver para uma função unária nos permite escrever tubos muito concisos quando a operação que queremos executar é uma chamada de função unária :
value |> foo
para chamadas de função unárias. Isso inclui funções unárias que foram criadas por bibliotecas de curry de funções como Ramda, bem como funções de seta unárias que executam desestruturações complexas em seus argumentos: pipes F# seriam um pouco menos detalhados com uma chamada de função implícita (no (%)
).
Contra: A restrição significa que quaisquer operações executadas por outra sintaxe devem ser um pouco mais detalhadas, envolvendo a operação em uma função de seta unária:
value |> x=> x.foo()
para chamadas de método,value |> x=> x + 1
para aritmética,value |> x=> [x, 0]
para literais de array,value |> x=> ({foo: x})
para literais de objeto,value |> x=> `${x}`
para literais de modelo,value |> x=> new Foo(x)
para construção de objetos,value |> x=> import(x)
para chamar palavras-chave semelhantes a funções,Até mesmo chamar funções nomeadas requer encapsulamento quando precisamos passar mais de um argumento :
value |> x=> foo(1, x)
para chamadas de função n-árias. Contra: as operações await
e yield
têm como escopo a função que as contém e, portanto, não podem ser tratadas apenas por funções unárias . Se quisermos integrá-los em uma expressão pipe, await
e yield
devem ser tratados como casos de sintaxe especiais :
value |> await
por aguardar promessas, evalue |> yield
para gerar valores do gerador. Tanto Hack pipes quanto F# pipes, respectivamente, impõem uma pequena taxa de sintaxe em diferentes expressões:
Hack pipes sobrecarregam levemente apenas chamadas de função unárias e
Os pipes F# sobrecarregam ligeiramente todas as expressões, exceto chamadas de função unárias.
Em ambas as propostas, a sintaxe taxa por expressão taxada é pequena ( ambos (%)
e x=>
são apenas três caracteres ). Contudo, o imposto é multiplicado pela prevalência das suas respectivas expressões tributadas. Por conseguinte, poderá fazer sentido impor um imposto sobre as expressões menos comuns e optimizar a favor das expressões mais comuns .
Chamadas de funções unárias são em geral menos comuns que todas as expressões, exceto funções unárias. Em particular, a chamada de método e a chamada de função n-ária sempre serão populares ; na frequência geral, a chamada de função unária é igual ou excedida apenas por esses dois casos - e muito menos por outras sintaxes onipresentes, como literais de array , literais de objeto e operações aritméticas . Este explicador contém vários exemplos reais dessa diferença na prevalência.
Além disso, várias outras novas sintaxes propostas, como chamada de extensão , expressões do e literais de registro/tupla , provavelmente também se tornarão difundidas no futuro . Da mesma forma, as operações aritméticas também se tornariam ainda mais comuns se o TC39 padronizasse a sobrecarga do operador . Desembaraçar as expressões dessas sintaxes futuras seria mais fluente com canais Hack em comparação com canais F#.
A taxa de sintaxe de Hack pipes em chamadas de funções unárias (ou seja, o (%)
para invocar a função unária do lado direito) não é um caso especial : ele simplesmente escreve explicitamente código comum , da maneira que normalmente faríamos sem um pipe.
Por outro lado, pipes F# exigem que distingamos entre “código que resolve uma função unária” versus “qualquer outra expressão” – e lembremos de adicionar o wrapper de função de seta em torno do último caso.
Por exemplo, com canais Hack, value |> someFunction + 1
é uma sintaxe inválida e falhará antecipadamente . Não há necessidade de reconhecer que someFunction + 1
não será avaliado como uma função unária. Mas com pipes F#, value |> someFunction + 1
ainda é uma sintaxe válida - apenas falhará tarde no tempo de execução , porque someFunction + 1
não pode ser chamado.
O grupo campeão de tubos apresentou tubos F# para o Estágio 2 ao TC39 duas vezes . Não teve sucesso em avançar para o Estágio 2 em ambas as vezes. Ambos os pipes F# (e o aplicativo de função parcial (PFA)) enfrentaram forte resistência de vários outros representantes do TC39 devido a várias preocupações. Estes incluíram:
await
.Essa resistência ocorreu fora do grupo de campeões de tubos. Consulte HISTORY.md para obter mais informações.
É crença do grupo campeão de pipe que qualquer operador de pipe é melhor que nenhum, para linearizar facilmente expressões profundamente aninhadas sem recorrer a variáveis nomeadas. Muitos membros do grupo campeão acreditam que os tubos Hack são ligeiramente melhores que os tubos F#, e alguns membros do grupo campeão acreditam que os tubos F# são ligeiramente melhores que os tubos Hack. Mas todos no grupo de campeões concordam que as flautas F# encontraram muita resistência para serem capazes de ultrapassar o TC39 em um futuro próximo.
Para enfatizar, é provável que uma tentativa de mudar dos pipes Hack de volta para os pipes F# resultará no fato de o TC39 nunca concordar com nenhum canal. A sintaxe PFA também enfrenta uma batalha difícil no TC39 (consulte HISTORY.md). Muitos membros do grupo de campeões de pipe acham que isso é lamentável e estão dispostos a lutar novamente mais tarde por uma mixagem dividida em F#-pipe e pela sintaxe PFA. Mas existem alguns representantes (incluindo implementadores de mecanismos de navegador) fora do Pipe Champion Group que geralmente são contra o incentivo à programação tácita (e à sintaxe PFA), independentemente dos Hack pipes.
(Um rascunho de especificação formal está disponível.)
A referência do tópico %
é um operador nulo . Ele atua como um espaço reservado para um valor de tópico e tem escopo lexical e é imutável .
%
não é uma escolha final (O token preciso para a referência do tópico não é final . %
poderia ser ^
, ou muitos outros tokens. Planejamos definir qual token real usar antes de avançar para o Estágio 3. No entanto, %
parece ser o menos sintaticamente problemático, e também se assemelha aos espaços reservados para strings de formato printf e aos literais de função #(%)
do Clojure .)
O operador pipe |>
é um operador infixo que forma uma expressão pipe (também chamada de pipeline ). Ele avalia seu lado esquerdo (a cabeça do tubo ou a entrada do tubo ), vincula imutavelmente o valor resultante (o valor do tópico ) à referência do tópico e, em seguida, avalia seu lado direito (o corpo do tubo ) com essa ligação. O valor resultante do lado direito torna-se o valor final de toda a expressão do pipe (a saída do pipe ).
A precedência do operador pipe é a mesma que:
=>
;=
, +=
, etc.;yield
dos operadores geradores e yield *
; É mais restrito do que apenas o operador vírgula ,
.
É mais flexível do que todos os outros operadores.
Por exemplo, v => v |> % == null |> foo(%, 0)
agruparia em v => (v |> (% == null) |> foo(%, 0))
,
que por sua vez é equivalente a v => foo(v == null, 0)
.
Um corpo de pipe deve usar seu valor de tópico pelo menos uma vez . Por exemplo, value |> foo + 1
é uma sintaxe inválida porque seu corpo não contém uma referência de tópico. Esse design ocorre porque a omissão da referência do tópico no corpo de uma expressão de canal é quase certamente um erro acidental do programador.
Da mesma forma, uma referência de tópico deve estar contida no corpo de um tubo. Usar uma referência de tópico fora do corpo de um pipe também é uma sintaxe inválida .
Para evitar agrupamento confuso, é uma sintaxe inválida usar outros operadores que tenham precedência semelhante (ou seja, a seta =>
, o operador condicional ternário ?
:
, os operadores de atribuição e o operador yield
) como um pipe head ou body . Ao usar |>
com esses operadores, devemos usar parênteses para indicar explicitamente qual agrupamento está correto. Por exemplo, a |> b ? % : c |> %.d
é uma sintaxe inválida; deve ser corrigido para a |> (b ? % : c) |> %.d
ou a |> (b ? % : c |> %.d)
.
Por último, as ligações de tópicos dentro do código compilado dinamicamente (por exemplo, com eval
ou new Function
) não podem ser usadas fora desse código. Por exemplo, v |> eval('% + 1')
gerará um erro de sintaxe quando a expressão eval
for avaliada em tempo de execução.
Não existem outras regras especiais .
Um resultado natural dessas regras é que, se precisarmos interpor um efeito colateral no meio de uma cadeia de expressões de pipe, sem modificar os dados que estão sendo canalizados, então poderíamos usar uma expressão de vírgula , como with value |> (sideEffect(), %)
. Como de costume, a expressão vírgula será avaliada no lado direito %
, essencialmente passando pelo valor do tópico sem modificá-lo. Isso é especialmente útil para depuração rápida: value |> (console.log(%), %)
As únicas alterações nos exemplos originais foram dedentação e remoção de comentários.
Em 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 ] ;
Em node/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 ) ;
Em sublinhado.js:
// Status quo
return filter ( obj , negate ( cb ( predicate ) ) , context ) ;
// With pipes
return cb ( predicate ) | > _ . negate ( % ) | > _ . filter ( obj , % , context ) ;
De ramda.js.