點燈坊

學而時習之,不亦悅乎

如何將 Array 中每筆資料之後新增一筆資料 ?

Sam Xiao's Avatar 2019-06-07

在使用 Vuetify 的 Lists 時,為了讓視覺呈現更加明顯,我們想要在每筆資料間加上一條分隔線,但由於 Lists 要求從 Data 動手,必須在兩筆資料間加上 { divider: true },這該如何實現呢 ?

Version

VS Code 1.35.0
Quokka 1.0.224
Ramda 0.26.1

Imperative

let data = [ 
  { status: 14 },
  { status: 16  },
  { status: 17 },
];

// fn :: [a] -> [b]
let fn = arr => {
  let result = [];
  let index = 0;

  for(let elem of arr) {
    result = [...result, elem];

    if (index < arr.length - 1) {
      result.push({ divider: true });
    }

    index++;
  }

  return result;
};

console.dir(fn(data));

從 API 獲得資料如下:

[ { status: 14 },
  { status: 16  },
  { status: 17 } ];

我們希望最後結果為:

[ { status: 14 },{ divider: true },{ status: 16 },{ divider: true },{ status: 17 } ]

也就是每一筆資料都新增了 { divider: true },但注意最後一筆又不新增。

第 7 行

// fn :: [a] -> [b]
let fn = arr => {
  let result = [];
  let index = 0;

  for(let elem of arr) {
    result.push(elem);

    if (index < arr.length - 1) {
      result.push({ divider: true });
    }

    index++;
  }

  return result;
};

13 行

result.push(elem);

Imperative 會先建立欲回傳的 result array,由於原來資料都要保留,所以使用了 result.push(elem)

15 行

if (index < arr.length - 1) {
  result.push({ divider: true });
}

因為每一筆資料都新增了 { divider: true },但最後一筆又不新增,因此使用了 (index < arr.length - 1) 做判斷,如此可確保最後一筆不會新增 { divider: true }

Imperative 寫法比較易錯的是 index 判斷,因為 index 為 zero-based,所以全部資料為 index < arr.length,不包含最後一筆為 index < arr.length - 1,這是很容易產生 bug 之處

chain000

Functional

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

let data = [ 
  { status: 14 },
  { status: 16  },
  { status: 17 }
];

// fn :: [a] -> [b]
let fn = pipe(
  map(x => [x, { divider: true }]),
  flatten,
  dropLast(1)
);

console.dir(fn(data));

我們知道需求為:

  • 每一筆資料都新增了 { divider: true }
  • 最後一筆不新增 { divider: true }

以 Functional 角度思考:

  • 每一筆資料從一筆變兩筆
  • 再刪除最後一筆資料

11 行

map(x => [x, { divider: true }]),

使用 map() 將每一筆資料變成兩筆,回傳為 array。

map()
Functor f => (a -> b) -> f a -> f b
將 Functor a 轉換成 Functor b

12 行

flatten,

因為每一筆都是 array,所以變成了如下的兩層 array。

[ [ { status: 14 }, { divider: true } ],[ { status: 16 }, { divider: true } ],[ { status: 17 }, { divider: true } ] ]

這並不是我們要的,因此須使用 flatten() 加以攤平。

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

13 行

dropLast(1)

因為最後一筆不必新增 { divider: true },所以使用 dropLast(1) 刪除之。

dropLast()
Number -> [a] -> [a]
Number -> String -> String
將 array 或 string 的最後幾筆資料刪除後回傳

我們發現使用 FP 後,透過 pipe() 組合 map()flatten()dropLast(),整個演算法清楚可見,不必如 imperative 須 trace code 才知道演算法為何。

chain001

Refactoring

import { pipe, init, chain } from 'ramda';

let data = [ 
  { status: 14 },
  { status: 16 },
  { status: 17 }
];

// fn :: [a] -> [b]
let fn = pipe(
  chain(x => [x, { divider: true }]),
  init
);

console.dir(fn(data));

其實以上寫法已相當清楚,可再稍微重構簡化。

11 行

chain(x => [x, { divider: true }]),

chain() 相當於 map() + flatten(),可再簡化成 chain()

chain()
Chain m => (a -> m b) -> m a -> m b
相當於 flatten()map() 組合

12 行

init

目標是不要最後一筆,init() 則是回傳不包含最後一筆的所有資料,因此也可簡化成 init()

init()
[a] -> [a]
String -> String
回傳不包含最後一筆資料的 array 或 string

chain002

Point-free

import { pipe, init, chain, of, append } from 'ramda';

let data = [ 
  { status: 14 },
  { status: 16  },
  { status: 17 },
];

// addDivider :: a -> b
let addDivider = pipe(
  of,
  append({ divider: true }), 
);

// fn :: [a] -> [b]
let fn = pipe(
  chain(addDivider),
  init
);

console.dir(fn(data));

若想連 callback 部分也 point-free,可再進一步重構。

21 行

chain(addDivider),

chain() 的 callback 抽成 addDivider()

13 行

// addDivider :: a -> b
let addDivider = pipe(
  of,
  append({ divider: true }), 
);

原本寫法為:

x => [x, { divider: true }]

經過抽絲剝繭,其實相當於

  • x 變成 [x]
  • 新增 { divider: true } 到 array

只要將以上想法組合起來即可。

of

使用 of()x 變成 [x]

of()
a -> [a]
將單一值變成 array

append({ divider: true }), 

使用 append(){ divider: true } 新增至 array。

append()
a -> [a] -> [a]
將新資料新增到 array 尾部

chain003

Conclusion

  • map() + flatten() 組合在實務上常常見到,可使用 chain() 加以重構
  • 實務上最後一筆常有特殊需求,可使用 dropLast() 刪除之
  • 是否要連 callback 也 point-free 屬於品味問題,可自行依可讀性決定

Reference

Ramda, map()
Ramda, pipe()
Ramda, flatten()
Ramda, dropLast()
Ramda, chain()
Ramda, init()
Ramda, of()
Ramda, append()