如何使用 Ramda 將 Nested Array 攤平 ?

實務上常會遇到兩層 Array,但最終希望兩層 Array 的 Element 經過 Function 運算後成為一層 Array,這個常見的需求該如何使用 Ramda 實現呢 ?

Version

VS Code 1.32.3

Quokka 1.0.195

Ramda 0.26.1

Imperative

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

// getBooks :: [a] -> [b]
const getBooks = source => {
  const result = [];

  for (let x of source) {
    for (let y of x.authors) {
      result.push(`${x.title}: ${y}`);
    }
  }

  return result;
};

console.dir(getBooks(data));

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

map()

import { map } from 'ramda';

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

// getBooks :: [a] -> [b]
const getBooks = map(x => map(y => `${x.title}: ${y}`, x.authors));

console.dir(getBooks(data));

由於是兩層 array,直覺會使用兩層 map() ,且內層 map()x.authors 決定。

但若使用兩層 map() ,則結果也是兩層 array,這顯然不是我們所要的。

flatten()

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

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

// getBooks :: [a] -> [b]
const getBooks = compose(
  flatten,
  map(x => map(y => `${x.title}: ${y}`, x.authors))
);

console.dir(getBooks(data));

Ramda 另外有提供 flatten() ,專門將多層 array 攤平成一層 array。

flatten()

[a] -> [b]

將多層 array 攤平成一層 array

chain()

import { map, chain } from 'ramda';

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

// getBooks :: [a] -> [b]
const getBooks = chain(x => map(y => `${x.title}: ${y}`, x.authors));

console.dir(getBooks(data));

對於 compose(flatten, map) ,由於太常使用,Ramda 另外提供了 chain() ,因此只要使用 chain(map()) 就能達成需求。

注意此時 chain() callback 的 x 為 object, x.authors 才是 array,繼續傳給內層的 map()

chain()

Chain m => (a → m b) → m a → m b

若 data 為 array,相當於 flatten(map) ,俗稱 flatMap()

mChain 型別,其定義可參考 Fantacy Land ,先簡單想成可以是 array 或 function 即可。

(a -> m b) :第一個參數為 function,由任意型別轉成 array 或 function

m a :第二個參數為 array 或 function

m b :回傳為另一個 array 或 function

Point-free

import { chain, ap, concat, converge, pipe, prop, __, of } from 'ramda';

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

// concatTitleAuthor :: Object -> [a -> b]
const concatTitleAuthor = pipe(
  prop('title'),
  concat(__, ': '),
  concat,
  of
);

// parser :: a -> b
const parser = converge(
  ap, [
    concatTitleAuthor,
    prop('authors')
  ]
);

// getBooks :: [a] -> [b]
const getBooks = chain(parser);
console.dir(getBooks(data));

目前 chain() 的 callback 還帶有 x , 還不是 Point-free,有進一步優化的空間嗎 ?

[ 'Secrets of the JavaScript Ninja: John Resig', 
  'Secrets of the JavaScript Ninja: Bear Bibeault', 
  'RxJS in Action: Paul P.Daniels', 
  'RxJS in Action: Luis Atencio' ]

由於結果為 titleauthor 兩部份相加,且只要是同一個 object,其 title 部分是相同的,因此可以將 title 部分先準備好成 partially applied function,再配合 ap() 對於所有 authors 相加。

// parser :: a -> b
const parser = converge(
  ap, [
    concatTitleAuthor,
    prop('authors')
  ]
);

ap() 第一個參數為 function array,第二個參數為 data,由於 chain() callback 的 x 為 object,因此會使用 converge() 先對 object 加以拆解。

其中包含 ap() 這個 converging function,與 concatTitleAuthor()prop('author') 兩個 branching function。

// concatTitleAuthor :: Object -> [a -> b]
const concatTitleAuthor = pipe(
  prop('title'),
  concat(__, ': '),
  concat,
  of
);

先使用 prop('title') 拆解 object,再傳給 concat(__, ': '), 截至目前為止,已經完成相加 title 部份,這些都是 value。

但我們還得對 author 相加,且 ap() 要的是 function array,因此將 pipeline 的結果傳給 concat() ,產生新的 String -> String function,此為 partially applied function。

最後再依 ap() 的要求,將 function 傳給 of() 產生 function array,儘管此時 array 只有一個 function,但依然符合 ap() 的要求。

第一個 concat() 是產生 String,第二個 concat() 是產生 partially applied function,為 ap() 做準備

Conclusion

  • chain() 搭配 array 時,可視為 flatMap() ,用來將 nested array 攤平
  • chain() 搭配 array,且希望將 chain() 的 callback 也能 Point-free 時,可搭配 converge()ap() ,將 nested array 傳給 ap() 第二個參數,主要功能則放在 ap() 第一個參數
  • 由於 ap() 需要 function array,因此會搭配 of() 將 function 轉成 function array
  • 基本的 Point-free 可使用 pipe()compose() ,進階就得使用 useWith()converge()chain()ap() ,要臨場視需求靈活運用
  • Point-free 有可能會使原本的程式更為複雜,實務上需自行拿捏,並不是所有 callback 都得 Point-free;但以練習的角度,可藉由 Point-free 熟悉各 operator 的靈活組合

Reference

Ramda , map()

Ramda , flatten()

Ramda , chain()

Ramda , ap()

Ramda , of()

Ramda , concat()

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章