Pipe Operator (|>) for JavaScript 提案給 js 增加了 Pipe 語法,這次結合 A pipe operator for JavaScript: introduction and use cases 文章一起深入瞭解這個提案。
概述
Pipe 語法可以將函式呼叫按順序打平。如下方函式,存在三層巢狀,但我們解讀時需要由內而外閱讀,因為呼叫順序是由內而外的:
const y = h(g(f(x)))
Pipe 可以將其轉化為正常順序:
const y = x |> f(%) |> g(%) |> h(%)
Pipe 語法有兩種風格,分別來自 Microsoft 的 F#) 與 Facebook 的 Hack)。
之所以介紹這兩個,是因為 js 提案首先要決定 “借鑑” 哪種風格。js 提案最終採用了 Hack 風格,因此我們最好把 F# 與 Hack 的風格都瞭解一下,並對其優劣做一個對比,才能知其所以然。
Hack Pipe 語法
Hack 語法相對冗餘,在 Pipe 時使用 %
傳遞結果:
'123.45' |> Number(%)
這個 %
可以用在任何地方,基本上原生 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# 語法相對精簡,預設不使用額外符號:
'123.45' |> Number
但在需要顯式宣告引數時,為了解決上一個 Pipe 結果符號從哪來的問題,寫起來反而更為複雜:
2 |> $ => add2(1, $)
await 關鍵字 - Hack 優
F# 在 await
yield
時需要特殊語法支援,而 Hack 可以自然的使用 js 內建關鍵字。
// Hack
value |> await %
// F#
value |> await
F# 程式碼看上去很精簡,但實際上付出了高昂的代價 - await
是一個僅在 Pipe 語法存在的關鍵字,而非普通 await
關鍵字。如果不作為關鍵字處理,執行邏輯就變成了 await(value)
而不是 await value
。
解構 - F# 優
正因為 F# 繁瑣的變數宣告,反而使得在應對解構場景時得心應手:
// F#
value |> ({ a, b }) => someFunction(a, b)
// Hack
value |> someFunction(%.a, %.b)
Hack 也不是沒有解構手段,只是比較繁瑣。要麼使用立即呼叫函式表示式 IIFE:
value |> (({ a, b }) => someFunction(a, b))(%)
要麼使用 do
關鍵字:
value |> do { const { a, b } = %; someFunction(a, b) }
但 Hack 雖敗猶榮,因為解決方法都使用了 js 原生提供的語法,所以反而體現出與 js 已有生態親和性更強,而 F# 之所以能優雅解決,全都歸功於自創的語法,這些語法雖然甜,但割裂了 js 生態,這是 F# like 提案被放棄的重要原因之一。
潛在改進方案
雖然選擇了 Hack 風格,但 F# 與 Hack 各有優劣,所以列了幾點優化方案。
利用 Partial Application Syntax 提案降低 F# 傳參複雜度
F# 被詬病的一個原因是傳參不如 Hack 簡單:
// Hack
2 |> add2(1, %)
// F#
2 |> $ => add2(1, $)
但如果利用處於 stage1 的提案 Partial Application Syntax 可以很好的解決問題。
這裡就要做一個小插曲了。js 對柯里化沒有原生支援,但 Partial Application Syntax 提案解決了這個問題,語法如下:
const add = (x, y) => x + y;
const addOne = add~(1, ?);
addOne(2); // 3
即利用 fn~(?, arg)
的語法,將任意函式柯里化。這個特性解決 F# 傳參複雜問題簡直絕配,因為 F# 的每一個 Pipe 都要求是一個函式,我們可以將要傳參的地方記為 ?
,這樣返回值還是一個函式,完美符合 F# 的語法:
// F#
2 |> add~(1, ?)
上面的例子拆開看就是:
const addOne = add~(1, ?)
2 |> addOne
想法很美好,但 Partial Application Syntax 得先落地。
融合 F# 與 Hack 語法
在簡單情況下使用 F#,需要利用 %
傳參時使用 Hack 語法,兩者混合在一起寫就是:
const resultArray = inputArray
|> filter(%, str => str.length >= 0) // Hack
|> map(%, str => '['+str+']') // Hack
|> console.log // F#
不過這個 提案 被廢棄了。
創造一個新的操作符
如果用 |>
表示 Hack 語法,用 |>>
表示 F# 語法呢?
const resultArray = inputArray
|> filter(%, str => str.length >= 0) // Hack
|> map(%, str => '['+str+']') // Hack
|>> console.log // F#
也是看上去很美好,但這個特性連提案都還沒有。
如何用現有語法模擬 Pipe
即便沒有 Pipe Operator (|>) for JavaScript 提案,也可以利用 js 現有語法模擬 Pipe 效果,以下是幾種方案。
Function.pipe()
利用自定義函式構造 pipe 方法,該語法與 F# 比較像:
const resultSet = Function.pipe(
inputSet,
$ => filter($, x => x >= 0)
$ => map($, x => x * 2)
$ => new Set($)
)
缺點是不支援 await
,且存在額外函式呼叫。
使用中間變數
說白了就是把 Pipe 過程拆開,一步步來寫:
const filtered = filter(inputSet, x => x >= 0)
const mapped = map(filtered, x => x * 2)
const resultSet = new Set(mapped)
沒什麼大問題,就是比較冗餘,本來可能一行能解決的問題變成了三行,而且還宣告瞭三個中間變數。
複用變數
改造一下,將中間變數變成複用的:
let $ = inputSet
$ = filter($, x => x >= 0)
$ = map($, x => x * 2)
const resultSet = new Set($)
這樣做可能存在變數汙染,可使用 IIFE 解決。
精讀
Pipe Operator 語義價值非常明顯,甚至可以改變程式設計的思維方式,在序列處理資料時非常重要,因此命令列場景非常常見,如:
cat "somefile.txt" | echo
因為命令列就是典型的輸入輸出場景,而且大部分都是單輸入、單輸出。
在普通程式碼場景,特別是處理資料時也需要這個特性,大部分具有抽象思維的程式碼都進行了各種型別的管道抽象,比如:
const newValue = pipe(
value,
doSomething1,
doSomething2,
doSomething3
)
如果 Pipe Operator (|>) for JavaScript 提案通過,我們就不需要任何庫實現 pipe 動作,可以直接寫成:
const newValue = value |> doSomething1(%) |> doSomething2(%) |> doSomething3(%)
這等價於:
const newValue = doSomething3(doSomething2(doSomething1(value)))
顯然,利用 pipe 特性書寫處理流程更為直觀,執行邏輯與閱讀邏輯是一致的。
實現 pipe 函式
即便沒有 Pipe Operator (|>) for JavaScript 提案,我們也可以一行實現 pipe 函式:
const pipe = (...args) => args.reduce((acc, el) => el(acc))
但要實現 Hack 引數風格是不可能的,頂多實現 F# 引數風格。
js 實現 pipe 語法的考慮
從 提案 記錄來看,F# 失敗有三個原因:
- 記憶體效能問題。
await
特殊語法。- 割裂 js 生態。
其中割裂 js 生態是指因 F# 語法的特殊性,如果有太多庫按照其語法實現功能,可能導致無法被非 Pipe 語法場景所複用。
甚至還有部分成員反對 隱性程式設計(Tacit programming),以及柯里化提案 Partial Application Syntax,這些會使 js 支援的程式設計風格與現在差異過大。
看來處於鄙視鏈頂端的程式設計風格在 js 是否支援不是能不能的問題,而是想不想的問題。
pipe 語法的弊端
下面是普通 setState
語法:
setState(state => ({
...state,
value: 123
}))
如果改為 immer
寫法如下:
setState(produce(draft => draft.value = 123))
得益於 ts 型別自動推導,在內層 produce
裡就已經知道 value
是字串型別,此時如果輸入字串會報錯,而如果其在另一個上下文的 setState
內,型別也會隨著上下文的變化而變化。
但如果寫成 pipe 模式:
produce(draft => draft.value = 123) |> setState
因為先考慮的是如何修改資料,此時還不知道後面的 pipe 流程是什麼,所以 draft
的型別無法確定。所以 pipe 語法僅適用於固定型別的資料處理流程。
總結
pipe 直譯為管道,潛在含義是 “資料像流水線一樣被處理”,也可以形象理解為每個函式就是一個不同的管道,顯然下一個管道要處理上一個管道的資料,並將結果輸出到下一個管道作為輸入。
合適的管道數量與體積決定了一條生產線是否高效,過多的管道型別反而會使流水線零散而雜亂,過少的管道會讓流水線笨重不易擴充,這是工作中最大的考驗。
討論地址是:精讀《pipe operator for JavaScript》· Issue #395 · dt-fe/weekly
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)