4

Pipe Operator (|>) for JavaScript proposal adds a Pipe syntax to js. This time, we combine the A pipe operator for JavaScript: introduction and use cases article to learn more about this proposal.

Overview

The Pipe syntax flattens function calls in sequence. The following function has three levels of nesting, but we need to read it from the inside out when we interpret it, because the calling order is from the inside out:

const y = h(g(f(x)))

Pipe can convert this to normal order:

const y = x |> f(%) |> g(%) |> h(%)

Pipe syntax comes in two flavors, from Microsoft's F# ) and Facebook's Hack ).

These two are introduced because the js proposal first decides which style to "borrow" from. The js proposal finally adopts the Hack style, so we'd better understand both the F# and Hack styles, and compare their pros and cons to know why.

Hack Pipe Syntax

Hack syntax is relatively redundant, use % to pass the result when Pipe:

'123.45' |> Number(%)

This % can be used anywhere, basically native js syntax supports:

value |> someFunction(1, %, 3) // function calls
value |> %.someMethod() // method call
value |> % + 1 // operator
value |> [%, 'b', 'c'] // Array literal
value |> {someProp: %} // object literal
value |> await % // awaiting a Promise
value |> (yield %) // yielding a generator value

F# Pipe syntax

F# syntax is relatively compact and does not use extra symbols by default:

'123.45' |> Number

But when you need to explicitly declare the parameters, in order to solve the problem of where the last Pipe result symbol comes from, it is more complicated to write:

2 |> $ => add2(1, $)

await keyword - Hack Advantage

F# needs special syntax support for await yield , and Hack can naturally use js built-in keywords.

// Hack
value |> await %
// F#
value |> await

F# code looks lean, but it actually comes at a high price - await is a keyword that exists only in Pipe syntax, not the normal await keyword. If not handled as a keyword, the execution logic becomes await(value) instead of await value .

Destructuring - F# Excellent

It is because of the cumbersome variable declaration of F# that it is handy when dealing with destructuring scenarios:

// F#
value |> ({ a, b }) => someFunction(a, b)
// Hack
value |> someFunction(%.a, %.b)

Hack is not without deconstruction methods, but it is more cumbersome. Either use an immediate call function expression IIFE:

value |> (({ a, b }) => someFunction(a, b))(%)

Either use the do keyword:

value |> do { const { a, b } = %; someFunction(a, b) }

However, although Hack is defeated, because the solutions all use the grammar provided by js natively, it reflects a stronger ecological affinity with js, and the reason why F# can be solved elegantly is all due to its own grammar. Although these syntaxes are sweet, they separate the js ecology, which is one of the important reasons why the F# like proposal was abandoned.

Potential Improvements

Although the Hack style is chosen, F# and Hack have their own advantages and disadvantages, so a few optimization schemes are listed.

Using the Partial Application Syntax proposal to reduce the complexity of F# parameter passing

One of the reasons F# has been criticized is that passing parameters is not as easy as hacking:

// Hack
2 |> add2(1, %)
// F#
2 |> $ => add2(1, $)

But if you use the proposal Partial Application Syntax in stage1, the problem can be solved very well.

Here's a little interlude. js has no native support for currying, but the Partial Application Syntax proposal solves this problem with the following syntax:

const add = (x, y) => x + y;
const addOne = add~(1, ?);
addOne(2); // 3

That is, use the syntax of fn~(?, arg) to curry any function. This feature is perfect for solving the complex problem of passing parameters in F#, because every Pipe in F# requires a function. We can record the place to pass parameters as ? , so the return value is still a function, which perfectly conforms to the syntax of F#:

// F#
2 |> add~(1, ?)

The above example is disassembled to see:

const addOne = add~(1, ?)
2 |> addOne

Great idea, but Partial Application Syntax to be implemented first.

Fusion of F# and Hack syntax

To use F# in a simple case, you need to use Hack syntax when passing parameters using % . The two are mixed together to write:

const resultArray = inputArray
  |> filter(%, str => str.length >= 0) // Hack
  |> map(%, str => '['+str+']') // Hack
  |> console.log // F#

However, this proposal was abandoned.

create a new operator

What if you use |> for Hack syntax and |>> for F# syntax?

const resultArray = inputArray
  |> filter(%, str => str.length >= 0) // Hack
  |> map(%, str => '['+str+']') // Hack
  |>> console.log // F#

It also looks nice, but this feature has not even been proposed yet.

How to mock Pipe with existing syntax

Even if there is no Pipe Operator (|>) for JavaScript proposal, the existing js syntax can be used to simulate the Pipe effect. The following are several solutions.

Function.pipe()

Construct the pipe method with a custom function, the syntax is similar to F#:

const resultSet = Function.pipe(
  inputSet,
  $ => filter($, x => x >= 0)
  $ => map($, x => x * 2)
  $ => new Set($)
)

The disadvantage is that await is not supported, and there are extra function calls.

use intermediate variables

To put it bluntly, it is to disassemble the Pipe process and write step by step:

const filtered = filter(inputSet, x => x >= 0)
const mapped = map(filtered, x => x * 2)
const resultSet = new Set(mapped)

There is no big problem, but it is redundant. The problem that could be solved in one line has become three lines, and three intermediate variables are also declared.

Reuse variables

Modify it to make intermediate variables reused:

let $ = inputSet
$ = filter($, x => x >= 0)
$ = map($, x => x * 2)
const resultSet = new Set($)

Doing so may have variable pollution, which can be resolved with IIFE.

intensive reading

The semantic value of Pipe Operator is very obvious, and it can even change the way of thinking of programming. It is very important when processing data serially, so command-line scenarios are very common, such as:

cat "somefile.txt" | echo

Because the command line is a typical input and output scene, and most of them are single input and single output.

This feature is also required in ordinary code scenarios, especially when processing data. Most of the code with abstract thinking has implemented various types of pipeline abstraction, such as:

const newValue = pipe(
  value,
  doSomething1,
  doSomething2,
  doSomething3
)

If Pipe Operator (|>) for JavaScript proposal is passed, we don't need any library to implement the pipe action, we can directly write:

const newValue = value |> doSomething1(%) |> doSomething2(%) |> doSomething3(%)

This is equivalent to:

const newValue = doSomething3(doSomething2(doSomething1(value)))

Obviously, it is more intuitive to use the pipe feature to write the processing flow, and the execution logic is consistent with the reading logic.

Implement the pipe function

Even without Pipe Operator (|>) for JavaScript proposal, we can implement the pipe function in one line:

const pipe = (...args) => args.reduce((acc, el) => el(acc))

But it is impossible to implement Hack parameter style, at most F# parameter style.

js implementation of pipe syntax considerations

From the proposal records, F# fails for three reasons:

  • Memory performance issues.
  • await Special syntax.
  • Split js ecology.

Among them, splitting the js ecology means that due to the particularity of F# syntax, if there are too many libraries to implement functions according to their syntax, it may not be reused by non-Pipe syntax scenarios.

There are even some members who oppose Tacit programming , and the currying proposal Partial Application Syntax , which will make the programming style supported by js too different from the current one.

It seems that the programming style at the top of the contempt chain is not a question of whether or not js is supported, but a question of whether you want it or not.

Disadvantages of pipe syntax

Here is the normal setState syntax:

setState(state => ({
  ...state,
  value: 123
}))

If it is changed to immer written as follows:

setState(produce(draft => draft.value = 123))

Thanks to the automatic derivation of the ts type, it is already known in the inner layer produce that value is a string type. At this time, if the input string is an error, if it is in another context setState , the type will also change with the context. and change.

But if written in pipe mode:

produce(draft => draft.value = 123) |> setState

Because the first consideration is how to modify the data, at this time it is not known what the subsequent pipeline process is, so the type of draft cannot be determined. So the pipe syntax is only suitable for a fixed type of data processing flow.

Summarize

Pipe is literally translated as a pipeline. The potential meaning is "data is processed like a pipeline". It can also be understood as a different pipeline for each function. Obviously, the next pipeline needs to process the data of the previous pipeline and output the result to the next one. pipe as input.

The appropriate number and volume of pipelines determine whether a production line is efficient. Too many types of pipelines will make the pipeline scattered and cluttered. Too few pipelines will make the pipeline bulky and difficult to expand. This is the biggest test in work.

The discussion address is: Intensive Reading "pipe operator for JavaScript" Issue #395 dt-fe/weekly

If you want to participate in the discussion, please click here , there are new topics every week, released on weekends or Mondays. Front-end intensive reading - help you filter reliable content.

Pay attention to front-end intensive reading WeChat public number

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 license )

黄子毅
7k 声望9.5k 粉丝