Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strange edge case with Map.update(...) #1984

Open
arnfaldur opened this issue Feb 14, 2024 · 3 comments
Open

Strange edge case with Map.update(...) #1984

arnfaldur opened this issue Feb 14, 2024 · 3 comments

Comments

@arnfaldur
Copy link

This was discussed in this issue #1061 but does not seem to be fixed and I could not find any mentions of it in the documentation.

The issue is the behavior enforced in this test https://github.com/immutable-js/immutable-js/blame/d7664bf9d3539da8ea095f2ed08bbe1cd0d46071/__tests__/updateIn.ts#L69
Here is the test in question to save you a click.

  it('identity with notSetValue is still identity', () => {
    const m = Map({ a: { b: { c: 10 } } });
    expect(m.updateIn(['x'], 100, id => id)).toEqual(m);
  });

As the test suggests, calling map.update("field", defaultValue, id => id) does not set the map's field to defaultValue as I would have expected. This behavior can lead to surprising bugs like the one illustrated by the following example:

const scoreData = [
  ['alice', 0],
  ['brian', 2],
  ['chris', 0],
  ['alice', 1],
  ['brian', 0],
  ['chris', 0],
  ['alice', 1],
  ['brian', 1],
  ['chris', 0]
];

const scores = scoreData.reduce(
  (scores, [player, score]) =>
    scores.update(player, 0, (previousScore) => previousScore + score),
  Immutable.Map()
);

This takes a set of players and scores and aggregates the score data for each player and I would expect this:

console.log(scores.toJS())
> { chris: 0, brian: 3, alice: 2 }

but the actual result is this:

console.log(scores.toJS())
> { brian: 3, alice: 2 }

This happens because previousScore => previousScore + score is the identity function if score === 0 and previousScore is a number. To mitigate this something like this works:

const scores = scoreData.reduce(
  (scores, [player, score]) =>
    scores
      .update(player, 0, (previousScore) => previousScore + 1)
      .update(player, 0, (previousScore) => previousScore - 1)
      .update(player, 0, (previousScore) => previousScore + score),
  Immutable.Map()
);

Less generally one can do .update(player, null, previousScore => previousScore + score) which works because null + 0 evaluates to 0 while null != 0. or more generally .update(player, null, (prev) => (prev === null ? 0 : prev) + score) which should work for a larger variety of types.

.updateIn has the same issue.

What do you think?

Would you be open to a PR changing this behavior?

@jdeniau
Copy link
Member

jdeniau commented Apr 19, 2024

It seems a tricky one, because I think that the default value happen "after" the update function.
It is useful if the expected value is to return the default value if the function return undefined.

Without looking at other immutable functions, I would say that the default value should be injected in the argument of the callback if the key doesn't exist (if your are willing to open a PR, that will be great!)

fix for your case

I think that in your particular case, you can handle that in your function directly:

prevScore => (prevScore + score)?? 0

Or something like that (maybe (prevScore?? 0) + score, I do not have a computer now to validate).

@arnfaldur
Copy link
Author

I'm not working on that code anymore and not consuming immutable-js anywhere else so I'm less invested in the outcome of this. I will therefore unfortunately most likely not work on a PR addressing this in the near future.

IIRC this line .update(player, null, previousScore => previousScore + score) worked for my original usecase.

@jdeniau
Copy link
Member

jdeniau commented Apr 20, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants