點燈坊

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

使用 wrap() 讓 null 不進 Array

Sam Xiao's Avatar 2019-09-28

在 Elixir 的部落格發現一個很帥的 wrap() Function,除了可以將 Array 攤平,還可以讓 null 優雅消失,特別將其 Porting 到 Wink-fp。

Version

macOS Mojave 10.14.6
VS Code 1.38.1
Quokka 1.0.253
Ramda 0.26.1
Wink-fp 0.1.1

wrap()

import { pipe, filter, identity, unnest, of } from 'ramda';

let wrap = pipe(
  of,
  unnest,
  filter(identity)
);

wrap(1); // ?
wrap(null); // ?
wrap([2, 3]); // ?

wrap() 的功能如下:

  • 若傳入為 primitive value,則直接塞入 array
  • 若傳入為 null,則不會塞入 array
  • 若傳入為 array,則自動拆解後再塞入 array

of() 先將 value 變成 array,這使得 wrap(1) 成為 [1]

wrap([2, 3]) 則會變成 [[2, 3]],顯然多了一層 array,所以還要配合 unnest() 攤平一層,但這對 wrap(1) 沒有影響,因為只有一層的 array 對 unnest() 無效。

filter(identity) 則是針對 wrap(null),因為 null !== null,因此會被 filter() 濾掉,也就是 null 不會新增進 array,但其他 primitive value 與 array 則毫無影響。

wrap() 有什麼威力呢 ? 讓我們繼續看下去。

wrap() 本質與 of() 一樣,只是多了 null 無法塞入 array,且 array 會先拆解後再塞入 array

wrap000

Imperative

let data = [
  { teacher: { name: 'John', age: 40 } },
  { assistant: null },
  { students: [
    { name: 'Amber', age: 20 },
    { name: 'William', age: 22 }
  ] }
];


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

  for (let obj of arr) {
    for (let obj2 of Object.values(obj)) {
      if (obj2 !== null && !Array.isArray(obj2)) {
        result.push(obj2);
      }

      if (Array.isArray(obj2)) {
        for (let obj3 of Object.values(obj2)) {
          if (obj3 !== null) {
            result.push(obj3);
          }
        }
      }
    }
  }

  return result;
};

console.dir(fn(data));

data 為 array,每個 object 的 key 並不相同,各為 teacherassistantstudents。且自個 value 亦為 object、null 與 array,除了 key 不同,value 格式也不同。

[ { name: 'John', age: 40 },{ name: 'Amber', age: 20 },{ name: 'William', age: 22 } ]

若我們希望結果為一層 array,且僅包含 object,並不包含 null

14 行

for (let obj of arr) {
}

若使用 imperative,由於 data 為 arr,所以要先使用 for loop 展開。

for (let obj2 of Object.values(obj)) {
  if (obj2 !== null && !Array.isArray(obj2)) {
    result.push(obj2);
  }
}

由於需求不顯示 null,因此要使用 if 先過濾 null

array 也必須排除,因為 array 稍後要特別處理。

if (Array.isArray(obj2)) {
  for (let obj3 of Object.values(obj2)) {
    if (obj3 !== null) {
      result.push(obj3);
    }
  }
}

由於 students 為 array,因此又要使用 for loop 加以展開。

可以發現 imperative 寫法,需動用三層 for loop,且還必須用 if 判斷 null 與 array 另外處理, 可讀性很差

wrap004

Pipeline

import { pipe, filter, identity, unnest, of, values, chain } from 'ramda';

let data = [
  { teacher: { name: 'John', age: 40 } },
  { assistant: null },
  { students: [
    { name: 'Amber', age: 20 },
    { name: 'William', age: 22 }
  ] }
];

let wrap = pipe(
  of,
  unnest,
  filter(identity)
);

let fn = pipe(
  chain(x => wrap(values(x))),
  unnest
);

console.dir(fn(data));

若使用了 wrap(),則 null 不會進 array,會優雅的消失,因此不用特別判斷。

chain() 雖然號稱是 flatMap() ,但實際上是 pipe(map, unnest),只能使 array 攤平一層,這對 teacher 是有用的,但 students 還有一層,因此必須再使用 pipe(chian, unnest) 繼續攤平。

可以發現使用了 wrap() 之後,除了看不到 for loop 外,連 null 與 array 都不用判斷,非常優雅

wrap001

Point-free

import { pipe, filter, identity, unnest, of, values, chain, compose } from 'ramda';

let data = [
  { teacher: { name: 'John', age: 40 } },
  { assistant: null },
  { students: [
    { name: 'Amber', age: 20 },
    { name: 'William', age: 22 }
  ] }
];

let wrap = pipe(
  of,
  unnest,
  filter(identity)
);

let fn = pipe(
  chain(compose(wrap, values)),
  unnest
);

console.dir(fn(data));

其實之前的寫法,可讀性已經很高,若想將 chian() 的 callback 進一步 Point-free,可使用 compose()wrap()values() 加以組合。

實務上對架構會使用 pipe() 顯示流程,callback 會使用 compose() 表示組合

wrap002

Function Composition

import { filter, identity, unnest, of, values, chain, compose } from 'ramda';

let data = [
  { teacher: { name: 'John', age: 40 } },
  { assistant: null },
  { students: [
    { name: 'Amber', age: 20 },
    { name: 'William', age: 22 }
  ] }
];

let wrap = compose(
  filter(identity),
  unnest,
  of
);

let fn = compose(
  unnest,
  chain(compose(wrap, values))  
);

console.dir(fn(data));

既然能使用 pipe() 達成 pipeline,反向使用 compose() 就是 function composition 了。

wrap005

Wink-fp

import { unnest, values, chain, compose } from 'ramda';
import { wrap } from 'wink-fp';

let data = [
  { teacher: { name: 'John', age: 40 } },
  { assistant: null },
  { students: [
    { name: 'Amber', age: 20 },
    { name: 'William', age: 22 }
  ] }
];

let fn = compose(
  unnest,
  chain(compose(wrap, values))  
);

console.dir(fn(data));

Wink-fp 已經提供 wrap(),可直接使用。

wrap()
a | [b] -> [a] | [b]
除了 null 以外值都可塞進 array

a | [b]:可傳入單一值或 array

[a] | [b]:回傳為 array,但不包含 null

wrap006

Conclusion

  • Elixir 的 wrap() 理念很好,除了讓 array 自動減少一層外,還可讓 null 優雅的消失,他山之石可以攻錯,我們也可以在 Wink-fp 實現 wrap()
  • chain() 雖然號稱是 flatMap(),其實是 compose(unnest, map),只能攤平一層 array

Reference

Taiansu, List 操作小技巧
Ramda, of()
Ramda, unnest()
Ramda, identity()
Ramda, filter()
Ramda, chain()
Ramda, pipe()
Ramda, compose()
Ramda, values()