簡體   English   中英

減少函數中的變異累加器是否被認為是不好的做法?

[英]Is mutating accumulator in reduce function considered bad practice?

我是函數式編程的新手,我正在嘗試重寫一些代碼以使其更具功能性以掌握概念。 剛才我發現了Array.reduce()函數並用它來創建一個組合數組的對象(我之前用過for循環)。 但是,我不確定某些事情。 看看這段代碼:

const sortedCombinations = combinations.reduce(
    (accum, comb) => {
        if(accum[comb.strength]) {
            accum[comb.strength].push(comb);
        } else {
            accum[comb.strength] = [comb];
        }

        return accum;
    },
    {}
);

顯然,這個函數改變了它的參數accum ,所以它不被認為是純的。 另一方面,如果我理解正確,reduce 函數會在每次迭代中丟棄累加器,並且在調用回調函數后不使用它。 盡管如此,它不是一個純函數。 我可以像這樣重寫它:

const sortedCombinations = combinations.reduce(
    (accum, comb) => {
        const tempAccum = Object.assign({}, accum);
        if(tempAccum[comb.strength]) {
            tempAccum[comb.strength].push(comb);
        } else {
            tempAccum[comb.strength] = [comb];
        }

        return tempAccum;
    },
    {}
);

現在,在我的理解中,這個函數被認為是純函數。 但是,它每次迭代都會創建一個新對象,這會消耗一些時間,顯然還消耗內存。

所以問題是:哪個變體更好,為什么? 純度真的那么重要以至於我應該犧牲性能和內存來實現它嗎? 或者也許我遺漏了一些東西,有更好的選擇嗎?

TL; DR:如果您擁有蓄能器,則不是。


在 JavaScript 中使用擴展運算符來創建漂亮的單行歸約函數是很常見的。 開發人員經常聲稱這也使他們的功能在過程中變得純粹。

const foo = xs => xs.reduce((acc, x) => ({...acc, [x.a]: x}), {});
//------------------------------------------------------------^
//                                                   (initial acc value)

但是讓我們考慮一下......如果你改變了acc可能會出什么問題? 例如,

const foo = xs => xs.reduce((acc, x) => {
  acc[x.a] = x;
  return acc;
}, {});

絕對沒有。

acc的初始值是一個動態創建的空文字對象。 在這一點上,使用擴展運算符只是一種“裝飾性”選擇。 這兩個函數都是純函數。

不變性是一種特性,而不是過程本身。 這意味着克隆數據以實現不變性很可能是一種幼稚且低效的方法。 大多數人忘記了擴展運算符無論如何只能進行淺層克隆!

不久前我寫了這篇文章,我聲稱變異和函數式編程不必相互排斥,並且我還表明使用擴展運算符不是一個微不足道的選擇。

盡管存在潛在的性能問題,但在每次迭代時創建一個新對象是常見做法,有時也建議這樣做。

(編輯:)我想這是因為如果您只想獲得一個一般性建議,那么與變異相比,復制不太可能引起問題。 如果您有超過 1000 次迭代,則性能開始成為一個“真正的”問題。 (有關更多詳細信息,請參閱下面我的更新)

您可以通過這種方式使您的函數純例如:

const sortedCombinations = combinations.reduce(
    (accum, comb) => {
        return {
            ...accum,
            [comb.strength]: [
                ...(accum[comb.strength] || []),
                comb
            ]
        };
    },
    {}
);

如果您的 state 和 reducer 在其他地方定義,純度可能會變得更加重要:

const myReducer = (accum, comb) => {
    return {
        ...accum,
        [comb.strength]: [
            ...(accum[comb.strength] || []),
            comb
        ]
    };
};

const initialState = {};
const sortedCombinations = combinations.reduce( myReducer, initialState );
const otherSortedCombinations = otherCombinations.reduce( myReducer, initialState );
const otherThing = otherList.reduce( otherReducer, initialState );

更新 (2021-08-22):

本次更新的前言

正如評論中所述(並且在問題中也提到),當然在每次迭代時復制性能較低。

而且我承認,在很多情況下,從技術上講,我看不出改變累加器的任何缺點(如果您知道自己在做什么!)。

實際上,再次考慮一下,受到評論和其他答案的啟發,我改變了主意,現在會考慮更頻繁地進行變異,至少在我沒有看到任何風險的情況下,例如其他人以后會誤解我的代碼。

但話又說回來,問題顯然是關於純度的……無論如何,這里有更多細節:

純度

(免責聲明:我必須在這里承認我了解React ,但我不太了解“函數式編程的世界”以及他們關於優勢的論點,例如在 Haskell 中)

使用這種“純”方法是一種權衡 你失去了性能,而你贏得了更容易理解和更少耦合的代碼。

例如在React 中,有許多嵌套的組件,你總是可以依賴當前組件的一致狀態。 你知道它不會在外面的任何地方改變,除非你明確地傳遞了一些“onChange”回調。

如果你定義一個對象,你肯定知道它會一直保持不變 如果你需要一個修改過的版本,你會有一個新的變量賦值,這樣很明顯你正在使用一個新版本的數據,並且任何可能使用舊對象的代碼都不會受到影響。:

const myObject = { a1: 1, a2: 2, a3: 3 };        <-- stays unchanged

// ... much other code ...

const myOtherObject = modifySomehow( myObject ); <-- new version of the data

優點、缺點和注意事項

我無法給出一般性建議,哪種方式(復制或變異)是“更好的”。 Mutating 的性能更高,但如果您不確定發生了什么,可能會導致許多難以調試的問題。 至少在有些復雜的場景中。

1.非純減速機問題

正如我在原始答案中已經提到的,非純函數可能會無意中改變一些外部狀態:

var initialValue = { a1: 1, a2: 2, a3: 3, a4: 4 };
var newKeys = [ 'n1', 'n2', 'n3' ];

var result = newKeys.reduce( (acc, key) => {
    acc[key] = 'new ' + key;
    return acc
}, initialValue);

console.log( 'result:', result );             // We are interested in the 'result',
console.log( 'initialValue:', initialValue ); // but the initialValue has also changed.

有人可能會爭辯說,您可以事先復制初始值:

var result = newKeys.reduce( (acc, key) => {
    acc[key] = 'new ' + key;
    return acc
}, { ...initialValue }); // <-- copy beforehand

但是,在例如對象非常大且嵌套的情況下,這可能效率更低,reducer 經常被調用,並且reducer 內部可能有多個有條件地使用的小修改,這些修改只有很小的變化。 (想想 React 中的useReducerRedux reducer

2.淺拷貝

另一個答案正確地指出,即使使用所謂的純方法,仍然可能引用原始對象。 這確實是一個需要注意的,但只出現的問題,如果你不遵循這個“不變”的做法因此不夠:

var initialValue = { a1: { value: '11'}, a2: { value: '22'} }; // <-- an object with nested 'non-primitive' values

var newObject = Object.keys(initialValue).reduce( (acc, key) => {
    return {
        ...acc,
        ['newkey_' + key]: initialValue[key], // <-- copies a reference to the original object
    };
}, {}); // <-- starting with empty new object, expected to be 'pure'

newObject.newkey_a1.value = 'new ref value'; // <-- changes the value of the reference
console.log( initialValue.a1 ); // <-- initialValue has changed as well

這不是問題,如果注意不復制任何引用(有時這可能不是微不足道的):

var initialValue = { a1: { value: '11'}, a2: { value: '22'} };
var newObject = Object.keys(initialValue).reduce( (acc, key) => {
    return {
        ...acc,
        ['newkey_' + key]: { value: initialValue[key].value }, // <-- copies the value
    };
}, {});

newObject.newkey_a1.value = 'new ref value';
console.log( initialValue.a1 ); // <-- initialValue has not changed

3. 性能

幾個元素的性能沒有問題,但是如果對象有幾千個項目,性能確實成為一個重大問題:

// create a large object
var myObject = {}; for( var i=0; i < 10000; i++ ){ myObject['key' + i] = i; } 

// copying 10000 items takes seconds (increasing exponentially!)
// (create a new object 10000 times, with each 1,2,3,...,10000 properties)
console.time('copy')
var result = Object.keys(myObject).reduce( (acc, key)=>{
    return {
        ...acc,
        [key]: myObject[key] * 2
    };
}, {});
console.timeEnd('copy');

// mutating 10000 items takes milliseconds (increasing linearly)
console.time('mutate')
var result = Object.keys(myObject).reduce( (acc, key)=>{
    acc[key] = myObject[key] * 2;
    return acc;
}, {});
console.timeEnd('mutate');

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM