Ramda 之如何將 Id 轉換成 Value ?
栏目: JavaScript · 发布时间: 6年前
内容简介:實務上從 API 獲得的資料,可能只有 id,並沒有實際的 value,我們必須自行VS Code 1.30.2Quokka 1.0.136
實務上從 API 獲得的資料,可能只有 id,並沒有實際的 value,我們必須自行 map() 將 id 轉換成 value,這個常見的應用,若使用 Ramda 該怎麼寫呢 ?
Version
VS Code 1.30.2
Quokka 1.0.136
Ramda 0.26.1
Imperative
const countryLut = {
1: 'USA',
2: 'Chain',
3: 'Japan',
};
const data = [
{ id: 1, language: 'English' , countryId: 1},
{ id: 2, language: 'Chinese', countryId: 2 },
{ id: 3, language: 'Japanese', countryId: 3},
];
const getData = data => {
let result = [];
for(let x of data) {
result.push({
...x,
country: countryLut[x.countryId],
});
}
return result;
};
console.dir(getData(data));
data 只有 id 、 language 與 countryId ,但需求是能根據 countryLut 對照表,新增 country property。
Imperative 會使用 for loop,先建立好要回傳的新 array: result ,將新的 object 一一 push() 進 result ,原本的 property 全數使用 ... spread operator 加以保留。
至於查詢 countryLut ,可使用 ECMAScript 的語言特性,直接使用 [] 加以查詢。
這是大家熟悉的寫法,但有幾個問題:
-
resultarray 並不是必須的,算是為了結果所產生的中繼變數 - 必須使用
push()這種 side effect 寫法對 array 新增資料 - 必須跟著
forloop 內的程式碼一行一行看下去,才知道到底想做什麼,並不能很直覺的看出這段程式碼的意圖,也就是 Imperative 寫法會嘗試將演算法直接寫在 function 內
Array.prototype
const countryLut = {
1: 'USA',
2: 'Chain',
3: 'Japan',
};
const data = [
{ id: 1, language: 'English' , countryId: 1},
{ id: 2, language: 'Chinese', countryId: 2 },
{ id: 3, language: 'Japanese', countryId: 3},
];
const getCountries = data =>
data.map(x => ({
...x,
country: countryLut[x.countryId]
}));
console.dir(getCountries(data));
ECMAScript 提供了 Functional 的 map() Higer Order Function,讓我們一眼就可以看到他的意圖:就是為了產生一個筆數相同的 array,只是內容略有調整。
map() 的參數為 callback function,其中 x 為 array 中的 object,相當於 for(let x of data) 的 x 。
為了保留 object 原本的 property,一樣使用 ... spread operator 將 property 展開。
Callback 主要目的就是回傳新 object,因此可使用 {} 直接組新 object,但 ES2015 規定,若直接回傳 object,必須再加上 () ,否則會誤判。
我們可以發現 Functional 寫法有幾個特色:
- 中繼變數
result不見了 - 不必使用
push()這種 side effect 寫法 -
map()意圖明顯,一看到map()就知道他要產生新格式 array
但若嚴格來說,還有改善空間:
-
getCountries()的參數data是否真的需要 ? 能否再簡化 ? -
map()的 callback 參數x參數是否真的需要 ? 能否再簡化 ?
Ramda
Map()
import { map } from 'ramda';
const countryLut = {
1: 'USA',
2: 'Chain',
3: 'Japan',
};
const data = [
{ id: 1, language: 'English' , countryId: 1},
{ id: 2, language: 'Chinese', countryId: 2 },
{ id: 3, language: 'Japanese', countryId: 3},
];
const getCountries = map(x => ({
...x,
country: countryLut[x.countryId]
}));
console.dir(getCountries(data));
我們希望將 getCountries() 的參數 data 加以簡化,可使用 Ramda 的 map() 取代 Array.prototype.map() ,callback 則完全保留。
但 map() callback 的參數 x 依然存在,所以還有重構空間。
Converge()
import { map, assoc, converge, identity, compose, prop, __ } from 'ramda';
const countryLut = {
1: 'USA',
2: 'Chain',
3: 'Japan',
};
const data = [
{ id: 1, language: 'English' , countryId: 1},
{ id: 2, language: 'Chinese', countryId: 2 },
{ id: 3, language: 'Japanese', countryId: 3},
];
const lookupLut = (propName, lut) => compose(
prop(__, lut),
prop(propName),
);
const getCountries = map(converge(
assoc('country'),
[
lookupLut('countryId', countryLut),
identity,
]
));
console.dir(getCountries(data));
回想 map() 的 callback 部分:
x => ({
...x,
country: countryLut[x.countryId]
})
說穿了我們的目的就是:從 object 取得 countryId 值,並對 countryLut 查表,最後將其值新增至 country property。
若以鐵道圖表示如下:
15 行
const lookupLut = (propName, lut) => compose( prop(__, lut), prop(propName), );
其中前兩個流程 從 object 取得 countryId 值 與 對 countryLut 查表 ,這兩個流程在實務上經常遇到,可以抽出來寫成 function:傳入 property 名稱與 LUT object
透過 Ramda 的 compose() ,可將兩個 prop() 加以組合成新 function。
像 lookupLut() 這類 general function,可以重構到 helper module 內,將來其他地方要用只要 import lookupLut() 即可使用,FP 就是因為這些小 function 存在,所以重複使用的程度非常高。
20 行
const getCountries = map(converge(
assoc('country'),
[
lookupLut('countryId', countryLut),
identity,
]
));
console.dir(getCountries(data));
前兩個流程都解決了,剩下最後一個流程: 將其值新增至 country ,Ramda 特提供了 assoc() 負責對 object 新增 property。
assoc()
String -> a -> {k: v} -> {k: v}
複製原本 object 的 property 外,還可新增 property
assoc() 的第一個參數是新的 property 名稱,所以我們傳入 country 。
第二個參數要傳入的是其 value,我們已經建立了 lookupLut() ,這也沒問題。
第三個參數要傳入為原 object,也就是 map() callback 的 x 。
但卻面臨了一個問題:
assoc() 該如何與 map() 串起來 ? map() 的 callback 只有一個參數 x object,但 assoc() 要的卻是兩個參數:value 與 object。
這是 Ramda 初學者一定會卡關的地方。
Ramda 另外提供了 converge() ,負責將原本一個參數變成兩個參數,再交給 assoc() 執行。
對於 lookupLut('countryId', countryLut) 而言,因為 lookupLut() 的兩個參數已經填滿,其實 x object 傳進去會忽略,不過並不會影響結果。
assoc() 第三個參數要的就是 x object,所以使用了 Ramda 的 identity ,傳回自己本身的 object,在 FP 稱為 Identity Function。
identity()
a -> a
產生回傳與原本輸入值相同的 function
透過 converge() , map() 與 assoc() 就接起來了。
我們可以發現 map() callback 的 x 被 Point-free 掉了,且還產生了 lookupLut() Higher Order Function 可供日後重複使用。
Chain()
import { map, assoc, chain, compose, prop, __ } from "ramda";
const countryLut = {
1: "USA",
2: "Chain",
3: "Japan"
};
const data = [
{ id: 1, language: "English", countryId: 1 },
{ id: 2, language: "Chinese", countryId: 2 },
{ id: 3, language: "Japanese", countryId: 3 }
];
const lookupLut = (propName, lut) => compose(
prop(__, lut),
prop(propName)
);
const getCountries = map(chain(
assoc("country"),
lookupLut("countryId", countryLut))
);
console.dir(getCountries(data));
使用 converge() 已經接近完美,配合 identity() 可讀性也高,但 identity() 似乎是一個可有可無的 function,能否也 Point-free 掉呢 ?
20 行
const getCountries = map(chain(
assoc("country"),
lookupLut("countryId", countryLut))
);
使用 Ramda 的 chain() 可將 identity() 省略。
chain()
Chain m => (a -> m b) -> m a -> m b
chain(f, g)(x) === f(g(x), x)
chain() 會自動將參數 x object 直接傳給 assoc() ,我們就不必再使用 identity() 了。
Conclusion
- 由於 Ramda 的參數都是 function,會無形中要求自己重構出很多小 function,而這些小 function 就是日後組合的本錢,會大幅增加開發效率
-
converge()為實務上常用的 operator,尤其當 operator 間的參數個數不相同時,會需要converge()作為中間人加以處理 - 若一開始覺得
chain()太玄,無法一步到位想到chain(),可先重構成converge(),看到identity之後,自然會想到chain()
Reference
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。