點燈坊

失くすものさえない今が強くなるチャンスよ

使用 pipeWith() 在 Pipeline 之前先執行 Transforming Function

Sam Xiao's Avatar 2020-04-04

是否曾經想在 Pipeline 中回傳 null,表示中斷 Pipeline 呢 ? 透過新的 pipeWith() 則可達成。

Version

macOS Catalina 10.15.3
VS Code 1.43.2
Quokka 1.0.285
Ramda 0.27.0

pipeWith()

import { pipeWith, isNil, unapply } from 'ramda'

let f1 = x => {
  if (x === 1) return null
  return x + 1
}

let f2 = x => {
  if (x === 3) return null
  return x + 3
}

let f3 = x => y => x + y

let notNil = (f, x) => isNil(x) ? x : f(x)

let f = pipeWith(notNil)([
  f1,
  f2,
  f3(3)
])

f(1) // ?
f(2) // ?
f(3) // ?

第 3 行

let f1 = x => {
  if (x === 1) return null
  return x + 1
}

若 input 為 1 則 return null 終止 pipeline,否則繼續 x + 1

第 8 行

let f2 = x => {
  if (x === 3) return null
  return x + 3
}

若由 f1() 計算的結果為 3 則 return null 終止 pipeline,否則繼續 x + 3

我們發現 f1()f2() 都有終止 pipeline 的需求,這在傳統 pipe() 無法實現

17 行

let f = pipeWith(notNil)([
  f1,
  f2,
  f3(3)
])

pipeWith() 的第一個 argument 為 transforming function,當 pipeline 執行第二個之後的 function 前,會先執行該 function,因此可判斷是否為 null

pipeWith()
((* → *), [((a, b, …, n) → o), (o → p), …, (x → y), (y → z)]) → ((a, b, …, n) → z)
每個 pipeline 會先經過 transforming function 運算,才會執行下一個 pipeline

* -> *:transforming function

((* → *), [((a, b, …, n) → o), (o → p), …, (x → y), (y → z)]) → ((a, b, …, n) → z):data 為 function array,為要 pipeline 的 function

((a, b, …, n) → z):回傳為新 function

15 行

let notNil = (f, x) => isNil(x) ? x : f(x)

notNil() 為 transforming function:

  • 第一個 argument 為下一個 pipeline function
  • 第二個 argument 為上一個 pipeline function 所回傳值

因此 notNil() 可判斷 null 是否繼續 pipeline。

with000

pipeN()

import { pipeWith, isNil, unapply } from 'ramda'

let f1 = x => {
  if (x === 1) return null
  return x + 1
}

let f2 = x => {
  if (x === 3) return null
  return x + 3
}

let f3 = x => y => x + y

let notNil = (f, x) => isNil(x) ? x : f(x)

let pipeN = unapply(pipeWith(notNil))

let f = pipeN(
  f1,
  f2,
  f3(3)
)

f(1) // ?
f(2) // ?
f(3) // ?

pipeWith() 雖然好用,但有兩個問題:

  • 每次都要傳入 transforming function
  • Data 為 function array,與傳統 pipe() 習慣不一樣

15 行

let notNil = (f, x) => isNil(x) ? x : f(x)

let pipeN = unapply(pipeWith(notNil))

pipeWith()notNil() 組合成 pipeN(),為了維持 pipe() 的 variadic function 特性,特別使用 unapply() 轉換。

19 行

let f = pipeN(
  f1,
  f2,
  f3(3)
)

如此 pipeN() 就可類似 pipe() 的使用方式,且遇到 null 還會終止運算。

with001

pipe() + andThen()

import { pipe, andThen } from 'ramda'

let add = x => y => x + y
let addAsync = x => async y => x + y

let f = pipe(
  addAsync(2),
  andThen(add(3)),
  andThen(addAsync(4))
)

f(1) // ?

第 3 行

let add = x => y => x + y
let addAsync = x => async y => x + y

add() 為 sync function,而 addAsync() 為 async function,

第 6 行

let f = pipe(
  addAsync(2),
  andThen(add(3)),
  andThen(addAsync(4))
)

若想同時 pipeline sync function 與 async function,則第二個 function 之後都要加上 andThen()

with002

pipeP()

import { pipeP, add } from 'ramda'
import { async2 } from 'wink-fp'

let addAsync = async2(add)

let f = pipeP(
  addAsync(2),
  add(3),
  addAsync(4)
)

f(1) // ?

也因為一堆 function 都要加上 andThen(),Ramda 提出了 pipeP(),專門應付 sync function 與 async function 的組合。

pipeP()
((a → Promise b), (b → Promise c), …, (y → Promise z)) → (a → Promise z)
若第一個 function 為 async function,可使用 pipeP() 組合後續 sync function 或 async function

with003

pipeWith() + andThen()

import { pipeWith, andThen, unapply, add } from 'ramda'
import { async2 } from 'wink-fp'

let addAsync = async2(add)

let pipeP = unapply(pipeWith(andThen))

let f = pipeP(
  addAsync(2),
  add(3),
  addAsync(4)
)

f(1) // ?

pipeP() 在實務上很好用,只可惜在 0.26.0 被列為 deprecated,因為被 pipeWith() 所取代。

pipeWith() 雖然更萬用,但必須將 function 以 array 傳入,與傳統 pipe() 習慣不同。

我們可將 pipeWith()andThen() 組合,最後以 unapply() 轉成 variadic function。

with004

Conclusion

  • pipeWith() 實務上很少直接拿來使用,但用來產生其他 pipeX() 系列 higher order function 則非常好用

Reference

Ramda, pipeWith()
Ramda, isNil()
Ramda, unapply()
Ramda, pipe()
Ramda, andThen()
Ramda, pipeP()
Ramda, add()