Using Currying to Compose Reducers

I have learned about currying not too long ago, but haven't been able to see uses for it in my work. Functional Programming, especially techniques such as currying and partial application, can be a bit unintuitive, and thus can take time before it sinks in your brain and comes natural.

The usecase I am writing about this time is not a brilliant answer for a rare and complex problem, but instead a good answer for a day-to-day problem, commonly encountered when writing reducers (specifically for Flux/Redux). This answer is powerful but requires, in my opinion, unintuitive tools, and for that reason I hope this post can be of value to some.

The problem I am talking about is combining redcuers. In this post I will be using Redux but the concepts here apply in different environments as well.

A Reducer in Redux is a pure function that accepts a state and an action, and returns a new, transformed, state.

For example I may have the following reducers that handle actions in a modal with an alternating view:

function changeViewReducer(state, action) {
  const { viewName, index, count } = action.result;

  return Object.assign({}, state, {
    index,
    count,
    currentView: viewName,
  });
}

function startFlowReducer(state, action) {
  const { viewName, index, count } = action.result;

  return Object.assign({}, state, {
    index,
    count,
    currentView: viewName,
    isModalOn: true,
  });
}

changeViewReducer would handle an action of view change inside the modal, assigning index, count (used for a widget that shows the user which view is he viewing and how many are ahead) and currentView. startFlowReducer would handle an action that starts the flow, which means setting the same values as the former reducer, but in addition he has to set isModalOn: as true.

This is a small case of code repetition, but it is not a far fetched scenario that with many more reducers, our code can get out of hand. Maintanence becomes hard when action data needs to be handled differently, or state properties get renamed or moved. In order to avoid the need to track a property in all of the reducers it is used and update it accordingly, some sort of code reuse is desired.

If we break down our reducers to smaller buildings blocks, we recognize two roles that are fulfilled:

  1. Assigning { index, count, currentView: action.viewName } to the state - which is what changeViewReducer does.
  2. Assigning { isModalOn: true } to the state - which is what startFlowReducer does in addition to what the former does. Let's call it showModalReducer.

In a former post I wrote I demonstrate how by piping together small, pure, "building block" functions, we can compose functions for our complex operations. A good solution for our problem can be to compose startFlowReducer from the two "building blocks" I have outlined above.

By composing startFlowReducer I mean that the result of startFlowReducer would be the result of the state after going through two reducers. We can use a pipe function to do that:

// Accept a number functions, and return a function that pipes the results through the accepted functions
// For example pipe(f, g, h) returns x => f(g(h(x)))
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x)

When trying to apply the above pipe function to our reducers we run into a problem. A function only returns one value, but our reducers require two arguments, state and action. If we were to pipe our reducers using this pipe function, the second reducer would be called with undefined as the action (which might work in this case since showModalReducer does not use the action result, but it won't work for other reducers that do).

In order to solve that, we have to change the interface of our function to fit into the pipe. While the state param changes in between reducer calls, our action param does not. We can take advantage of that, by currying the reducers. Eric Elliott gives a very simple definition of currying:

Curry: A function that takes a function with multiple parameters as input and returns a function with exactly one parameter.

In our case, this is what we're trying to do:

const curryReducer = (fn, action) => state => fn(state, action)

The curried reducers accept the action in advanced, before called. For example our curried changeViewReducer would essentially look something like this:

function curriedChangeViewReducer(action) {
  return state => changeViewReducer(state, action) // The returned function only needs `state` as a param
}

function curriedShowModalReducer(action) {
  return state => showModalReducer(state, action)
}

The results are functions that accept state, and call our reducer with the action we provided at the moment of currying. We can pipe them.

const startFlowReducer = (state, action) => {
  const curriedChangeViewReducer = curryReducer(changeViewReducer, action)
  const curriedShowModalReducer = curryReducer(showModalReducer, action)

  return pipe(curriedChangeViewReducer, curriedShowModalReducer)
}

The operation of currying reducers then piping them can be written into a function:

const pipeReducers = (...reducers) =>
  (state, action) =>
    pipe(
      ...reducers.map(fn => curryReducer(fn, action))
    )(state);

const startFlowReducer(changeViewReducer, showModalReducer)

Breaking this down, pipeReducers is a function that accepts a number of reducer functions, and returns one combined reducer function. The combined reducer function runs pipe on all of the reducers after they have been curried so they can be piped. Calling that pipe function with the state would yield us our desired result.