在 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
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 並不相同,各為 teacher
、assistant
與 students
。且自個 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 另外處理, 可讀性很差
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 都不用判斷,非常優雅
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()
表示組合
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 了。
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
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()