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

Transducers in Redux #176

Closed
gaearon opened this issue Jun 24, 2015 · 35 comments
Closed

Transducers in Redux #176

gaearon opened this issue Jun 24, 2015 · 35 comments

Comments

@gaearon
Copy link
Contributor

gaearon commented Jun 24, 2015

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 renamed composeReducers). 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:

function whitelistActions(reducer, actionPredicate) {
  return (state, action) => actionPredicate(action) ? reducer(state, action) : state;
}

It's an example higher order reducer. Its purpose is a performance optimization by “cutting off” some expensive reducers from some unrelated actions.

const reducer = composeReducers({
  entities: composeReducers(entitiesReducers),

  // performance optimization!
  editor: whitelistActions( 
    composeReducers(editorReducers),
    action => action.type.startsWith('EDITOR_')
  )
});

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:

  • undo/redo
  • pagination
  • optimistic updates
  • performance optimization
  • throttling actions
  • etc

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?

@kevinrobinson
Copy link

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 n for items in the log grows) and another was an optimizer that could memoizing hot calls to reducers. The problems I saw were how to make this work when composing calls, I found I wanted to graph of computation Clojure style and then sort of stopped and didn't work much further on it. So I'm excited to see you're thinking about this and super curious to hear about ideas other people have. :)

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.

@rpominov
Copy link

Wow!

They are precisely the same thing as transducers in ClojureScript.

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 :)

@maybenomad
Copy link

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 function passed to composeReducers in a transducer which only implements the step method. And @rpominov isn't crazy at all, sticking with the spec outlined by Cognitect is definitely the way to go here - it gives us access to all of the above libraries for free (and the spec is honestly trivial to implement).

@hugooliveirad
Copy link

@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.

@alexeygolev
Copy link

@hugobessaa I agree... Using ramda with redux will be so comfortable this way

@maybenomad
Copy link

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.

@hugooliveirad
Copy link

@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.

@maybenomad
Copy link

@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 toFn function for transducer transformers).

@gaearon
Copy link
Contributor Author

gaearon commented Jun 26, 2015

I'm dumb in this area so I'd be happy to see whatever you folks come up with 💃

@acdlite
Copy link
Collaborator

acdlite commented Jul 4, 2015

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.

@acdlite
Copy link
Collaborator

acdlite commented Jul 4, 2015

Okay I lied, that's not a transducer... A transducer is reducer => reducer' Still useful, though!

@acdlite
Copy link
Collaborator

acdlite commented Jul 5, 2015

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):

Transducers typically operate on collections. It's possible to use transducers to transform asynchronous streams, but it requires the use of local state that persists over time. We can't do this, because Redux makes a hard assumption that the reducer is a pure function -- it must return the same result for a given state and action, every time.

For this reason, transduce() transforms actions one at a time. That means transducers like filter() and map() work fine, but take() and dedupe() do not.

I'm not sure if this limits its usefulness prohibitively. On the plus side, I finally understand transducers :D

@clearjs
Copy link
Contributor

clearjs commented Jul 5, 2015

Isn't it possible to store transducer (and generally, middleware) state in the global app state under some unique key?

@acdlite
Copy link
Collaborator

acdlite commented Jul 5, 2015

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.

@rpominov
Copy link

rpominov commented Jul 5, 2015

@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 dedupe() will work, and if a transducer with early termination used like take() an exception will be thrown.

@clearjs
Copy link
Contributor

clearjs commented Jul 5, 2015

@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?

@acdlite
Copy link
Collaborator

acdlite commented Jul 5, 2015

@rpominov That doesn't look like it works with stateful transducers to me... Can you show me an example?

@acdlite
Copy link
Collaborator

acdlite commented Jul 5, 2015

@rpominov Okay, I can see how that could work with dedupe(), but then there's still the bigger problem of the reducer no longer being pure.

@rpominov
Copy link

rpominov commented Jul 5, 2015

@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
http://www.youtube.com/watch?v=6mTbuzafcII

@rpominov
Copy link

rpominov commented Jul 5, 2015

Right, but I think they don't intend to be pure anyway,

Sorry, need to clarify, I meant transducers don't intend to be pure, while reducers in Redux certainly are.

@clearjs
Copy link
Contributor

clearjs commented Jul 5, 2015

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 dedupe(), there would be something like dedupe(state)().

@acdlite
Copy link
Collaborator

acdlite commented Jul 5, 2015

A key would be generated per each usage of such transducers

How is that pure?

@clearjs
Copy link
Contributor

clearjs commented Jul 5, 2015

Why not? UID generation is a pure operation. Did I misunderstand you?

@rpominov
Copy link

rpominov commented Jul 5, 2015

A key would be generated per each usage of such transducers

But where the state of dedupe will be stored? I also don't understand.

@clearjs
Copy link
Contributor

clearjs commented Jul 5, 2015

dedupe would be a function that returns a transducer which stores its state in the global app state under a unique key generated per a call to the dedupe function. Each invocation of this transducer should store a new state instance under the same key (i.e., transducer's state shouldn't be mutated).

@clearjs
Copy link
Contributor

clearjs commented Jul 5, 2015

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?

@clearjs
Copy link
Contributor

clearjs commented Jul 5, 2015

On the other hand, it's difficult for me to imagine a use case where such pseudo-stateful operations were necessary for a reducer.

@rpominov
Copy link

rpominov commented Jul 5, 2015

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).

@clearjs
Copy link
Contributor

clearjs commented Jul 5, 2015

@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.

@acdlite
Copy link
Collaborator

acdlite commented Jul 5, 2015

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 into(to, xform, from) to dispatch a collection of actions.

screen shot 2015-07-05 at 2 24 59 pm

https://github.com/acdlite/redux-transducers#example-mapping-strings-to-actions

@Chris-Andrews
Copy link

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.

@kurtharriger
Copy link

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.
These libraries already provide ways to compose event streams and and implement some common stateful transforms such as debounce and flatmapLatest.

@gaearon
Copy link
Contributor Author

gaearon commented Jul 31, 2015

Closing, as the issue has been dormant for a while.
redux-transducers is mentioned in the Ecosystem page of the new docs.

Please feel free to keep the discussion going.

@gaearon gaearon closed this as completed Jul 31, 2015
@mindjuice
Copy link
Contributor

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:

Why not? UID generation is a pure operation. Did I misunderstand you?

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.

@clearjs
Copy link
Contributor

clearjs commented Jul 31, 2015

@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.

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