點燈坊

學而時習之,不亦悅乎

使用 chain() 實現 flatMap()

Sam Xiao's Avatar 2019-08-06

chain() 屬於 Ramda 較進階 Function,威力強大。當 Data 為 Nested Array 時,其結果將攤平為一層 Array,俗稱 flatMap()

Version

macOS Mojave 10.14.5
VS Code 1.35.0
Quokka 1.0.224
ECMAScript 2019
Ramda 0.26.1

Imperative

let data = [
  { title: 'Secrets of the JavaScript Ninja', 
    authors: ['John Resig', 'Bear Bibeault'] },
  { title: 'RxJS in Action', 
    authors: ['Paul P.Daniels', 'Luis Atencio'] }
];

let usrFn = arr => {
  let result = [];

  for (let x of arr)
    for (let y of x.authors)
      result.push(y);

  return result;
};

console.dir(usrFn(data));

實務上常會遇到 nested array,若使用 imperative,就得使用兩層 for loop,且內層 array 由外層 array 決定。

chain000

Array.prototype

map()

let data = [
  { title: 'Secrets of the JavaScript Ninja', 
    authors: ['John Resig', 'Bear Bibeault'] },
  { title: 'RxJS in Action', 
    authors: ['Paul P.Daniels', 'Luis Atencio'] }
];

let usrFn = arr => arr.map(x => x.authors);

console.dir(usrFn(data));

若使用 map(),由於 data 是兩層 array,最後結果也會維持兩層 array,這顯然不是我們所要的。

chain001

flat()

let data = [
  { title: 'Secrets of the JavaScript Ninja', 
    authors: ['John Resig', 'Bear Bibeault'] },
  { title: 'RxJS in Action', 
    authors: ['Paul P.Daniels', 'Luis Atencio'] }
];

let usrFn = arr => arr.map(x => x.authors).flat();

console.dir(usrFn(data));

ES2019 新增 Array.prototype.flat(),可將多層 array 攤平,相當於 Ramda 的 flatten()

chain007

import { flatten } from 'ramda';

let data = [1, [2, 3, [4, 5]]];

data.flat(1); // ?
flatten(data); // ?

flat()flatten() 的差異:

  • flat() 可指定要攤平的層數
  • flatten() 則齊頭式全部攤平

chain006

flatMap()

let data = [
  { title: 'Secrets of the JavaScript Ninja', 
    authors: ['John Resig', 'Bear Bibeault'] },
  { title: 'RxJS in Action', 
    authors: ['Paul P.Daniels', 'Luis Atencio'] }
];

let usrFn = arr => arr.flatMap(x => x.authors);

console.dir(usrFn(data));

map()flat() 的組合實在太常使用,ES2019 另外也新增了 Array.prototype.flatMap()

chain008

Ramda

map()

import { map } from 'ramda';

let data = [
  { title: 'Secrets of the JavaScript Ninja', 
    authors: ['John Resig', 'Bear Bibeault'] },
  { title: 'RxJS in Action', 
    authors: ['Paul P.Daniels', 'Luis Atencio'] }
];

let usrFn = map(x => x.authors);

console.dir(usrFn(data));

Array.prototype 下的 method 是以 OOP 設計,method 掛在 data 上;但 Ramda 是以 FP 呈現,全部都是 function,data 則為 function 最後一個 argument,方便 function composition 與 point-free。

Array.prototype.map() 改成 Ramda 的 map(),最後結果也會維持兩層 array,這顯然不是我們所要的。

chain003

flatten()

import { map, pipe, flatten } from 'ramda';

let data = [
  { title: 'Secrets of the JavaScript Ninja', 
    authors: ['John Resig', 'Bear Bibeault'] },
  { title: 'RxJS in Action', 
    authors: ['Paul P.Daniels', 'Luis Atencio'] }
];

let usrFn = pipe(
  map(x => x.authors),
  flatten,
);

console.dir(usrFn(data));

Ramda 另提供 flatten(),將多層 array 攤平成一層 array,這就是我們所要的結果。

flatten()
[a] -> [b]
將多層 array 攤平成一層 array

chain002

chain()

import { chain } from 'ramda';

let data = [
  { title: 'Secrets of the JavaScript Ninja', 
    authors: ['John Resig', 'Bear Bibeault'] },
  { title: 'RxJS in Action', 
    authors: ['Paul P.Daniels', 'Luis Atencio'] }
];

let usrFn = chain(x => x.authors);

console.dir(usrFn(data));

pipe(map, flatten) 實在太常使用,Ramda 另外提供了 chain()

chain()
(a → b) → [a] → [b]
相當於 flatten()map() 組合

(a -> b):相當於 map function

[a]:data 為 array

[b]:回傳攤平後的 array

chain004

Point-free

import { chain, prop } from 'ramda';

let data = [
  { title: 'Secrets of the JavaScript Ninja', 
    authors: ['John Resig', 'Bear Bibeault'] },
  { title: 'RxJS in Action', 
    authors: ['Paul P.Daniels', 'Luis Atencio'] }
];

let usrFn = chain(prop('authors'));

console.dir(usrFn(data));

若想讓 chain() 的 callback 也 point-free,可改用 prop() 產生。

chain005

Conclusion

  • chain() 搭配 array 時,與 ES2019 的 flatMap() 完全相同,都可將 nested array 攤平
  • chain() 還能用在 function,chain(f, g)(x) == f(g(x), x)

Reference

MDN, Array.prototype.map()
MDN, Array.prototype.flat()
MDN, Array.prototype.flatMap()
Ramda, map()
Ramda, flatten()
Ramda, chain()
Ramda, assoc()
Ramda, prop()