點燈坊

學而時習之,不亦悅乎

如何自行實作 compose() 與 pipe() ?

Sam Xiao's Avatar 2019-06-15

compose()pipe() 是 FP 最重要兩個 Function,其中 Function Composition 就是由這兩個 Function 展開,實務上都是直接使用 Ramda 或 Crocks 所提供的 compose()pipe(),能自己使用 ECMAScript 實現嗎 ?

Version

VS Code 1.35.1
Quokka 1.0.227
Ramda 0.26.1

Ramda

import { compose, filter, map } from 'ramda';

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
];

// fn :: Number -> [a] -> [b]
let fn = price => compose(
  map(x => x.title),
  filter(x => x.price === price),
);

fn(300)(data); // ?

Ramd 初體驗 一文中,曾經使用 Ramda 最具代表性的 compose()filter()map() 實現 function composition。

其中 compose() 為 Ramda 所提供。

pipe000

compose()

import { filter, map } from 'ramda';

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
];

// compose :: ((y → z), (x → y), …, (o → p), ((a, b, …, n) → o)) → ((a, b, …, n) → z)
let compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));

// fn :: Number -> [a] -> [b]
let fn = price => compose(
  map(x => x.title),
  filter(x => x.price === price),
);

fn(300)(data); // ?

第 1 行

import { filter, map } from 'ramda';

沒使用 Ramda 所提供的 compose()

第 9 行

// compose :: ((y → z), (x → y), …, (o → p), ((a, b, …, n) → o)) → ((a, b, …, n) → z)
let compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));

自行使用 ECMAScript 內建的 reduce() 實現 compose()

(...fns)

使用 ES6 的 rest parameter,表示 compose() 的 argument 個數無限,且 fns 為 array,可大膽使用 Array.prototype 下的 method。

(f, g) => (...args) => f(g(...args))

g 為目前 function,f 為組合後 function,因為結果仍是 function,所以為 (...args) => f(g(...args))

reduce() 為 array 由左至右,因此最後 element 為 g,相當於 compose()由右至左

pipe001

pipe()

import { filter, map } from 'ramda';

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
];

// pipe :: (((a, b, …, n) → o), (o → p), …, (x → y), (y → z)) → ((a, b, …, n) → z)
let pipe = (...fns) => fns.reduceRight((f, g) => (...args) => f(g(...args)));

let fn = price => pipe(
  filter(x => x.price === price),
  map(x => x.title),
);

fn(300)(data); // ?

第 1 行

import { filter, map } from 'ramda';

沒使用 Ramda 所提供的 pipe()

第 9 行

// (((a, b, …, n) → o), (o → p), …, (x → y), (y → z)) → ((a, b, …, n) → z)
let pipe = (...fns) => fns.reduceRight((f, g) => (...args) => f(g(...args)));

pipe()由左至右 運算,也就是 array 由右至左,因此使用 reduceRight()

pipe002

Conclusion

  • compose()pipe() 一直被認為是黑魔法,事實上只是將 function 加以 reduce() 而已,能自己寫過一次,就不會覺得那麼遙不可及了

See Also

Ramd 初體驗

Reference

MDN, Rest parameters
MDN, reduce()
MDN, reduceRight()
Ramda, compose()
Ramda, pipe()