JavaScript - Reduce 関数でアキュムレータを変更することは悪い習慣とみなされますか?

okwaves2024-01-25  8

私は関数型プログラミングの初心者で、概念をより理解できるようにいくつかのコードを関数型に書き直そうとしています。たった今、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;
    },
    {}
);

明らかに、この関数は引数 acum を変更するため、純粋とはみなされません。一方、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;
    },
    {}
);

私の理解では、この関数は純粋であると考えられています。ただし、反復ごとに新しいオブジェクトが作成されるため、いつか、そしてもちろん記憶も。

そこで問題は、どのバリアントが優れているのか、そしてその理由は何なのかということです。純粋性は本当に重要であり、それを達成するためにパフォーマンスとメモリを犠牲にしなければならないのでしょうか?それとも、何かが足りないので、もっと良い選択肢があるのでしょうか?

2

元のアキュムレータを渡したが、それが他には存在しない場合に、アキュムレータを変更することが問題になるのはなぜだと思いますか?外部の副作用を作成している場合は問題になりますが (副作用のある機能ツールが悪であるという理由だけで)、書かれているように、外部から目に見える副作用はありません。確かに、これは少し不純です (そして、おそらく単純な for ループで実行した方がよいでしょう) が、とにかく Reduce を使用している場合はどうすればよいでしょうか?そうですね。

– シャドウレンジャー

2020 年 9 月 3 日 16:17

パラメータとして ({ ...accum }, comb) を使用しないのはなぜですか?即座にコピー ...

– ニーナ・ショルツ

2020 年 9 月 3 日 16:24

2

局所的な突然変異は、グローバルな範囲に漏れる突然変異よりも害が少ないです。しかし、地元の人々であっても、事態がさら​​に複雑になるとすぐに反撃する可能性があります。この問題は最近私に起こり、3 時間を費やして修正しました。

– 

user5536315

2020 年 9 月 3 日 18:58

2

「純度を達成するためにパフォーマンスとメモリを犠牲にするほど、純度は本当に重要ですか?」 -いいえ、決してそうではありません。ただし、コードを「機能的」と呼ぶべきではありません。純粋性と不変性を侵害する場合は、それ以上のことはできません。

– ベルギ

2020 年 9 月 3 日 19:21

ああ、気の利いた人、クラスに属さない関数が OOP でなくなるのと同じように、機能するために完全に機能する必要はありません。

– user1713450

2020 年 9 月 5 日 5:11



------------------------

TL;博士:アキュムレータを所有している場合はそうではありません。

JavaScript では、見栄えの良い 1 行の削減関数を作成するためにスプレッド演算子を使用するのが非常に一般的です。開発者は、その過程で関数が純粋になるとよく主張します。

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 の初期値は、実行中に作成される空のリテラル オブジェクトです。スプレッド演算子の使用は「表面的な」ものにすぎません。この時点での選択。どちらの関数も純粋です。

不変性は特性であり、プロセスそのものではありません。つまり、不変性を実現するためにデータのクローンを作成することは、おそらく素朴で非効率なアプローチである可能性が高いということです。ほとんどの人は、スプレッド オペレーターは浅いクローンのみを実行することを忘れています。とにかく!

私は少し前にこの記事を書きましたが、そこではミューテーションと関数型プログラミングが相互に排他的である必要はないと主張し、スプレッド演算子の使用が簡単な選択ではないことも示しました。

2

4

関数型プログラミングは相互に排他的である必要はありません。これは大胆なスタンスです。突然変異により、プログラムに時間が導入されます。これと同様に、これらは考慮する必要がある一種の暗黙的な状態パラメータです。突然変異は局所的および等価的レアを妨げるこれは、多くの FP イディオムの代数構造を維持する FP の最も重要な法則です。

– 

user5536315

2020 年 9 月 3 日 19:31

5

@scriptum わかっています! ;) しかし、それは「eval は悪だ」と言っているようなものです。ユースケースを問わず。いいえ、それは必ずしも悪いことではなく、自分が何をしているのかを確実に知る必要があります。 foo の 2 番目のバージョンは純粋な私見です。リダクション関数はアキュムレータを変更しますしかし、それはまったく有害な突然変異ではないことを認識する必要があります。これは実用的な関数型プログラミングです。私がそれを説明します。

– カスタムコマンダー

2020 年 9 月 3 日 19:38



------------------------

パフォーマンス上の問題が発生する可能性があるにもかかわらず、反復ごとに新しいオブジェクトを作成するのが一般的な方法であり、推奨される場合もあります。

(編集:) 一般的なアドバイスを 1 つだけ知りたい場合は、コピーすると問題が発生する可能性が低くなるためだと思います。 突然変異よりも問題があります。パフォーマンスが「本物」になり始める。問題 もっと持っていたらたとえば約 1000 回の反復です。 (詳細については、以下の私のアップデートを参照してください)

関数を純粋にすることができます。このようにして:

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

状態とリデューサーが別の場所で定義されている場合、純粋性がより重要になる可能性があります。

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' コールバックを明示的に渡した場合を除き、外部では変更されないことがわかります。

オブジェクトを定義すると、それが常に変更されないことがわかります。 変更が必要な場合IED バージョンでは、新しい変数の代入が行われます。 こうすることで、新しいバージョンのデータを使用していることが明確になります。 これ以降、古いオブジェクトを使用する可能性のあるコードは影響を受けません。:

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

// ... much other code ...

const myOtherObject = modifySomehow( myObject ); <-- new version of the data
長所、短所、および注意点

どちらの方法 (コピーまたは変更) が「より良い方法」であるかについて、一般的なアドバイスはできません。 変更するとパフォーマンスが向上しますが、何が起こっているのか完全にわからない場合は、デバッグが難しい多くの問題が発生する可能性があります。 少なくとも、ある程度複雑なシナリオでは。

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

しかし、これは c ではさらに効率が悪いかもしれません。たとえば、オブジェクトは非常に大きく、ネストされています。 レデューサーは頻繁に呼び出され、条件付きで小さな変更が複数使用される可能性があります。 減速機の内部はほとんど変化していません。 (React の useReducer を考えてください。 または Redux リデューサー)

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

1

2

アキュムレータを制御する場合、つまり、最初の例のようにインラインで作成した場合は、100% 常に変更する必要があります。その理由には、コードの実行時の理解が含まれます。突然変異 - 実行時間は O(n) です。n はリストの長さです。突然変異が存在する場合、そのコストは基本的に O(1) です。コピー - 実行時間は O(n * m) です。n はリストの長さ、m はキーの長さです。オブジェクト内でコピーされます。特に、オブジェクトのキーの数が反復処理によって増加する場合、これは事実上 O(n^2) になる可能性があります。

– マイク

2021 年 8 月 18 日 15:51

総合生活情報サイト - OKWAVES
総合生活情報サイト - OKWAVES
生活総合情報サイトokwaves(オールアバウト)。その道のプロ(専門家)が、日常生活をより豊かに快適にするノウハウから業界の最新動向、読み物コラムまで、多彩なコンテンツを発信。