Jeyabalan Thavamani
Jeyabalan Thavamani

Reputation: 3327

Modify Array of Object using ramda

I have an array of object like below ,

[
{      
  "myValues": []
},
{
 "myValues": [],
 "values": [
    {"a": "x", "b": "1"},
    {"a": "y", "b": "2"}
  ],
  "availableValues": [],      
  "selectedValues": []
}
]

also if i iterate the object, the "values" key present in the object, it will convert it into like below,

[
{      
  "myValues": []
},
{
  "myValues": [],
  "values": [
    {"a": "x", "b": "1"},
    {"a": "y", "b": "2"}
  ],
  "availableValues": [
    {"a": "x", "b": "1"},
    {"a": "y", "b": "2"}
  ],      
  "selectedValues": ["x", "y"]
}
]

I tried with ramda functional programming but no result,

let findValues = x => {
  if(R.has('values')(x)){
  let availableValues = R.prop('values')(x);
  let selectedValues = R.pluck('a')(availableValues);
  R.map(R.evolve({
    availableValues: availableValues,
    selectedValues: selectedValues
  }))
 }
}
R.map(findValues, arrObj);

Upvotes: 1

Views: 3970

Answers (2)

Scott Sauyet
Scott Sauyet

Reputation: 50787

Immutability

First of all, you need to be careful with your verbs. Ramda will not modify your data structure. Immutability is central to Ramda, and to functional programming in general. If you're actually looking to mutate your data, Ramda will not be your toolkit. However, Ramda will transform it, and it can do so in a relatively memory-friendly way, reusing those parts of your data structure not themselves transformed.

Fixing your approach

Let's first look at cleaning up several problems in your function:

let findValues = x => {
  if (R.has('values')(x)) {
    let availableValues = R.prop('values')(x);
    let selectedValues = R.pluck('a')(availableValues);
    R.map(R.evolve({
      availableValues: availableValues,
      selectedValues: selectedValues
    }))
  }
}

The first, most obvious issue is that this function does not return anything. As mentioned above, Ramda does not mutate your data. So a function that does not return something is useless when supplied to Ramda's map. We can fix this by returning the result of the map(evolve) call and returning the original value when the if condition fails:

let findValues = x => {
  if (R.has('values')(x)) {
    let availableValues = R.prop('values')(x);
    let selectedValues = R.pluck('a')(availableValues);
    return R.map(R.evolve({
      availableValues: availableValues,
      selectedValues: selectedValues
    }))
  }
  return x;
}

Next, the map call makes no sense. evolve already iterates the properties of the object. So we can remove that. But we also need to apply evolve to your input value:

let findValues = x => {
  if (R.has('values')(x)){
    let availableValues = R.prop('values')(x);
    let selectedValues = R.pluck('a')(availableValues);
    return R.evolve({
      availableValues: availableValues,
      selectedValues: selectedValues
    }, x)
  }
  return x;
}

There's one more problem. Evolve expects an object containing functions that transform values. For instance,

evolve({
  a: n => n * 10,
  b: n => n + 5
})({a: 1, b: 2, c: 3}) //=> {a: 10, b: 7, c: 3}

Note that the values of the properties in the object supplied to evolve are functions. This code is supplying values. We can wrap those values with R.always, via availableValues: R.always(availableValues), but I think it simpler to use a lambda with a parameter of "_", which signifies that the parameter is unimportant: availableValues: _ => availableValues. You could also write availableValues: () => available values, but the underscore demonstrates the fact that the usual value is ignored.

Fixing this gets a function that works:

let findValues = x => {
  if (R.has('values')(x)){
    let availableValues = R.prop('values')(x);
    let selectedValues = R.pluck('a')(availableValues);
    return R.evolve({
      availableValues: _ => availableValues,
      selectedValues: _ => selectedValues
    }, x)
  }
  return x;
}

let arrObj = [{myValues: []}, {availableValues: [], myValues: [], selectedValues: [], values: [{a: "x", b: "1"}, {a: "y", b: "2"}]}];

console.log(R.map(findValues, arrObj))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>

If you run this snippet here, you will also see an interesting point, that the resulting availableValues property is a reference to the original values one, not a separate copy of it. This helps show that Ramda is reusing what it can of your original data. You can also see this by noting that R.map(findValues, arrObj)[0] === arrObj[0] //=> true.

Other Approaches

I wrote my own versions of this, not starting from yours. Here is one working function:

const findValues = when(
  has('values'),
  x => evolve({
    availableValues: _ => prop('values', x),
    selectedValues: _ => pluck('a', prop('values', x))
  }, x)
)

Note the use of when, which captures the notion of "when this predicate is true for your value, use the following transformation; otherwise use your original value." We pass the predicate has('values'), essentially the same as above. Our transformation is similar to yours, using evolve, but it skips the temporary variables, at the minor cost of repeating the prop call.

Something still bothers me about this version. Using those lambdas with "_" is really a misuse of evolve. I think this is cleaner:

const findValues = when(
  has('values'),
  x => merge(x, {
    availableValues: prop('values', x),
    selectedValues: pluck('a', prop('values', x))
  })
)

Here we use merge, which is a pure function version of Object.assign, adding the parameters of the second object to those of the first, replacing when they conflict.

This is what I would choose.

Anther solution

Since I started writing this answer, Scott Christopher posted another one, essentially

const findValues = R.map(R.when(R.has('values'), R.applySpec({
  myValues:        R.prop('myValues'),
  values:          R.prop('values'),
  availableValues: R.prop('values'),
  selectedValues:  R.o(R.pluck('a'), R.prop('values'))
})))

This is a great solution. It also is nicely point-free. If your input objects are fixed to those properties, then I would choose this over my merge version. If your actual objects contain many more properties, or if there are dynamic properties that should be included in the output only if they are in the input, then I would choose my merge solution. The point is that Scott's solution is cleaner for this specific structure, but mine scales better to more complex objects.

Step-by-step

This is not a terribly complex transformation, but it is non-trivial. One simple way to build something like this is to build it up in very minor stages. I often do this in the Ramda REPL so I can see the results as I go.

My process looked something like this:

Step 1

Make sure I properly transform only the correct values:

const findValues = when(
  has('values'), 
  always('foo')
)
map(findValues, arrObj) //=> [{myValues: []}, "foo"]

Step 2

Make sure my transformer function is given the proper values.

const findValues = when(
  has('values'), 
  keys
)
map(findValues, arrObj) 
//=> [{myValues: []}, ["myValues", "values", "availableValues", "selectedValues"]]

Here identity would work just as well as keys. Anything that demonstrates that the right object is passed would be fine.

Step 3

Test that merge will work properly here.

const findValues = when(
  has('values'), 
  x => merge(x, {foo: 'bar'})
)
map(findValues, arrObj) 
//=> [
//     {myValues: []}, 
//     {
//       myValues: [], 
//       values: [{a: "x", b: "1"}, {a: "y", b: "2"}],
//       availableValues: [], 
//       selectedValues: [], 
//       foo: "bar"
//     }
//   ]

So I can merge two objects properly. Note that this keeps all the existing properties of my original object.

Step 4

Now, actually transform the first value I want:

const findValues = when(
  has('values'),
  x => merge(x, {
    availableValues: prop('values', x)
  })
)
map(findValues, arrObj) 
//=> [
//     {myValues: []}, 
//     {
//       myValues: [], 
//       values: [{a: "x", b: "1"}, {a: "y", b: "2"}],
//       availableValues: [{a: "x", b: "1"}, {a: "y", b: "2"}],
//       selectedValues: []
//     }
//   ]

Step 5

This does what I want. So now add the other property:

const findValues = when(
  has('values'),
  x => merge(x, {
    availableValues: prop('values', x),
    selectedValues: pluck('a', prop('values', x))
  })
)
map(findValues, arrObj) 
//=> [
//     {myValues: []}, 
//     {
//       myValues: [], 
//       values: [{a: "x", b: "1"}, a: "y", b: "2"}],
//       availableValues: [{a: "x", b" "1"}, {a: "y", b: "2"}], 
//       selectedValues: ["x", "y"]
//     }
//   ]

And this finally gives the result I want.

This is a quick process. I can run and test very quickly in the REPL, iterating my solution until I get something that does what I want.

Upvotes: 6

Scott Christopher
Scott Christopher

Reputation: 6516

R.evolve allows you to update the values of an object by passing them individually to their corresponding function in the supplied object, though it doesn't allow you to reference the values of other properties.

However, R.applySpec similarly takes an object of functions and returns a new function that will pass its arguments to each of the functions supplied in the object to create a new object. This will allow you to reference other properties of your surrounding object.

We can also make use of R.when to apply some transformation only when it satisfies a given predicate.

const input = [
  {      
    "myValues": []
  },
  {
    "myValues": [],
    "values": [
      {"a": "x", "b": "1"},
      {"a": "y", "b": "2"}
    ],
    "availableValues": [],      
    "selectedValues": []
  }
]

const fn = R.map(R.when(R.has('values'), R.applySpec({
  myValues:        R.prop('myValues'),
  values:          R.prop('values'),
  availableValues: R.prop('values'),
  selectedValues:  R.o(R.pluck('a'), R.prop('values'))
})))

const expected = [
  {      
    "myValues": []
  },
  {
    "myValues": [],
    "values": [
      {"a": "x", "b": "1"},
      {"a": "y", "b": "2"}
    ],
    "availableValues": [
      {"a": "x", "b": "1"},
      {"a": "y", "b": "2"}
    ],      
    "selectedValues": ["x", "y"]
  }
]

console.log(R.equals(fn(input), expected))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

Upvotes: 2

Related Questions