Ramda: How can I make this imperative reducer more declarative?

Viewed 274

I have the following reducer function:

The first argument to the reducers is the aggregated value, and the second argument is the next value. The below reducer function is reducing over the same reaction argument but aggregating the state$ value. Each reducer function yields a new aggregated value.

/**
 * Applies all the reducers to create a state object.
 */
function reactionReducer(reaction: ReactionObject): ReactionObject {
    let state$ = reactionDescriptionReducer({}, reaction);
    state$ = reactionDisabledReducer(state$, reaction);
    state$ = reactionIconReducer(state$, reaction);
    state$ = reactionOrderReducer(state$, reaction);
    state$ = reactionStyleReducer(state$, reaction);
    state$ = reactionTitleReducer(state$, reaction);
    state$ = reactionTooltipReducer(state$, reaction);
    state$ = reactionVisibleReducer(state$, reaction);
    return state$;
}

const state = reactionReducer(value);

The above works but the function is fixed with the list of reducers. It seems like I should be able to do something like this with RamdaJS.

const state = R.????({}, value, [reactionDescriptionReducer
    reactionDisabledReducer,
    reactionIconReducer,
    reactionOrderReducer,
    reactionStyleReducer,
    reactionTitleReducer,
    reactionTooltipReducer,
    reactionVisibleReducer]);

I am new to RamdaJS so forgive me if this is a noob question.

How can I execute a chain of reducers using just RamdaJS?

3 Answers

and constructs a new reducer, (r, x) => ..., by combining the two (2) input reducers, f and g -

const and = (f, g) =>
  (r, x) => g (f (r, x), x)

all, by use of and, constructs a new reducer by combining an arbitrary number of reducers -

const identity = x =>
  x

const all = (f = identity, ...more) =>
  more .reduce (and, f)

Define myReducer using all -

const myReducer =
  all
    ( reactionDisabledReducer
    , reactionIconReducer
    , reactionOrderReducer
    // ...
    )

Given a mocked implementation for these three (3) reducers -

const reactionDisabledReducer = (s, x) =>
  x < 0
    ? { ...s, disabled: true }
    : s

const reactionIconReducer = (s, x) =>
  ({ ...s, icon: `${x}.png` })

const reactionOrderReducer = (s, x) =>
  x > 10
    ? { ...s, error: "over 10" }
    : s

Run myReducer to see the outputs

const initState =
  { foo: "bar" }

myReducer (initState, 10)
// { foo: 'bar', icon: '10.png' }

myReducer (initState, -1)
// { foo: 'bar', disabled: true, icon: '-1.png' }

myReducer (initState, 100)
// { foo: 'bar', icon: '100.png', error: 'over 10' }

Expand the snippet below to verify the results in your browser -

const identity = x =>
  x

const and = (f, g) =>
  (r, x) => g (f (r, x), x)

const all = (f, ...more) =>
  more .reduce (and, f)

const reactionDisabledReducer = (s, x) =>
  x < 0
    ? { ...s, disabled: true }
    : s

const reactionIconReducer = (s, x) =>
  ({ ...s, icon: `${x}.png` })

const reactionOrderReducer = (s, x) =>
  x > 10
    ? { ...s, error: "over 10" }
    : s

const myReducer =
  all
    ( reactionDisabledReducer
    , reactionIconReducer
    , reactionOrderReducer
    // ...
    )

const initState =
  { foo: "bar" }

console .log (myReducer (initState, 10))
// { foo: 'bar', icon: '10.png' }

console .log (myReducer (initState, -1))
// { foo: 'bar', disabled: true, icon: '-1.png' }

console .log (myReducer (initState, 100))
// { foo: 'bar', icon: '100.png', error: 'over 10' }


You can choose whatever names you like for and and all. I could see them as part of a reducer module, like reducer.and and reducer.all

My first attempt would not involve Ramda at all, just a simple:

const makeReducer = (...fns) => (x) => fns .reduce ( (s, fn) => fn (s, x), {} )

const fn = makeReducer (
  (state$, reaction) => ({...state$, foo: `<<-${reaction.foo}->>`}),
  (state$, reaction) => ({...state$, bar: `=*=${reaction.bar}=*=`}),
  (state$, reaction) => ({...state$, baz: `-=-${reaction.baz}-=-`})
)

console .log (
  fn ( {foo: 'a', bar: 'b', baz: 'c'} )
) //~> {foo: '<<-a->>', bar: '=*=b=*=', baz: '-=-c-=-'}

While you could choose to use Ramda's reduce and flip, it doesn't seem as though they'll add much here.

One option of utilising Ramda here would be to make use of the fact that it supports passing functions as a monad instance to R.chain (otherwise known as the Reader monad).

This lets you sequence a number of functions together that share some common environment - in your case, reaction.

We can make use of R.pipeWith(R.chain) to allow composing a series of these functions that take some input (e.g. your $state threading through each function) and returns a function that takes the environment, producing a result to pass on to the next function in the pipeline.

// Some mock functions to demonstrate

const reactionDescriptionReducer = ({...state}, reaction) =>
  ({ description: reaction, ...state })

const reactionDisabledReducer = ({...state}, reaction) =>
  ({ disabled: reaction, ...state })

const reactionIconReducer = ({...state}, reaction) =>
  ({ icon: reaction, ...state })

// effectively `R.pipeK`
const kleisli = R.pipeWith(R.chain)

// we need the functions going into chain to be curried
const curried = f => a => b => f(a, b)

// finally, compose the series of functions together
const reactReducer = kleisli([
  curried(reactionDescriptionReducer),
  curried(reactionDisabledReducer),
  curried(reactionIconReducer)
])({})

// and if all goes well...
console.log(
  reactReducer("someCommonReactionValue")
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>

Related