减少函数中的变异累加器是否被认为是不好的做法? [英] Is mutating accumulator in reduce function considered bad practice?

查看:19
本文介绍了减少函数中的变异累加器是否被认为是不好的做法?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我是函数式编程的新手,我正在尝试重写一些代码,以使其更符合函数式来掌握概念.刚才我发现了 Array.reduce() 函数并用它来创建一个组合数组的对象(我之前使用过 for 循环).但是,我不确定某些事情.看看这段代码:

I'm new to functional programming and I'm trying rewrite some code to make it more functional-ish to grasp the concepts. Just now I've discovered Array.reduce() function and used it to create an object of arrays of combinations (I've used for loop before that). However, I'm not sure about something. Look at this code:

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

        return accum;
    },
    {}
);

显然,这个函数改变了它的参数accum,所以它不被认为是纯的.另一方面,如果我理解正确,reduce 函数会在每次迭代中丢弃累加器,并且在调用回调函数后不使用它.尽管如此,它不是一个纯函数.我可以像这样重写它:

Obviously, this function mutates its argument accum, so it is not considered pure. On the other hand, the reduce function, if I understand it correctly, discards accumulator from every iteration and doesn't use it after calling callback function. Still, it's not a pure function. I can rewrite it like this:

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;
    },
    {}
);

现在,在我的理解中,这个函数被认为是纯函数.但是,它每次迭代都会创建一个新对象,这会消耗一些时间,显然还消耗内存.

Now, in my understanding, this function is considered pure. However, it creates a new object every iteration, which consumes some time, and, obviously, memory.

所以问题是:哪个变体更好,为什么?纯度真的那么重要以至于我应该牺牲性能和内存来实现它吗?或者我遗漏了什么,还有更好的选择吗?

So the question is: which variant is better and why? Is purity really so important that I should sacrifice performance and memory to achieve it? Or maybe I'm missing something, and there is some better option?

推荐答案

在每次迭代时创建一个新对象是常见做法,有时也建议这样做,尽管存在任何潜在的性能问题.

Creating a new object on every iteration is common practice, and sometimes recommended, despite any potential performance issues.

()我想这是因为如果您只想获得一个一般性建议,那么复制不太可能的原因问题比变异.表演开始变得​​真实"问题如果你有超过 1000 次迭代.(有关更多详细信息,请参阅下面我的更新)

() I guess that is because if you want to have only one general advice, then copying less likely causes problems than mutating. The performance starts to become a "real" issue if you have more than lets say about 1000 iterations. (For more details see my update below)

你可以使你的函数纯例如这样:

You can make your function pure in e.g. in this way:

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

如果你的 state 和 reducer 是在别处定义的,那么纯度可能会变得更加重要:

Purity might become more important if your state and reducer is defined somewhere else:

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):

本次更新的前言

如评论中所述(以及问题中也提到),当然在每次迭代中进行复制的性能较低.

Update (2021-08-22):

preface to this update

As stated in the comments (and also mentioned in the question), of course copying on every iteration is less performant.

而且我承认,在很多情况下,技术上我看不出改变累加器有什么缺点(如果你知道自己在做什么!).

And I admit that in many cases, technically I can't see any disadvantages of mutating the accumulator (if you know what you are doing!).

其实,再想想,从评论和其他答案中得到启发,我改变了主意,现在会考虑更频繁地变异,也许至少我没有看到任何风险,例如后来别人误解了我的代码.

Actually, thinking about it again, inspired from the comments and other answers, I changed my mind a bit, and will consider mutating more often now, maybe at least where I don't see any risk that e.g. somebody else misunderstands my code later.

但问题再次明确地与纯度有关......无论如何,这里有更多细节:

But then again the question was explicitly about purity ... anyway, so here some more details:

(免责声明:我必须在这里承认,我知道 React,但我知道的不多关于函数式编程的世界"以及他们关于优势的论点,例如在 Haskell 中)

(Disclaimer: I must admit here that I know about React, but I don't know much about "the world of functional programming" and their arguments about the advantages, e.g. in Haskell)

使用这个纯"方法是一种权衡.你失去了性能,但你赢得了更容易理解和更少耦合的代码.

Using this "pure" approach is a tradeoff. You loose performance, and you win easier understandable and less coupled code.

例如在 React 中,有很多嵌套的组件,你可以随时依赖当前组件的一致状态.你知道它不会在外面的任何地方改变,除非你明确地传递了一些onChange"回调.

E.g. in React, with many nested Components, you can always rely on the consistent state of the current component. You know it will not be changed anywhere outside, except if you have passed down some 'onChange' callback explicitly.

如果您定义一个对象,您肯定知道它始终保持不变.如果您需要修改版本,您将有一个新的变量分配,这样很明显您正在使用新版本的数据从这里开始,任何可能使用旧对象的代码都不会受到影响.:

If you define an object, you know for sure it will always stay unchanged. If you need a modified version, you would have an new variable assignment, this way it is obvious that you are working with a new version of the data from here down, and any code that might use the old object will not be affected.:

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

// ... much other code ...

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

优点、缺点和注意事项

我无法给出一般性建议,哪种方式(复制或变异)是更好的".变异的性能更高,但如果您不确定发生了什么,可能会导致许多难以调试的问题.至少在有些复杂的情况下是这样.

Pros, Cons, and Caveats

I couldn't give a general advice which way (copy or mutate) is "the better one". Mutating is more performant, but can cause lots of hard-to-debug problems, if you aren't absolutely sure what's happening. At least in somewhat complex scenarios.

正如我在原始答案中已经提到的,非纯函数可能会无意中改变一些外部状态:

As already mentioned in my original answer, a non-pure function might unintentionally change some outside state:

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.

有人可能会争辩说您可以预先复制初始值:

Somebody might argue that you can copy the initial value beforehand:

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

但是在以下情况下,这可能效率更低,例如该对象非常大且嵌套,经常调用reducer,可能有多个条件使用的小修改减速机内部,变化不大.(想想 React 中的 useReducer,或 Redux 减速器)

But this might be even less efficient in cases where e.g. the object is very big and nested, the reducer is called often, and maybe there are multiple conditionally used small modifications inside the reducer, which are only changing little. (think of useReducer in React, or the Redux reducer)

另一个答案正确地指出,即使使用据称纯方法,也可能仍然引用原始对象.这确实是需要注意的事情,但是只有当您不遵循这种一成不变"的方法结果时才会出现问题:

An other answer stated correctly that even with the supposedly pure approach there might still be a reference to the original object. And this is indeed something to be aware of, but the problems arise only if you do not follow this 'immutable' approach consequently enough:

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

这不是问题,如果注意不复制引用(有时这可能不是微不足道的):

This is not a problem, if it is taken care that no references are copied (which might be not trivial sometimes):

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.表现

几个元素的性能没有问题,但是如果对象有几千个项目,性能确实是一个很大的问题:

3. performance

The performance is no problem with a few elements, but if the object has several thousand items, the performance becomes indeed a significant issue:

// 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');

这篇关于减少函数中的变异累加器是否被认为是不好的做法?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆