-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Transducers in Redux #176
Comments
Interesting! I prototyped this when looking at reducing over a whole log, since it seemed like a cool kind of optimization and I was generally excited about transducers. :) In stepping back a bit, it seemed like two other optimizations might be more important than allocations. One was to keep snapshots of previous reductions (especially as the Another thought is that I think finding the right API for composition is even more important, and so grounding compute or memory optimizations in real numbers might help too. I don't have a sense of quite how bad the naive solutions are, so that might help inform what optimizations can have the most impact. |
Wow!
Well not precisely, there is actually a spec for transducers in JavaScript https://github.com/cognitect-labs/transducers-js#the-transducer-protocol it mentions three functions instead of one etc. This spec has number of compatible implementations:
Maybe it would be cool if Redux support actual spec, then we could use existing transducers implementations. Although it probably too crazy :) |
It's about time, yo! ;) I'll mess around with this idea a bit today. I think the easiest way to do this is to transparently wrap each |
@rpominov following a spec is the whole purpose of transducers! That way our reducers can talk with actions and really any other communication system that follow the spec (collections, streams, channels, pipes, observables…). Clojure's page about transducers has some insight. It also links this nice talk of Rich Hickey. |
@hugobessaa I agree... Using ramda with redux will be so comfortable this way |
While we're discussing - have we thought about using transducers for middleware? After all, middleware in Redux seems to be (correct me if I'm wrong) just a form of event stream processing. I think with enough thought these could really tie a lot of the functional concepts we already have together nicely. And with a default set of Redux-specific middleware transducers and a nice API for creating new ones, it would be very possible to do this while maintaining the "don't have to live in a cabin built out of functional programming books to understand this" philosophy of the library. If you stretch the concept to its extreme, you could implement Flux as one gigantic transducer. Stores reduce over actions to accumulate their state, components reduce over store state to display the proper UI elements and actions reduce over input events on components to perform their duties. It's the circle of life, baby! I'm not actually suggesting this (I've actually tried it - turns out transducers don't solve everything D:), but I think it's worth considering various uses for them while we're on the topic. |
@Funkenstein this is pretty much the way Cycle.js works (just with different naming and events/views decoupling). I think Redux should do as much as possible to be just a lib that connect stuff that aren't redux specific. |
@hugobessaa Ah, I believe I misunderstood the goal. We don't want transducer support in Redux, we just want transducers written for Redux. And upon using them, they will just return standard reducer functions which are already compatible with Redux (easy enough, any transducer library worth its salt has a |
I'm dumb in this area so I'd be happy to see whatever you folks come up with 💃 |
The most common transducer I think will be something like this, which simply executes multiple reducers in sequence https://github.com/acdlite/reduce-reducers. I'm not an expert on this topic either... I'm sure this operation already has an actual name in FP. |
Okay I lied, that's not a transducer... A transducer is |
I made a library to create reducers from transducers: https://github.com/acdlite/redux-transduce It supports transducers that conform to the transducer protocol used by transducers.js and transducers-js. I did discover a caveat, though (quoting from the README):
I'm not sure if this limits its usefulness prohibitively. On the plus side, I finally understand transducers :D |
Isn't it possible to store transducer (and generally, middleware) state in the global app state under some unique key? |
I thought about that, but how would the key be generated? What if you call the same transducer multiple times? Eventually, you end up just writing your own reducer manually, which kinda defeats the purpose. |
@acdlite Wow, cool! I was thinking about function with exact same signature earlier today :) I would implement it a bit different, though: function applyTransducer(transducer, reducer) {
const xform = transducer({
'@@transducer/step': reducer
})
return (state, action) => {
const result = xform['@@transducer/step'](state, action)
if (result && result['@@transducer/reduced']) {
throw new Error('Early termination doesn\'t make sense in context of Redux (you should\'t use transducers like take(n))')
}
return result
}
} This way stateful transducers like |
@acdlite A unique key could be used for that. One per an invocation of any (including async) action creator. Update: actually, this won't work, I guess. We'd need a way to let transducers store data there, which would require reimplementing them. Thoughts? |
@rpominov That doesn't look like it works with stateful transducers to me... Can you show me an example? |
@rpominov Okay, I can see how that could work with |
@acdlite Right, but I think they don't intend to be pure anyway, although it might be a problem with their usage in Redux, I agree. Seems like transducers don't fit very well here. Btw, I recommend everybody to watch (if haven't already) this talk by Rich Hickey about transducers |
Sorry, need to clarify, I meant transducers don't intend to be pure, while reducers in Redux certainly are. |
Custom implementations that would have their state stored externally may be used instead of stateful transducers. A key would be generated per each usage of such transducers. So, instead of |
How is that pure? |
Why not? |
But where the state of |
|
Those keys can be stored under another unique key, one per an action creator invocation. This should work, but looks really complicated. Would some kind of middleware be able to simplify that? |
On the other hand, it's difficult for me to imagine a use case where such pseudo-stateful operations were necessary for a reducer. |
If we want to make each action invocation to be pure, @acdlite already did that in https://github.com/acdlite/redux-transduce by simply initialising transducer on each action. What we can't do is to allow transducers to store state between actions invocations (if we want reducers to be pure). |
@rpominov If there are good use cases for this, it can be done. Transducers needn't store local state, they can return it instead and it can be made to land in the global app state. The problem is that this brings in more magic. |
So it turns out that transducers work really well if you use them outside the reducer — to dispatch actions! This can be done by wrapping the store (using a higher-order store) so that it conforms to the transducer protocol. Then you can use https://github.com/acdlite/redux-transducers#example-mapping-strings-to-actions |
I think the approach @acdlite posted above is a great method for combining transducers with redux. As it was pointed out above, an important aspect of the design of redux is that its reducers are pure functions, and some transducers are stateful transformations. The idea that you could use only stateless transducers is problematic because transducers are composable, and it may be difficult to know whether a composed transducer contains any stateful operations. However, by transforming the input to a reducer, you can keep the reducer pure, enable stateful transformations, and preserve the ability to combine and reuse those transformations by applying them to the input of other reducers. |
Perhaps alternatively a library such as baconjs. There is a library called react-bacon that sorta prototyped the idea that I played around with once before. There is also a react rxjs library too that may be woth looking at. |
Closing, as the issue has been dormant for a while. Please feel free to keep the discussion going. |
Sorry to necro this thread, but I just came across it. Very interesting example of using transducers with Redux @acdlite. Thanks for that. Regarding this though:
A UUID generation function is the least pure function you can possibly have. It's guaranteed to return a different value every time you call it! 😄 It's even worse than a random number generator, because that will sometimes return a previously returned value. |
@mindjuice You're right, thanks! That's a glitch in my thought process that happened due to inertia after thinking about commands vs. queries (CQS) in context of another problem. UUID has query signature (it returns a value), which somehow led me to stating that it is pure. But actually it isn't pure nor does it satisfy CQS, as it is not referentially transparent. |
Reusing Store functionality is something Flux has traditionally been very bad at, due to Stores not being composable. For example, I do silly things in Flux React Router Example just to reuse the pagination code in different Stores.
The reason for this being hard in Flux is because each Store is an event emitter, and composing event emitters is hard. But in Redux, “Stores” (reducers, really!) are pure functions, and pure functions are easy to compose!
This gist shows how reducers can be easily composed. But it's still manual composition. It's expressive, but there is some boilerplate for repetitive tasks. For example, you might want to implement things like optimistic updates, undo/redo or pagination scoped to specific parts of your state, but then you can't do it just once on the top—you need to repeat this logic in every relevant reducer.
Higher order functions to the rescue! In fact, we already have a higher-order reducer:
composeStores
(to be renamedcomposeReducers
). It takes your app's reducers and returns a reducer that composes their state into a single object.We can express more patterns with higher order reducers? Absolutely! In fact this may be the beginning of reusing logic in Flux because these higher order reducers don't need to know about specific actions in your app.
Consider this:
It's an example higher order reducer. Its purpose is a performance optimization by “cutting off” some expensive reducers from some unrelated actions.
This is just an example of what higher-order reducers can do. They're not tied to your particular app. You can pass particular action types as parameters to them so they know which precisely actions to handle.
You can user higher-order reducers to implement things like:
in a generic way, reusable across the Redux ecosystem.
And guess what? We didn't invent higher-order reducers! They are precisely the same thing as transducers in ClojureScript.
Anybody want to take a stab at writing some transducers for Redux?
The text was updated successfully, but these errors were encountered: