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

Investigate Cerebral #343

Closed
gaearon opened this issue Jul 28, 2015 · 34 comments
Closed

Investigate Cerebral #343

gaearon opened this issue Jul 28, 2015 · 34 comments

Comments

@gaearon
Copy link
Contributor

gaearon commented Jul 28, 2015

Cerebral by @christianalfoni seems very interesting.

People say:

sticking with cerebral. Signals concept is a winner! Latest update is even more flexible.

Is there something we can bring to Redux from Cerebral?

@Agamennon
Copy link

i was looking into it, wonder if it is able to hot-reload, but the thing that made the most sense to me was that the we are definitively doing the router wrong! this video highlights the issue.

@ioss
Copy link

ioss commented Jul 28, 2015

Is there something we can bring to Redux from Cerebral?

The UI of the Debug-Tools? j/k

I haven't checked the complete code yet, so all my impressions are from the video. Same goes for Redux, as I just started with react two days ago. (So maybe I should not answer at all? :) )

The chaining of the signalhandlers (Cerebral: ?actions?, Redux: reducers) in response to a signal (Redux: action) is maybe - haven't decided yet - nice. It provides some documentation out of the box, but is nothing that can't be done by chaining normal functions in a reducer. Also, it will only call the handlers responsible for an action and not call every reducer (that's how I understood Redux for now). Don't know if that is performance relevant.

Also signalhandlers can return values, that get merged with their args and will be passed to the next signalhandler in the chain. Something I actually don't like, as it creates a hidden contract between any two signalhandlers.

One "thing" that we could look into is how easy it is to have async handlers/reducers (well actually more like ActionCreators) in Cerebral: Just put them into Arrays inside of the chain.
In doing it that way the devtools know that it is an async handler an will store the result, so that on replay the actual function will not be called. Which on the other hand means, that the function should probably not do any state changes and only transform/enhance the signal data to be consumed by the next sync handler in the chain.

All in all, I don't see anything that can be done in Cerebral, that can not be done in Redux.

I still think - if possible at all, still wrapping my head around it - we should come up with a solution for async, that put's the function closer to the reducers (even if it is only by naming or examples) or even into them, as this seems to be - as far as I can tell from the issues here - one of the harder problems for new users of Redux?

@christianalfoni
Copy link

Hi guys, just putting in my two cents!

I am sorry to say I have not looked too much into Redux yet... very interested, but spending as much time as I can on getting Cerebral to a 1.0 version :-)

What I think users of Cerebral benefits from, as @ioss points out, is how you define "application flow". What I mean is that when you request a state change from the UI layer, or whatever, you often have many things you want to occur. An example would be:

  1. Set application in loading state
  2. Get something from the server
  3. Set the thing from the server in your state store
  4. If server request fails, set an error in state store
  5. Run some calculations etc. based on the new stuff you got from the server to set as state
  6. Unset application loading state

All this is described very precise with a Cerebral signal, which also the debugger benefits from. It is not clear to me how you would define this kind of flow with Redux, but as I said, I am not familiar enough with it.

@ioss I completely see your point with the "args". The reasoning behind it is that although the actions are reusable across signals, they should be able to operate on some input to the signal chain itself and some output from a previous signal. For example handling output from an async signal. That said, this merging strategy might not be the best way to go. Let me know if you have some ideas on how it could be solved differently :-)

@Agamennon The hot-loader for React, by @gaearon, is really amazing! But one thing I think we often overlook is that the really great thing about the hot-loader is not avoiding a browser refresh, it is that the components keep their state after you change your code. So, even though you have to refresh the browser, using Cerebral, your application continues in its previous state. And it does not require React to do so. I understand Redux also has a local-storage middleware? So it works there too, but hot-loading requires React. Again, live code update is awesome for sure, but live-reload with reproduced state on load gives the same thing.. its just a browser blink further away ;-)

Thanks for taking a look at Cerebral and giving the feedback. It is just as useful for me! So thanks for putting up the issue @gaearon!

@nateajohnson
Copy link

Question for "hot loading" for both frameworks: if you've navigated a few links deep into a wizard and pulled up a modal, what happens when code is changed and hot reload happens? Does the nav and modal stay where they are with state intact?

@gaearon
Copy link
Contributor Author

gaearon commented Jul 28, 2015

@nateajohnson This is not really related either to Redux or Cerebral and depends on your hot reloading solution. React Hot Loader keeps the state of React components so yeah, as long as you're using React, UI stays during code change.

@christianalfoni
Copy link

@nateajohnson Just adding that you can keep state with normal Live-Reload too, using Cerebral (Also Redux I think?). This does not specifically require React, but you browser will blink, as it is a full refresh.

@nateajohnson
Copy link

Thanks Dan. I only replied to this thread because it was mentioned that hot reload in cerebral works with a reload. I'm just trying to understand how that can be replayed in both frameworks. I think from what you said it works ok in redux because there is no reload.

@gaearon
Copy link
Contributor Author

gaearon commented Jul 28, 2015

I demo hot reloading with (and without) Redux in my talk: http://youtube.com/watch?v=xsSnOQynTHs
You can indeed implement “hard” reloading in Redux with something like persistState from devtools.

@nateajohnson
Copy link

Thanks @christianalfoni and @gaearon. I appreciate the prompt answers from both of you. I think I'm starting to get it. I'm going to step out and watch both projects for a bit.

@ms88privat
Copy link

For me this "controller.signal('formSubmitted', setLoading, [saveForm], unsetLoading);" and "controller.recorder.record(controller.get());" looks really great. I think it is possible with Redux too, but you don't have it out of the box?

@idream3
Copy link

idream3 commented Jul 29, 2015

@gaearon @christianalfoni Personally I have found that the conceptual model of signals (or channels, impluses...) as implemented in Cerebral is really nice to work with.

Having all UI 'inputs' simply emit a signal/impulse when some thing has 'happened' separates the UI from taking actions directly, or, just decouples it in a way that I like. Then, It is up to the 'controller' (brain) to decide what actions to actually take.

Having the ability to look at a signal and see that it describes the action flow is fantastic, especially with async flow.

Controller.signal('formSubmitted',
  setLoading,
  [saveForm, {
    resolve: [closeModal],
    reject: [setFormError]
  }],
  unsetLoading
);

I have never liked the ActionCreator concept from Flux - Why do we need to create an action? (Although I understand the technical reason).

Oh, and the Chrome dev tools debugger is so great - being able to see each executed signal with it's corresponding actions and payloads.

I urge you to have a play with it yourself. You may be interested in something like You might be interested in https://github.com/garth/ganglion-impulse as middleware, if that is possible.

I have not had any experience with Redux yet, but I having been following closely.

Thanks for all your amazing contributions :)

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

I have not played with Cerebral yet (lol I'm also crazy busy with our 1.0 release) but it looks like Signals correspond to Actions going through Middleware very closely. The only difference so far is that Signals seem a first-class concept, and they are composable. Maybe we can add something composable like createSignal too in 2.0. I'd like to see some API proposal here from people understanding both Redux and Cerebral.

I have never liked the ActionCreator concept from Flux - Why do we need to create an action? (Although I understand the technical reason).

An action creator in Redux isn't that different from a signal. It's just that we don't ship a helper to compose action creators into some kind of a flow. In Redux, there's an additional difference: instead of operating on the model directly, the action is given to the reducer, but in the context of signal composition this doesn't make any difference.

@philholden
Copy link

I've been doing the set loading workflow like this in redux with thunk middleware:

export function fetchCampaigns(mode) {

  return (dispatch) => {
    dispatch(setLoading(true, mode));

    getCampaigns(mode)
    .then(rows => {
      dispatch(setLoading(false, mode));
      dispatch(setRows(rows));
    })
    .catch(() => {
      dispatch(setLoading(false, mode));
    });
  };
}

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

Yeah, it seems to achieve a similar goal. Signals are more opinionated (e.g. async = always a promise), but this helps them be concise (array instead of a bunch of waterfall calls).

I feel createSignal can be implemented in userland, like reselect implemented Nuclear's selectors in userland. Then we'll figure out how to hook it up to devtools and voila!

@ioss
Copy link

ioss commented Jul 29, 2015

@gaearon : With createSignal you are referring to Controller.signal('methodName', ...handlers) ?

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

@ioss Yes.

@ioss
Copy link

ioss commented Jul 29, 2015

Yes, I see this too in "redux-addons".

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

How does Cerebral express a sequence vs a parallel?

e.g.

controller.signal('formSubmitted',
  setLoading,
  [saveForm, {
    resolve: [closeModal],
    reject: [setFormError]
  }],
  unsetLoading
);

looks like a sequence. How do I run saveForm and saveOtherForm in parallel?

@christianalfoni
Copy link

controller.signal('formSubmitted',
  setLoading,
  [saveForm, saveOtherForm, {
    resolve: [closeModal],
    reject: [setFormError]
  }],
  unsetLoading
);

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

@christianalfoni

So first level is sequential, arrays inside it are parallel. Can you nest sequential inside parallel? Another question is: do the arrays inside resolve/reject also mean “parallel”?

How do you pass data between signals? Is there just initial arguments being passed in a sequence? Can signals modify the arguments they output?

@christianalfoni
Copy link

@gaearon ,

You can not nest sequential inside parallel, at least not now. Have not met a use case for that yet. The arrays after resolve/reject are run synchronously, but if you put an array in there those are run async again.

controller.signal('formSubmitted',
  setLoading,
  [saveForm, {
    resolve: [closeModal, [moreAsync]],
    reject: [setFormError]
  }],
  unsetLoading
);

Thinking of it, the following syntax might make things more clear:

controller.signal('formSubmitted', [ // <- array
  setLoading,
  [saveForm, saveOtherForm, {
    resolve: [closeModal, [moreAsync]],
    reject: [setFormError]
  }],
  unsetLoading
]);

So the resolve/reject is just concatenated on to the initial array. Hm... is this clearer? Might consider changing to that as it would allow for "options" on the signal more easily.

All actions receives an "args" object where initial signal value, returned action values, resolved action values and rejected action values are merged in. If that makes sense :)

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

@christianalfoni What benefits do you find to this kind of control flow DSL compared to imperative code like this? Being able to see the flow in debugger? Not messing with promises manually?

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

There ought to be a library that transforms some DSL like sequential(doSomething, doSomethingElse, parallel(wow, someOtherCoolStuff)) where doSomething and friends are (args) => Promise into a single (args) => Promise.

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

Somebody please implement redux-co. :-)
https://github.com/tj/co

@gaearon
Copy link
Contributor Author

gaearon commented Jul 29, 2015

Although frontend isn't ready for generators IMO.. Maybe something like https://github.com/thunks/thunks instead.

@christianalfoni
Copy link

@gaearon It all actually started out with the debugger. I wanted the developer to understand the flow of the application just playing around with the UI. This led to the initial signals implementation. When Cerebral first got some attention I was surprised that the signals implementation got more feedback than the debugger, it was just a fun and easy way to express the flow of the application :-)

So yeah, my conclusion is that I wanted an abstraction that lets you read a flow in your application from start to end without scrolling and jumping into different files. It was intended for the debugger, but the code itself became just as clear. I think the imperative code belongs in the actions, its "lower level" and can be a lot more complex. If that makes sense :)

Initially an action had to return a promise to be async. The problem with that is that Cerebral did not understand that an action was async at runtime. The other problem is that you could not clearly see in the signal what actions actually were async. So changing that, using arrays, solved those things. I started with a custom async implementation, but chose to use promises instead... just less work ;-)

It would indeed be nice with a standard implementation of this kind of sequence. It is really nice for expressing flow!

For the record I just want to mention @marbemac, who helped a lot with discussions and ideas on the current signals implementation.

@vladap
Copy link

vladap commented Jul 30, 2015

The flow definition is a data structure. It allows to treat code as data and manipulate it programmatically. Metaprogramming can be applied to write programs which either generate new flows or take existing flow and modify it to create a new one. It reminds me that one day I should learn some LISP.

I assume Cerebral execution layer is exploiting this property to automatically generate action log for replay and removes some of its boilerplate.

I think that significant difference from Redux design is that pure actions (aka reducers) can return values directly to async actions which then can continue in flow without having to either move more pure logic code to async actions (action creators) or abuse app state for transient values. It allows Cerebral to potentially replay more code and write logic in better enclosed actions. While Redux sometimes requires to artificially slice it away from reducers or delegate reducers to a dumb state updaters.

I tried to better described it in this comment in #291

DSL is double edged sword though. It is similar argument as using html templating language or write it directly in javascript (React). If DSL will be enough expressive to successfully declaratively define logic flow then fine. Otherwise in some cases it will be required to hide flow in actions. Can it express conditional branching? If such branching will be near a top level than I assume the rest of flow would be hidden in an action, no?

Today javascript is not that bad and when complex logic is properly factored out to functions then expressing the flow with Promise.then and Promise.all and arrow functions can be terse as well with all the flexibility on hand.

But DSL is exactly what gives Cerebral the power to generate action log for replay. If I understand the whole thing...

@vladar
Copy link

vladar commented Aug 1, 2015

@christianalfoni Given the example with "formSubmitted" signal - what would happen if this signal comes twice in a row? Say user unintentionally clicked "submit" button twice. And second signal came when form has already been in "submitting" state.

I am curious, because in general case reaction to event is a function of both - current state and event. So it's interesting how cerebral approaches this.

@christianalfoni
Copy link

Hi @vladar,

It is an interesting question indeed :-) In this scenario I would set an "isSubmitting" state as part of an action in the signal. That would be used to disable the submit button in the form, avoiding any more clicks as the form is submitting.

So the general answer would be to set a state that prevents the possibility to trigger the signal. There is no implementation that allows you to prevent a signal from running "from inside" the signal.

I suppose it is a question of what should be responsible for controlling when a signal can be triggered or not. Should it be the signal itself? Or should it be the event that triggers the signal? Currently it is the event that triggers the signal.

Let me know if you have other scenarios or if I was unclear on anything here :-)

@christianalfoni
Copy link

@vladap You make a very good point on the signals of Cerebral being an implementation that can describe flow good enough. You always meet some conditionals and currently the Cerebral signal implementation can branch out the result of async actions. So if async action(s) resolve, you can go down the resolve path with actions, or down the reject path with different actions.

cerebral.signal('appMounted', [getUsers, getProjects, getTasks, {
  resolve: [setUsers, setProjects, setTasks],
  reject: [setError]
}]);

A Cerebral signal is actually just a list of functions really. So a replay is basically just passing in the same initial argument to the signal and run the functions again. But there is special handling of async functions, one part storing their resolved/rejected value so in a replay it skips running the function and just resolves/rejects immediately.

@ericelliott
Copy link

For the debugger UI, therea's a slider monitor for Redux.

I'm excited to see this cross-pollination between Redux and Cerebral. Very cool.

@peteruithoven
Copy link
Contributor

The following blog post describes an alternative action > reducers flow with Redux like Reducers, which also gives you a flow overview, a bit like Cerebral?
http://www.code-experience.com/problems-with-flux/
(Disclaimer: I don't Cerebral and I'm just starting to learn Redux)

@gaearon
Copy link
Contributor Author

gaearon commented Sep 24, 2015

Closing as inactive.
Feel free to continue the discussion!

@gaearon gaearon closed this as completed Sep 24, 2015
@gaearon
Copy link
Contributor Author

gaearon commented Dec 22, 2015

There's been some interesting development on explicit side effects and async control flow in the Redux Saga project. Check it out: #1139

I think it may even allow us to build Cerebral-like async control flow debugger with full information about async actions: redux-saga/redux-saga#5.

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

No branches or pull requests