A bit of functional programming advice needed

Say I have a series of calculations that each use the …spread operator to add a new property to an object

Note: Just an example the actual calculations don’t matter here

const calc1 = (obj) => ({
  ...obj,
  x: obj.a + obj.b
})

const calc2 = (obj) => ({
  ...obj,
  y: obj.x + obj.c
})

const calc3 = (obj) => ({
  ...obj,
  z: obj.x + obj.y
})

const total = (obj) => ({
  ...obj,
  total: obj.x + obj.y + obj.z
})

const calculations = [calc1, calc2, calc3]

I then create a calculation function using pipe

const calculate = pipe(...calculations)
const result = calculate({a: 1, b: 2, c: 3})

// resulting in something like this
{
  a: 1,
  b: 2,
  ...
  ...
  z: 9,
  total: 24
}

To give you a picture, I am using the above procedure on form inputs to generate a hash-map of form outputs.

It’s workable, but to refactor further what if I took out the spread operators from each function e.g.

const calc1 = (obj) => ({
  x: obj.a + obj.b
})

const calc2 = (obj) => ({
  y: obj.x + obj.c
})

const calc3 = (obj) => ({
  z: obj.x + obj.y
})

What if I then make a function that wraps each of those functions to do the merging for me.

const mergeWith = fn => obj => ({ ...obj, ...fn(obj) })

I then create the pipe function like so

const calculate = pipe(...calculations.map(mergeWith))

The reason I am asking this, is that I have done quite a bit of searching through the likes of ramda, underscore etc. and I can’t find anything like this mergeWith function. This makes me think my approach is unnecessarily complicating things or is flawed.

Your instincts serve you well.

Unclassed Objects in Javascript are mutable, and hold no definite form.

x = {};
x.newprop = 3;

perfectly valid.
Calc3 then, is just:

obj.z = obj.x + obj.y;

It does remind me quite a bit of the step or combine function of a transducer though… in this vein, maybe we could parameterize the actual merging as well in a generic transform function:

const { curry, map, applyTo, merge, pipe } = require('ramda')
const transform = curry((fn, combine, obj) => combine(obj, fn(obj)))

const calc1 = transform(obj => ({ x: obj.a + obj.b }))
const calc2 = transform(obj => ({ y: obj.x + obj.c }))
const calc3 = transform(obj => ({ z: obj.x + obj.y }))
const total = transform(obj => ({ total: obj.x + obj.y + obj.z }))

const calculations = map(applyTo(merge), [calc1, calc2, calc3, total])
const result = pipe(...calculations)({ a: 1, b: 2, c: 3 })

console.log(result)

Either way, such an approach seems fine to me… it’s just another layer of abstraction, which would also allow applying a destructive Object.assign(), for example.

Edit: Or maybe a bit more straightforward w/o additional mapping etc. – just introduce a custom pipe function:

const { merge, reduce } = require('ramda')

const transformPipe = (...fns) => (combine, value) =>
  reduce((acc, fn) => combine(acc, fn(acc)), value, fns)

const result = transformPipe(
  obj => ({ x: obj.a + obj.b }),
  obj => ({ y: obj.x + obj.c }),
  obj => ({ z: obj.x + obj.y }),
  obj => ({ total: obj.x + obj.y + obj.z })
)(merge, { a: 1, b: 2, c: 3 })

console.log(result)

Note that in FP however, mutation is generally considered harmful. :-)

1 Like

Wouldn’t it also hate the idea of a function being declared with the assumption of the existance of properties of an object?, rather than say…

assignProp(obj,'x',obj.a,obj.b)

…with something like (I’m going to do this longhand because I clearly don’t know FP shortcuts very well)

function assignProp(obj,name,a,b) {
 let out = {...obj};
 out[name] = a + b;
 return out;
}

Or maybe i’m off base?

1 Like

Very interesting m3g4p0p, I need to look into ‘transducers’ and your code example there. I have heard the term, but it is a new one to me. Thanks.

I did got down that route initially, with a less declarative approach

const pipeAndMerge = (...fns) => 
  (value) => 
    fns.reduce((acc, fn) => ({
      ...acc,
      ...fn(acc)
    })
    , value
  )

However with still a limited knowledge of functional JS, I thought it was possibly a dodgy path to go down — in other words best to work with the established tools.

1 Like

@m_hutley yeah well but this is not a problem specific to FP and can be solved using default parameters…

function assignProp (obj, name, a = 0, b = 0) {
  // ...
}

A more interesting case would be if you specified the values to add by keys of the input object, but here Ramda et al. would indeed have tools to handle missing values:

const assignProp = (x, y, z, obj) => {
  const out = { ...obj }
  out[z] = R.propOr(0, x, obj) + R.propOr(0, y, obj)
  return out
}

Or even more point free:

const assignProp = (x, y, z, obj) => R.set(
  R.lensProp(z),
  R.add(
    R.propOr(0, x, obj),
    R.propOr(0, y, obj)
  ),
  obj
)

But oh well, this does not necessarily serve readability LOL.

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.