點燈坊

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

使用 pair() 包裹前一個 Function 回傳值

Sam Xiao's Avatar 2020-04-30

使用 Function Pipeline 時,有時會想使用前兩個 Function 的回傳值,但因為橫跨在不同 Function 彼此看不到對方,此時可使用 Pair 將前一個 Function 回傳值包起來傳到下一個 Function。

Version

macOS Catalina 10.15.4
VS Code 1.44.2
Quokka 1.0.289
Ramda 0.27.0

Dependent Function Call

import { pipe, prop, find, flip, propEq } from 'ramda'

let categories = [
  { id: 1, value: 'FP' },
  { id: 2, value: 'FRP' },
  { id: 3, value: 'JS' }
]

let fetchCategory = pipe(
  propEq('id'),
  flip(find)(categories)
)

let fetchBook = _ => 
  ({ title: 'FP in JavaScript',
    price: 100,
    categoryId: 1 })

let f = pipe(
  fetchBook,
  prop('categoryId'),
  fetchCategory,
  prop('value')
)

f() // ?

14 行

let fetchBook = _ => 
  ({ title: 'FP in JavaScript',
    price: 100,
    categoryId: 1 })

fetchBook() 只回傳 categoryId

第 9 行

let fetchCategory = pipe(
  propEq('id'),
  flip(find)(categories)
)

若要取得 category,還要呼料 fetchCategory()

19 行

let f = pipe(
  fetchBook,
  prop('categoryId'),
  fetchCategory,
  prop('value')
)

若最後只想取得 category(),可使用 function pipeline 組合 fetchBook()fetchCategory()

pair000

Return Array

import { pipe, prop, find, propEq, flip } from 'ramda'

let categories = [
  { id: 1, value: 'FP' },
  { id: 2, value: 'FRP' },
  { id: 3, value: 'JS' }
]

let fetchCategory = pipe(
  propEq('id'),
  flip(find)(categories)
)

let fetchBook = _ => 
  ({ title: 'FP in JavaScript',
    price: 100,
    categoryId: 1 })

let f = pipe(
  fetchBook,
  x => [fetchCategory(x.categoryId), x],
  ([x, y]) => [y.title, x.value]
)

f() // ?

若需求改變,我們不只希望回傳 category 而已,而是同時回傳 titlecategory

21 行

x => [fetchCategory(x.categoryId), x],

title 來自於 fetchBook(),而 category 來自於 fetchCategory(),這對 function pipeline 就比較尷尬,因為橫跨在不同 function 內,彼此看不到對方。

實務上會使用 [] 將前一個 function 回傳值包起來傳到下一個 function,讓下一個 function 也能讀取得到。

22 行

([x, y]) => [y.title, x.value]

由於前一個 function 包成 array 回傳,因此使用 ES6 的 array destructuring 解開,再整理成我們要的格式回傳。

pair001

Return Pair

import { pipe, prop, find, chain, flip, identity } from 'ramda'
import { propEq, converge, pair, nth,  } from 'ramda'

let categories = [
  { id: 1, value: 'FP' },
  { id: 2, value: 'FRP' },
  { id: 3, value: 'JS' }
]

let fetchCategory = pipe(
  propEq('id'),
  flip(find)(categories)
)

let fetchBook = _ => 
  ({ title: 'FP in JavaScript',
    price: 100,
    categoryId: 1 })

let f = pipe(
  fetchBook,
  converge(pair, [
    pipe(prop('categoryId'), fetchCategory), 
    identity
  ]),
  converge(pair, [
    pipe(nth(1), prop('title')), 
    pipe(nth(0), prop('value'))
  ])
)

f() // ?

22 行

converge(pair, [
  pipe(prop('categoryId'), fetchCategory), 
  identity
]),

回傳兩個 element 的 array 其實就是 pair,可透過 converge() 使其 point-free。

26 行

converge(pair, [
  pipe(nth(1), prop('title')), 
  pipe(nth(0), prop('value'))
])

最後回傳也是 pair,亦可再次使用 converge() 使其 point-free。

pair004

chain()

import { pipe, prop, find, chain, flip } from 'ramda'
import { propEq, converge, pair, nth,  } from 'ramda'

let categories = [
  { id: 1, value: 'FP' },
  { id: 2, value: 'FRP' },
  { id: 3, value: 'JS' }
]

let fetchCategory = pipe(
  propEq('id'),
  flip(find)(categories)
)

let fetchBook = _ => 
  ({ title: 'FP in JavaScript',
    price: 100,
    categoryId: 1 })

let f = pipe(
  fetchBook,
  chain(pair, pipe(prop('categoryId'), fetchCategory)),
  converge(pair, [
    pipe(nth(1), prop('title')), 
    pipe(nth(0), prop('value'))
  ])
)

f() // ?

22 行

chain(pair, pipe(prop('categoryId'), fetchCategory)),

當出現 converge(f, [g, identity]) 的 pattern 時,可重構成 chain(f, g)

pair002

Extract Function

import { pipe, prop, find, chain, flip } from 'ramda'
import { propEq, converge, pair, nth } from 'ramda'

let categories = [
  { id: 1, value: 'FP' },
  { id: 2, value: 'FRP' },
  { id: 3, value: 'JS' }
]

let fetchCategory = pipe(
  propEq('id'),
  flip(find)(categories)
)

let fetchBook = _ => 
  ({ title: 'FP in JavaScript',
    price: 100,
    categoryId: 1 })

let getTitle = pipe(
  nth(1), 
  prop('title')
)

let getValue = pipe(
  nth(0), 
  prop('value')
)

let getCategory = pipe(
  prop('categoryId'), 
  fetchCategory
)

let f = pipe(
  fetchBook,
  chain(pair, getCategory),
  converge(pair, [getTitle, getValue])
)

f() // ?

35 行

let f = pipe(
  fetchBook,
  chain(pair, getCategory),
  converge(pair, [getTitle, getValue])
)

pipe() 部分抽成獨立 function 後,f() 的可讀性更高了。

pair003

Conclusion

  • 以 array 或 pair 將上一個 function 的值包起來傳到下一個 function 的靈感來自於 Promise.all(),它會將 synchronous 與 asynchronous 值包起來傳到下一個 then(),此技巧也能用於 sync function

Reference

Ramda, pipe()
Ramda, prop()
Ramda, find()
Ramda, chain()
Ramda, flip()
Ramda, propEq()
Ramda, converge()
Ramda, pair()
Ramda, nth()