4

I'm trying to write code using Ramda to produce a new data structure, using only the id and comment keys of the original objects. I'm new to Ramda and it's giving me some fits, although I have experience with what I think is similar coding with Python.

Given the following initial data structure…

const commentData = {
  '30': {'id': 6, 'comment': 'fubar', 'other': 7},
  '34': {'id': 8, 'comment': 'snafu', 'other': 6},
  '37': {'id': 9, 'comment': 'tarfu', 'other': 42}
};

I want to transform it into this…

{
  '6': 'fubar',
  '8': 'snafu',
  '9': 'tarfu'
}

I found the following example in the Ramda cookbook that comes close…

const objFromListWith = R.curry((fn, list) => R.chain(R.zipObj, R.map(fn))(list));
objFromListWith(R.prop('id'), R.values(commentData));

But values it returns includes the whole original object as the values…

{
  6: {id: 6, comment: "fubar", other: 7},
  8: {id: 8, comment: "snafu", other: 6},
  9: {id: 9, comment: "tarfu", other: 42}
}

How can I reduce the values down to the value of their comment key only?

I don't need to use the code I got from the cookbook. If anyone can suggest some code that will give the results I'm looking for that's also better (simpler, shorter, or more efficient) than the example here, I'll be happy to use that instead.

Calvin Nunes
  • 6,376
  • 4
  • 20
  • 48
Mr. Lance E Sloan
  • 3,297
  • 5
  • 35
  • 50

2 Answers2

4

If you don't mind, you don't need to use Ramda for that, pure JS can handle it nicely:

You can use a combination of Object.values(), to get all values of your first object (commentData) and .forEach() (or even .map(), but slower), in the array that results from Object.values to insert values into a new object dynamically.

const commentData = {
  '30': {'id': 6, 'comment': 'fubar', 'other': 7},
  '34': {'id': 8, 'comment': 'snafu', 'other': 6},
  '37': {'id': 9, 'comment': 'tarfu', 'other': 42}
};

let values = Object.values(commentData)
let finalObj = {};

values.forEach(x => finalObj[x.id] = x.comment)

console.log(finalObj)

But, if you want an one-liner, you can go with Object.fromEntries() after returning arrays of key/value from .map() based on id and comment, like below:

const commentData = {
  '30': {'id': 6, 'comment': 'fubar', 'other': 7},
  '34': {'id': 8, 'comment': 'snafu', 'other': 6},
  '37': {'id': 9, 'comment': 'tarfu', 'other': 42}
};

console.log(Object.fromEntries(Object.values(commentData).map(x => [x.id, x.comment])))
Calvin Nunes
  • 6,376
  • 4
  • 20
  • 48
  • I tried a non-Ramda approach earlier, but I wasn't quite getting there. I'm using this in a Vue.js app written by someone else. I thought the use of Ramda might have been important because it returns a promise back to the caller rather than the actual value and that's what Vue wanted. Now I've debugged the app a bit more and I see I'm running into Vue-related problems. I'm sure if I get that resolved, I'll be able to use the data transformed this way. If you know a solution using Ramda, I'm still interested in seeing how it's done. – Mr. Lance E Sloan Oct 08 '19 at 19:00
  • I really have no knowledge on Ramda neither Vue, sorry. – Calvin Nunes Oct 08 '19 at 19:03
  • What kind of side-effects would result from using `map` here? – Mr. Lance E Sloan Oct 09 '19 at 11:10
  • It's more about performance, yesterday I did a test performance in this case between `forEach` and `map`, map was about 42% slower than forEach... Probably because `.map` returns a new array and executes the function while `.forEach` just executes the function without return – Calvin Nunes Oct 09 '19 at 11:36
  • You also could do that with the simple `for` loop using `Object.values()` array, but then a one-liner solution won't be possible – Calvin Nunes Oct 09 '19 at 11:39
  • @LS: That comment was left before the `map` that was there was replaced with the `forEach` shown above. I always prefer to `map`/`find`/`filter` or, if necessary, `reduce` when I can. But they have meaningful semantics, and to use them only to modify an external variable feels quite dirty. (I've removed the comment that prompted all this, since the issue is fixed.) – Scott Sauyet Oct 09 '19 at 12:38
  • I appreciate your first comment (that is deleted now) because it also helped me to go look for the differences and why `.map` is an overuse in this case, I'm just a junior dev, so is good to know when improvements can be made. But I don't call the usage of .map as an "issue", just a bad choice... :D – Calvin Nunes Oct 09 '19 at 12:43
  • Would the change from `map` to `forEach` in the first example also apply to the one-liner in the second example, which still uses `map`? I prefer the single line approach and I wondered whether that would suffer those side-effects. – Mr. Lance E Sloan Oct 09 '19 at 14:12
  • No, for the second example it won't work with `forEach` because it doesn't return anything (undefined, to be more precise), only `map` returns. If you really want a one liner not using .map, then go with below answer that uses `.reduce` – Calvin Nunes Oct 09 '19 at 14:15
  • @LS: It's not a matter of "suffering from side-effects". Rather `map` is designed to convert one array to another by applying a common function to each value; it should do no more than that. Choosing to update a separate data structure instead of returning a value in that function is a misuse, called a side-effect here because it changes something outside of the function's scope. `forEach` has no other purpose than side-effects, and I would avoid it as much as possible, but at least it matches the operation supplied here. – Scott Sauyet Oct 10 '19 at 01:45
  • @CalvinNunes: I'm glad it helped. I personally wouldn't spend too much time thinking about performance until I've tested and found that my code is too slow, then located and fixed the worst hot-spots. – Scott Sauyet Oct 10 '19 at 01:49
4

A Ramda one-liner would be

const foo = compose(fromPairs, map(props(['id', 'comment'])), values)

const commentData = {
  '30': {'id': 6, 'comment': 'fubar', 'other': 7},
  '34': {'id': 8, 'comment': 'snafu', 'other': 6},
  '37': {'id': 9, 'comment': 'tarfu', 'other': 42}
}

console.log(
  foo(commentData)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {compose, fromPairs, map, props, values} = R           </script>

But that doesn't seem a lot cleaner than (a slight tweak to) the suggestion in epascarello's comment:

const foo = (obj) => 
  Object.values(obj).reduce((o, v) => (o[v.id] = v.comment) && o, {})

or a similar version I would write, if it didn't cause any performance issues:

const foo = (obj) => 
  Object.values(obj).reduce((o, {id, comment}) => ({...o, [id]: comment}), {})
Mr. Lance E Sloan
  • 3,297
  • 5
  • 35
  • 50
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • You mentioned in a comment to another solution that using `map` in that case would have side-effects. What's different about this case that you're comfortable using `map`? Also, what performance issues could arise from the last form of the solution you gave? – Mr. Lance E Sloan Oct 09 '19 at 11:20
  • @LS: I responded there. At the time I wrote that note, that code looked like `values.map(x => finalObj[x.id] = x.comment)`, which strikes me as an abuse of `map`. The possible performance issue is [described by Rich Snapp](https://www.richsnapp.com/blog/2019/06-09-reduce-spread-anti-pattern). I choose to use this pattern for its expressivity so long as performance tests don't show it causing issues. But there's an argument for avoiding it altogether. – Scott Sauyet Oct 09 '19 at 12:45