0

How can I create a pure function that updates an object that's been initialized in another function something like:

parentFunction = (inputs: object[], condtionList: string[]) => {

  const newObject = {f1: val1[], f2: val2[], f3: val3[]...}
  inputs.forEach(input => {
    if(condition1){
      updateNewObject(condtion1, newObject, input, conditionList)
    }
    .
    .
    . 
  }
  return newObject
}

The below function is impure as it's updating the newObject (mutating the input) how can I convert it to a pure function?

updateNewObject(condition, newObject, input, conditionList) {
  const i = conditionList.indexOf(input.condition)
  if(i === 0){
    newObject.f1.push(input)
  }
  else if(i === 1) {
    newObject.f2.push(input)
  }
  .
  .
  .
}

The above has no return value. It takes the newObject as input and based on some conditionals pushes values to the properties of the newObject. Is there anyway to make the above function pure? or do I have to rethink how I am updating newObject?

Pointy
  • 405,095
  • 59
  • 585
  • 614
Haq.H
  • 863
  • 7
  • 20
  • 47
  • JS `Object`/`Array` types don't depend on mutability by design! Just don't mutate the input. If you are concerned about efficiency, use immutable data types. –  Apr 06 '20 at 18:29
  • Are you saying you cannot change `parentFunction`? – Bergi Apr 06 '20 at 18:50

4 Answers4

2

Functional programming is not only about purity, it's also about reusability and separation of concerns. It's difficult to write a big complex function, and even harder to test and maintain it. Following functional principles will help us avoid pain and discomfort.

Let's start by isolating the behaviours we care about. We identify functions push, update, and pushKey -

const identity = x =>
  x

const push = (a = [], value) =>
  a.concat([ value ])

const update = (o = {}, key = "", t = identity) =>
  ({ ...o, [key]: t(o[key]) })

const pushKey = (o = {}, key = "", value) =>
  update(o, key, a => push(a, value))

This allows you to perform basic immutable transformations easily -

const d1 = { a: [1], b: [] }
const d2 = pushKey(d1, "a", 2)
const d3 = pushKey(d2, "b", 3)
const d4 = pushKey(d3, "c", 4)

console.log(d1) // { a: [1], b: [] }
console.log(d2) // { a: [1, 2], b: [] }
console.log(d3) // { a: [1, 2], b: [3] }
console.log(d4) // { a: [1, 2], b: [3], c: [4] }

Expand the snippet below to run the program in your own browser -

const identity = x =>
  x

const push = (a = [], value) =>
  a.concat([ value ])

const update = (o = {}, key = "", t = identity) =>
  ({ ...o, [key]: t(o[key]) })

const pushKey = (o = {}, key = "", value) =>
  update(o, key, a => push(a, value))

const d1 = { a: [1], b: [] }
const d2 = pushKey(d1, "a", 2)
const d3 = pushKey(d2, "b", 3)
const d4 = pushKey(d3, "c", 4)

console.log(JSON.stringify(d1)) // { a: [1], b: [] }
console.log(JSON.stringify(d2)) // { a: [1, 2], b: [] }
console.log(JSON.stringify(d3)) // { a: [1, 2], b: [3] }
console.log(JSON.stringify(d4)) // { a: [1, 2], b: [3], c: [4] }

This allows you to separate your complex conditional logic into its own function -

const updateByCondition = (o = {}, conditions = [], ...) =>
{ if (...)
    return pushKey(o, "foo", someValue)
  else if (...)
    return pushKey(o, "bar", someValue) 
  else
    return pushKey(o, "default", someValue) 
}

The advantages to this approach are numerous. push, update, and pushKey are all very easy to write, test, and maintain, and they're easy to reuse in other parts of our program. Writing updateByCondition was much easier because we had better basic building blocks. It's still difficult to test due to whatever complexity you are trying to encode, however it is much easier to maintain due to separation of concerns.

Mulan
  • 129,518
  • 31
  • 228
  • 259
1

Classically, you'd create a new object with a new array with the new entry in it:

if(i === 0){
  //          v−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−v−− creates new object
  newObject = {...newObject, f1: [...newObject.f1, input]}
  //          ^                  ^−−−−−−−−−−−−−−−−−−−−−−^−−− creates new array
}
else if(i === 1) {
  //          v−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−v−− creates new object
  newObject = {...newObject, f2: [...newObject.f2, input]}
  //          ^                  ^−−−−−−−−−−−−−−−−−−−−−−^−−− creates new array
}

Then in parentFunction:

    newObject = updateNewObject(condtion1, newObject, input, conditionList)
//  ^^^^^^^^^^^^−−−−−−−−−−−−−−−−−−−−−− updates the value being returned

Or the update could be:

const name = i === 0 ? "f1" : (i === 1 ? "f2" : ""));
if (name) {
  newObject = {...newObject, [name]: [...newObject[name], input]}
}

...though the nested conditional is a bit meh. :-)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
1

If you want updateNewObject to be pure, have it create a new object that clones the original, mutate that, and then return the new object.

updateNewObject(condition, oldObject, input, conditionList) {
  const newObject = {...oldObject};
  const i = conditionList.indexOf(input.condition)
  if(i === 0){
    newObject.f1 = [...newObject.f1, input];
  }
  else if(i === 1) {
    newObject.f2 = [...newObject.f2, input];
  }
  .
  .
  .

  return newObject;
}

Note how newObject.f1 = [...newObject.f1, input]; creates a new array - this ensures that we not only don't mutate the object directly, but we don't mutate any of its fields (arrays) and instead create new ones.

Then tweak parentFunction so that it uses the value of each returned updateNewObject call:

parentFunction = (inputs: object[], condtionList: string[]) => {

  let newObject = {f1: val1[], f2: val2[], f3: val3[]...}
  inputs.forEach(input => {
    if(condition1){
      newObject = updateNewObject(condtion1, newObject, input, conditionList)
    }
    .
    .
    . 
  }
  return newObject
}
jered
  • 11,220
  • 2
  • 23
  • 34
-1

Just copy/map the array to get a new one. Don't mutate the same one.

Zazaeil
  • 3,900
  • 2
  • 14
  • 31