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

RFC: Replace run and mutate operator with action #173

Closed
christianalfoni opened this issue Dec 26, 2018 · 5 comments
Closed

RFC: Replace run and mutate operator with action #173

christianalfoni opened this issue Dec 26, 2018 · 5 comments

Comments

@christianalfoni
Copy link
Member

So we are closing in on release, woop woop!

I have one final breaking change I want to do. I want to replace the run and mutate operator with a single action operator. Let me explain why.

1. Introducing the concept

When we now explain Overmind we start out with simple imperative actions.

const myAction = ({ state, value }) => {
  state.foo = value
}

When we move to the functional world of operators we could make the jump by simply doing:

const myAction = action(({ state, value }) => {
  state.foo = value
})

Congrats, you have now created a lego block out of your action. You can now make this action only callable when the value has a certain length:

const myAction = pipe(
  filter(({ value }) => value.length > 2),
  action(({ state, value }) => {
    state.foo = value
  })
)

And so on. It creates a natural stepping stone from imperative actions to functional actions, where you have additional operators to manage "FLOW".

2. Overdoing the separation

The operators API should be closer to an action than rxjs. The reason is that there are only certain functional aspects that truly gives value. Let me explain with an example:

export const search = pipe(
  setQuery,
  filterValidQuery,
  debounce(200),
  getResult,
  catchResultError,
  setResult
)

This looks very nice, but the last part there. Creating three composable pieces to get and set the search result. This is where I think we are overdoing it. I think it makes a lot more sense to do:

export const search = pipe(
  setQuery,
  filterValidQuery,
  debounce(200),
  getSearchResult
)

Where getSearchResult looks like:

export const getSearchResult = action(async ({ state, api, value: query ) => {
  try {
    state.searchResult = await api.getPosts(query)
  } catch (error) {
    state.searchError = error
  }
})

Summary

So by getting rid of run and mutate we reduce API surface, we create a simpler transition to the functional world and we expose operators that actually manages FLOW. Like filter, debounce etc. Now map becomes an operator to put values into the flow, not as an intermediate step every time you want to express "grab data from effect and set state"... which is what we mostly do :)

Okay... thanks for listening :-)

@etienne-dldc
Copy link
Contributor

etienne-dldc commented Dec 26, 2018

I also encountered 2. while playing with overmind and that's what made me explore some alternative way to express actions (that and the type of parallele 😄 ) (https://github.com/etienne-dldc/meleze-store).
To be more specific I was doing some pattern matching (branch) and dealing with null value was just not practical... it feels like the declarativeness, while great for the most part, was holding me back.
Note: As I said before, I'm not proposing to change overmind's API, I'm just sharing my ideas in case it helps :)
So once I realized that I was not happy with the code I was writing I thought about how I could make it better. The answer I found was that the code I was having a hard time expressing declaratively should be written imperatively, with plain old Javascript !
You might say that RFC above solve my problem, and in a way it does. But what made me thing about a different system is that the action above is basically an opt out button, once you are in an action (aka imperative mode) you can't really go back to declarative, your only way is by returning an value but then it feels a bit out of order...
So how did I solve this ? By using the return value as the next thing to do.
Basically instead of building a function, I'm building an object (called an executable) that can be executed.
So const setFoo = mutate(({ state }) => { state.foo = 'bar' }) return an object that contain the mutation function.
Then you have an action() executable factory that take as parameter a function that must return an executable:

const maybeSetFoo = action(({ value }) => {
  if (value > 42) {
    // we return the mutation we want to perform
    return setFoo;
  }
  // null is ignored
  return null;
})

Here I could have used a filter but for more complex use case and things like type guards it's just not practical to use declarative code.
If you want to see what it looks like you can take a look here (still very WIP) : https://github.com/etienne-dldc/meleze-store/blob/master/src/index.ts

@abalmos
Copy link
Contributor

abalmos commented Dec 27, 2018

@christianalfoni Is this a thought experiment or does the implementation live somewhere we can play with?

@christianalfoni
Copy link
Member Author

@etienne-dldc Thanks for your input here! I see where you are going and yeah, that is certainly powerful! And I guess there is nothing wrong with us creating an operator that does exactly that. Instead of like when which returns a boolean, you return an actual operator. Though not sure about typing etc, which I guess you worked a lot on? :)

One thing I see though is that even though the implementation has a lot of power, it will hide some implementation details in the expressed pipe.

pipe(
  maybeSetFoo // You do not know what operator is actually run when truthy
)

pipe(
  when(valueIsMoreThan42, {
    true: setFoo,
    false: null
  })
)

This was of course a contrived example as well and splitting up into a when etc. is exactly where you start doing quite some additional work, as you point out. It is a difficult one and really nice to have both options POC-ed :)

@christianalfoni
Copy link
Member Author

@abalmos It is a tiny change I will do later today a long with a couple of other small fixes... which marks that my list of fixes is done and I can focus on more docs and start preparing some videos :)

@christianalfoni
Copy link
Member Author

Old stuff

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

3 participants