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

consider using Immer #295

Closed
nicolodavis opened this issue Nov 2, 2018 · 6 comments
Closed

consider using Immer #295

nicolodavis opened this issue Nov 2, 2018 · 6 comments
Assignees

Comments

@nicolodavis
Copy link
Member

nicolodavis commented Nov 2, 2018

People get immutability wrong all the time (self included). Even the ones that know what they're doing often have to write some really messy code when deep state updates are involved.

Immer seems to achieve the best of both worlds. We should consider bundling it in (and possible have a flag that allows turning it off for people that prefer not to use it).

move: (G, ctx) {
  G.hand++;
  G.deck--;
}

vs.

move: (G, ctx) {
  return {
    ...G,
    hand: G.hand + 1,
    deck: G.deck - 1,
  };
}

Open questions:

  1. How do we indicate invalid moves ( Illegal moves API #292 ) in this approach?
@philihp
Copy link
Contributor

philihp commented Nov 5, 2018

I would like to suggest the following syntax, as to avoid breaking existing moves, and is similar to normal Redux action creators vs. Redux-thunk action creators.

  • If the move comes back with an object (or an undefined), nothing is different. Assume the move function properly follows immutability practices (a equality of the input to output is also a deep equality).
  • If the move comes back with a function, then let's it's an producer, and pass that to immer's produce()
// default (current) behavior, process without Immer
drawCard: (G, ctx) => ({
  ...G,
  hand: G.hand + 1,
  deck: G.deck - 1,
})
// process with Immer
drawCard: (prevG, ctx) => nextG => {
  nextG.hand++;
  nextG.deck--;
}

In this manner, an invalid move returning undefined could be expressed as

drawCard: (prevG, ctx) => {
  if(prevG.deck === 0) return undefined
  return nextG => {
    nextG.hand++;
    nextG.deck--;
  }
}

@nicolodavis
Copy link
Member Author

Immer allows you to return the new state, so we can just turn on Immer in a backwards compatible way without needing to return a thunk.

I think both the following would be valid:

drawCard: (G, ctx) => {
  G.hand++;
  G.deck--;
}
drawCard: (G, ctx) => ({
  ...G,
  hand: G.hand + 1,
  deck: G.deck - 1,
})

so people can just choose the style that they prefer.

The main problem is the invalid move API. One option would be to do something like this:

import { INVALID_MOVE } from 'boardgame.io/core';

drawCard: (G, ctx) => {
  if (G.deck === 0) return INVALID_MOVE;
  ...
}

I think it's reasonable to disallow setting G to an string / integer constant so that INVALID_MOVE is never confused with actual game state.

@philihp
Copy link
Contributor

philihp commented Nov 6, 2018

It feels like that's enabling people to continue to get immutability wrong. How about this syntax?

drawCard: (G, ctx) => {
  this.hand++;
  this.deck--;
}

Regarding INVALID_MOVE, that looks like a reasonable syntax. The immer nothing constant could be aliased, and we'd get that behavior basically for free.

import { nothing } from "immer"
export const INVALID_MOVE = nothing

@nicolodavis
Copy link
Member Author

nicolodavis commented Nov 8, 2018

I prefer to think of it more as Immer allowing people not to think about immutability (rather than enabling them to get it wrong). For people that are not familiar with the concept, I think there is a basic expectation that they should be able to mutate G and everything should just work. Immer enables this while also being performant (and also providing an escape hatch for people that want to do immutability manually).

So I think we should just allow (Immer handled) mutations on Grather than using this or some other syntax, especially considering that we're not going to break existing moves (with the small caveat that we'll be changing the Illegal Moves API, which we might have done anyway in #292).

@philihp
Copy link
Contributor

philihp commented Nov 8, 2018

I can see the advantage of having current code errantly mutating state being safe once this is implemented. Immer automatically binds this to the draft of G inside of produce, so I would expect both should work.

@nicolodavis nicolodavis self-assigned this Nov 16, 2018
@nicolodavis
Copy link
Member Author

Supported in 0.28.0.

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