[英]declarative loop vs imperative loop
我正在嘗試將我的編程風格從命令式轉換為聲明式,但是有一些概念讓我感到困擾,比如循環的性能。 例如,我有一個原始的DATA ,在操縱它之后,我希望得到 3 個預期結果: itemsHash 、 namesHash 、 rangeItemsHash
// original data
const DATA = [
{id: 1, name: 'Alan', date: '2021-01-01', age: 0},
{id: 2, name: 'Ben', date: '1980-02-02', age: 41},
{id: 3, name: 'Clara', date: '1959-03-03', age: 61},
]
...
// expected outcome
// itemsHash => {
// 1: {id: 1, name: 'Alan', date: '2021-01-01', age: 0},
// 2: {id: 2, name: 'Ben', date: '1980-02-02', age: 41},
// 3: {id: 3, name: 'Clara', date: '1959-03-03', age: 61},
// }
// namesHash => {1: 'Alan', 2: 'Ben', 3: 'Clara'}
// rangeItemsHash => {
// minor: [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}],
// junior: [{id: 2, name: 'Ben', date: '1980-02-02', age: 41}],
// senior: [{id: 3, name: 'Clara', date: '1959-03-03', age: 61}],
// }
// imperative way
const itemsHash = {}
const namesHash = {}
const rangeItemsHash = {}
DATA.forEach(person => {
itemsHash[person.id] = person;
namesHash[person.id] = person.name;
if (person.age > 60){
if (typeof rangeItemsHash['senior'] === 'undefined'){
rangeItemsHash['senior'] = []
}
rangeItemsHash['senior'].push(person)
}
else if (person.age > 21){
if (typeof rangeItemsHash['junior'] === 'undefined'){
rangeItemsHash['junior'] = []
}
rangeItemsHash['junior'].push(person)
}
else {
if (typeof rangeItemsHash['minor'] === 'undefined'){
rangeItemsHash['minor'] = []
}
rangeItemsHash['minor'].push(person)
}
})
// declarative way
const itemsHash = R.indexBy(R.prop('id'))(DATA);
const namesHash = R.compose(R.map(R.prop('name')),R.indexBy(R.prop('id')))(DATA);
const gt21 = R.gt(R.__, 21);
const lt60 = R.lte(R.__, 60);
const isMinor = R.lt(R.__, 21);
const isJunior = R.both(gt21, lt60);
const isSenior = R.gt(R.__, 60);
const groups = {minor: isMinor, junior: isJunior, senior: isSenior };
const rangeItemsHash = R.map((method => R.filter(R.compose(method, R.prop('age')))(DATA)))(groups)
為了達到預期的結果,命令式只循環一次,而聲明式循環至少 3 次( itemsHash
, namesHash
, rangeItemsHash
) 。 哪一個更好? 性能上有什么取舍嗎?
與.map(f).map(g) ==.map(compose(g, f))
類似,您可以編寫 reducers 以確保單次通過即可獲得所有結果。
編寫聲明性代碼與循環一次或多次的決定沒有任何關系。
// Reducer logic for all 3 values you're interested in // id: person const idIndexReducer = (idIndex, p) => ({...idIndex, [p.id]: p }); // id: name const idNameIndexReducer = (idNameIndex, p) => ({...idNameIndex, [p.id]: p.name }); // Age const ageLabel = ({ age }) => age > 60? "senior": age > 40? "medior": "junior"; const ageGroupReducer = (ageGroups, p) => { const ageKey = ageLabel(p); return {...ageGroups, [ageKey]: (ageGroups[ageKey] || []).concat(p) } } // Combine the reducers const seed = { idIndex: {}, idNameIndex: {}, ageGroups: {} }; const reducer = ({ idIndex, idNameIndex, ageGroups }, p) => ({ idIndex: idIndexReducer(idIndex, p), idNameIndex: idNameIndexReducer(idNameIndex, p), ageGroups: ageGroupReducer(ageGroups, p) }) const DATA = [ {id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}, ] // Loop once console.log( JSON.stringify(DATA.reduce(reducer, seed), null, 2) );
主觀部分:是否值得? 我不這么認為。 我喜歡簡單的代碼,根據我自己的經驗,在處理有限的數據集時,從 1 到 3 個循環通常是不引人注意的。
所以,如果使用 Ramda,我會堅持:
const { prop, indexBy, map, groupBy, pipe } = R; const DATA = [ {id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}, ]; const byId = indexBy(prop("id"), DATA); const nameById = map(prop("name"), byId); const ageGroups = groupBy( pipe( prop("age"), age => age > 60? "senior": age > 40? "medior": "junior" ), DATA ); console.log(JSON.stringify({ byId, nameById, ageGroups }, null, 2))
<script src="https://cdn.jsdelivr.net/npm/ramda@0.27.1/dist/ramda.min.js"></script>
我對此有幾個回應。
首先,您是否經過測試知道性能是一個問題? 太多的性能工作是在甚至沒有接近成為應用程序瓶頸的代碼上完成的。 這通常以犧牲代碼的簡單性和清晰度為代價。 所以我通常的規則是先寫簡單明了的代碼,盡量不要在性能上犯傻,但不要過分擔心。 然后,如果我的應用程序速度慢得令人無法接受,請對其進行基准測試以找出導致最大問題的部分,然后對其進行優化。 我很少有這些地方相當於循環三次而不是一次。 但它當然可能發生。
如果確實如此,並且您確實需要在單個循環中執行此操作,那么在reduce
調用之上執行此操作並不難。 我們可以這樣寫:
// helper function const ageGroup = ({age}) => age > 60? 'senior': age > 21? 'junior': 'minor' // main function const convert = (people) => people.reduce (({itemsHash, namesHash, rangeItemsHash}, person, _, __, group = ageGroup (person)) => ({ itemsHash: {...itemsHash, [person.id]: person}, namesHash: {...namesHash, [person.id]: person.name}, rangeItemsHash: {...rangeItemsHash, [group]: [...(rangeItemsHash [group] || []), person]} }), {itemsHash: {}, namesHash: {}, rangeItemsHash: {}}) // sample data const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}] // demo console.log (JSON.stringify ( convert (data), null, 4))
.as-console-wrapper {max-height: 100%;important: top: 0}
(您可以刪除JSON.stringify
調用以證明引用在各種 output 哈希之間共享。)
有兩個方向我可能 go 從這里清理這段代碼。
首先是使用 Ramda。 它有一些功能可以幫助簡化這里的一些事情。 使用R.reduce
,我們可以消除煩人的占位符參數,我使用這些參數可以將默認參數group
添加到 reduce 簽名,並保持表達式超過語句的樣式編碼。 (我們也可以使用R.call
來做一些事情。)並且將evolve
與assoc
和over
等函數一起使用,我們可以使它更具聲明性,如下所示:
// helper function const ageGroup = ({age}) => age > 60? 'senior': age > 21? 'junior': 'minor' // main function const convert = (people) => reduce ( (acc, person, group = ageGroup (person)) => evolve ({ itemsHash: assoc (person.id, person), namesHash: assoc (person.id, person.name), rangeItemsHash: over (lensProp (group), append (person)) }) (acc), {itemsHash: {}, namesHash: {}, rangeItemsHash: {minor: [], junior: [], senior: []}}, people ) // sample data const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}] // demo console.log (JSON.stringify ( convert (data), null, 4))
.as-console-wrapper {max-height: 100%;important: top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js"></script> <script> const {reduce, evolve, assoc, over, lensProp, append} = R </script>
與前一個版本相比,此版本的一個小缺點是需要在累加器中預定義senior
、 junior
和minor
類別。 我們當然可以寫一個以某種方式處理默認值的lensProp
替代方案,但這會讓我們走得更遠。
我可能 go 的另一個方向是注意代碼中仍然存在一個潛在的嚴重性能問題,一個 Rich Snapp 稱為reduce ({...spread}) 反模式。 為了解決這個問題,我們可能想在 reduce 回調中改變我們的累加器 object。 Ramda——就其哲學性質而言——不會幫助你解決這個問題。 但是我們可以定義一些幫助函數,在我們解決這個問題的同時清理我們的代碼,如下所示:
// utility functions const push = (x, xs) => ((xs.push (x)), x) const put = (k, v, o) => ((o[k] = v), o) const appendTo = (k, v, o) => put (k, push (v, o[k] || []), o) // helper function const ageGroup = ({age}) => age > 60? 'senior': age > 21? 'junior': 'minor' // main function const convert = (people) => people.reduce (({itemsHash, namesHash, rangeItemsHash}, person, _, __, group = ageGroup(person)) => ({ itemsHash: put (person.id, person, itemsHash), namesHash: put (person.id, person.name, namesHash), rangeItemsHash: appendTo (group, person, rangeItemsHash) }), {itemsHash: {}, namesHash: {}, rangeItemsHash: {}}) // sample data const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}] // demo console.log (JSON.stringify ( convert (data), null, 4))
.as-console-wrapper {max-height: 100%;important: top: 0}
但最后,正如已經建議的那樣,除非性能被證明是一個問題,否則我不會這樣做。 我認為像這樣的 Ramda 代碼會更好:
const ageGroup = ({age}) => age > 60? 'senior': age > 21? 'junior': 'minor' const convert = applySpec ({ itemsHash: indexBy (prop ('id')), nameHash: compose (fromPairs, map (props (['id', 'name']))), rangeItemsHash: groupBy (ageGroup) }) const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}] console.log (JSON.stringify( convert (data), null, 4))
.as-console-wrapper {max-height: 100%;important: top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js"></script> <script> const {applySpec, indexBy, prop, compose, fromPairs, map, props, groupBy} = R </script>
在這里,為了保持一致性,我們可能希望將ageGroup
點和/或將其內聯到主 function 中。 這並不難,另一個答案舉了一個例子。 我個人覺得這樣更具可讀性。 (可能還有更簡潔的namesHash
版本,但我沒時間了。)
這個版本循環了三遍,正是你所擔心的。 有時這可能是個問題。 但除非這是一個可證明的問題,否則我不會為此付出太多努力。 干凈的代碼本身就是一個有用的目標。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.