Skip to content

Commit

Permalink
228
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed Feb 7, 2022
1 parent d01463f commit 751d78b
Show file tree
Hide file tree
Showing 2 changed files with 308 additions and 1 deletion.
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

前端界的好文精读,每周更新!

最新精读:<a href="./源码解读/227.%20%E7%B2%BE%E8%AF%BB%E3%80%8Azustand%20%E6%BA%90%E7%A0%81%E3%80%8B.md">227. 精读《zustand 源码》</a>
最新精读:<a href="./前沿技术/228.%E7%B2%BE%E8%AF%BB%E3%80%8Apipe%20operator%20for%20JavaScript%E3%80%8B.md">228.精读《pipe operator for JavaScript》</a>

素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)

Expand Down Expand Up @@ -184,6 +184,7 @@
- <a href="./前沿技术/224.%E7%B2%BE%E8%AF%BB%E3%80%8ARecords%20%26%20Tuples%20for%20React%E3%80%8B.md">224.精读《Records & Tuples for React》</a>
- <a href="./前沿技术/225.%E7%B2%BE%E8%AF%BB%E3%80%8AExcel%20JS%20API%E3%80%8B.md">225.精读《Excel JS API》</a>
- <a href="./前沿技术/226.%E7%B2%BE%E8%AF%BB%E3%80%8A2021%20%E5%89%8D%E7%AB%AF%E6%96%B0%E7%A7%80%E5%9B%9E%E9%A1%BE%E3%80%8B.md">226.精读《2021 前端新秀回顾》</a>
- <a href="./前沿技术/228.%E7%B2%BE%E8%AF%BB%E3%80%8Apipe%20operator%20for%20JavaScript%E3%80%8B.md">228.精读《pipe operator for JavaScript》</a>

### 设计模式

Expand Down
306 changes: 306 additions & 0 deletions 前沿技术/228.精读《pipe operator for JavaScript》.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
[Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) 提案给 js 增加了 Pipe 语法,这次结合 [A pipe operator for JavaScript: introduction and use cases](https://2ality.com/2022/01/pipe-operator.html) 文章一起深入了解这个提案。

## 概述

Pipe 语法可以将函数调用按顺序打平。如下方函数,存在三层嵌套,但我们解读时需要由内而外阅读,因为调用顺序是由内而外的:

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

Pipe 可以将其转化为正常顺序:

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

Pipe 语法有两种风格,分别来自 Microsoft 的 [F#](https://en.wikipedia.org/wiki/F_Sharp_(programming_language)) 与 Facebook 的 [Hack](https://en.wikipedia.org/wiki/Hack_(programming_language))

之所以介绍这两个,是因为 js 提案首先要决定 “借鉴” 哪种风格。js 提案最终采用了 Hack 风格,因此我们最好把 F# 与 Hack 的风格都了解一下,并对其优劣做一个对比,才能知其所以然。

### Hack Pipe 语法

Hack 语法相对冗余,在 Pipe 时使用 `%` 传递结果:

```js
'123.45' |> Number(%)
```

这个 `%` 可以用在任何地方,基本上原生 js 语法都支持:

```js
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 语法

F# 语法相对精简,默认不使用额外符号:

```js
'123.45' |> Number
```

但在需要显式声明参数时,为了解决上一个 Pipe 结果符号从哪来的问题,写起来反而更为复杂:

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

### await 关键字 - Hack 优

F# 在 `await` `yield` 时需要特殊语法支持,而 Hack 可以自然的使用 js 内置关键字。

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

F# 代码看上去很精简,但实际上付出了高昂的代价 - `await` 是一个仅在 Pipe 语法存在的关键字,而非普通 `await` 关键字。如果不作为关键字处理,执行逻辑就变成了 `await(value)` 而不是 `await value`

### 解构 - F# 优

正因为 F# 繁琐的变量声明,反而使得在应对解构场景时得心应手:

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

Hack 也不是没有解构手段,只是比较繁琐。要么使用立即调用函数表达式 IIFE:

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

要么使用 `do` 关键字:

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

但 Hack 虽败犹荣,因为解决方法都使用了 js 原生提供的语法,所以反而体现出与 js 已有生态亲和性更强,而 F# 之所以能优雅解决,全都归功于自创的语法,这些语法虽然甜,但割裂了 js 生态,这是 F# like 提案被放弃的重要原因之一。

### 潜在改进方案

虽然选择了 Hack 风格,但 F# 与 Hack 各有优劣,所以列了几点优化方案。

#### 利用 Partial Application Syntax 提案降低 F# 传参复杂度

F# 被诟病的一个原因是传参不如 Hack 简单:

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

但如果利用处于 stage1 的提案 [Partial Application Syntax](https://github.com/tc39/proposal-partial-application) 可以很好的解决问题。

这里就要做一个小插曲了。js 对柯里化没有原生支持,但 [Partial Application Syntax](https://github.com/tc39/proposal-partial-application) 提案解决了这个问题,语法如下:

```js
const add = (x, y) => x + y;
const addOne = add~(1, ?);
addOne(2); // 3
```
即利用 `fn~(?, arg)` 的语法,将任意函数柯里化。这个特性解决 F# 传参复杂问题简直绝配,因为 F# 的每一个 Pipe 都要求是一个函数,我们可以将要传参的地方记为 `?`,这样返回值还是一个函数,完美符合 F# 的语法:
```js
// F#
2 |> add~(1, ?)
```
上面的例子拆开看就是:
```js
const addOne = add~(1, ?)
2 |> addOne
```
想法很美好,但 [Partial Application Syntax](https://github.com/tc39/proposal-partial-application) 得先落地。
#### 融合 F# 与 Hack 语法
在简单情况下使用 F#,需要利用 `%` 传参时使用 Hack 语法,两者混合在一起写就是:
```js
const resultArray = inputArray
|> filter(%, str => str.length >= 0) // Hack
|> map(%, str => '['+str+']') // Hack
|> console.log // F#
```
不过这个 [提案](https://github.com/tc39/proposal-smart-pipelines) 被废弃了。
#### 创造一个新的操作符
如果用 `|>` 表示 Hack 语法,用 `|>>` 表示 F# 语法呢?
```js
const resultArray = inputArray
|> filter(%, str => str.length >= 0) // Hack
|> map(%, str => '['+str+']') // Hack
|>> console.log // F#
```
也是看上去很美好,但这个特性连提案都还没有。
### 如何用现有语法模拟 Pipe
即便没有 [Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) 提案,也可以利用 js 现有语法模拟 Pipe 效果,以下是几种方案。
#### Function.pipe()
利用自定义函数构造 pipe 方法,该语法与 F# 比较像:
```js
const resultSet = Function.pipe(
inputSet,
$ => filter($, x => x >= 0)
$ => map($, x => x * 2)
$ => new Set($)
)
```
缺点是不支持 `await`,且存在额外函数调用。
#### 使用中间变量
说白了就是把 Pipe 过程拆开,一步步来写:
```js
const filtered = filter(inputSet, x => x >= 0)
const mapped = map(filtered, x => x * 2)
const resultSet = new Set(mapped)
```
没什么大问题,就是比较冗余,本来可能一行能解决的问题变成了三行,而且还声明了三个中间变量。
#### 复用变量
改造一下,将中间变量变成复用的:
```js
let $ = inputSet
$ = filter($, x => x >= 0)
$ = map($, x => x * 2)
const resultSet = new Set($)
```
这样做可能存在变量污染,可使用 IIFE 解决。
## 精读
Pipe Operator 语义价值非常明显,甚至可以改变编程的思维方式,在串行处理数据时非常重要,因此命令行场景非常常见,如:
```bash
cat "somefile.txt" | echo
```
因为命令行就是典型的输入输出场景,而且大部分都是单输入、单输出。
在普通代码场景,特别是处理数据时也需要这个特性,大部分具有抽象思维的代码都进行了各种类型的管道抽象,比如:
```js
const newValue = pipe(
value,
doSomething1,
doSomething2,
doSomething3
)
```
如果 [Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) 提案通过,我们就不需要任何库实现 pipe 动作,可以直接写成:
```js
const newValue = value |> doSomething1(%) |> doSomething2(%) |> doSomething3(%)
```
这等价于:
```js
const newValue = doSomething3(doSomething2(doSomething1(value)))
```
显然,利用 pipe 特性书写处理流程更为直观,执行逻辑与阅读逻辑是一致的。
### 实现 pipe 函数
即便没有 [Pipe Operator (|>) for JavaScript](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) 提案,我们也可以一行实现 pipe 函数:
```js
const pipe = (...args) => args.reduce((acc, el) => el(acc))
```
但要实现 Hack 参数风格是不可能的,顶多实现 F# 参数风格。
### js 实现 pipe 语法的考虑
从 [提案](https://github.com/tc39/proposal-pipeline-operator#tc39-has-rejected-f-pipes-multiple-times) 记录来看,F# 失败有三个原因:
- 内存性能问题。
- `await` 特殊语法。
- 割裂 js 生态。
其中割裂 js 生态是指因 F# 语法的特殊性,如果有太多库按照其语法实现功能,可能导致无法被非 Pipe 语法场景所复用。
甚至还有部分成员反对 [隐性编程(Tacit programming)](https://en.wikipedia.org/wiki/Tacit_programming),以及柯里化提案 [Partial Application Syntax](https://github.com/tc39/proposal-partial-application),这些会使 js 支持的编程风格与现在差异过大。
看来处于鄙视链顶端的编程风格在 js 是否支持不是能不能的问题,而是想不想的问题。
### pipe 语法的弊端
下面是普通 `setState` 语法:
```ts
setState(state => ({
...state,
value: 123
}))
```
如果改为 `immer` 写法如下:
```ts
setState(produce(draft => draft.value = 123))
```
得益于 ts 类型自动推导,在内层 `produce` 里就已经知道 `value` 是字符串类型,此时如果输入字符串会报错,而如果其在另一个上下文的 `setState` 内,类型也会随着上下文的变化而变化。
但如果写成 pipe 模式:
```ts
produce(draft => draft.value = 123) |> setState
```
因为先考虑的是如何修改数据,此时还不知道后面的 pipe 流程是什么,所以 `draft` 的类型无法确定。所以 pipe 语法仅适用于固定类型的数据处理流程。
## 总结
pipe 直译为管道,潜在含义是 “数据像流水线一样被处理”,也可以形象理解为每个函数就是一个不同的管道,显然下一个管道要处理上一个管道的数据,并将结果输出到下一个管道作为输入。
合适的管道数量与体积决定了一条生产线是否高效,过多的管道类型反而会使流水线零散而杂乱,过少的管道会让流水线笨重不易拓展,这是工作中最大的考验。
> 讨论地址是:[精读《pipe operator for JavaScript》· Issue #395 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/395)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))

0 comments on commit 751d78b

Please sign in to comment.