3
\$\begingroup\$

Recently I asked on SO about using point free methods to rearrange some data

The idea was to turn data in this format:

const data = [
  {
    timeline_map: {
      "2017-05-06": 770,
      "2017-05-07": 760,
      "2017-05-08": 1250,
    }
  }, {
    timeline_map: {
      "2017-05-06": 590,
      "2017-05-07": 210,
      "2017-05-08": 300,
    }
  }, {
    timeline_map: {
      "2017-05-06": 890,
      "2017-05-07": 2200,
      "2017-05-08": 1032,
    }
  }
]

Into this:

const hope = [
  ["2017-05-06", 770, 590, 890],
  ["2017-05-07", 760, 210, 2200],
  ["2017-05-08", 1250, 300, 1032],
]

The answers I got seemed kind of verbose in my opinion however (though I've no idea if they are better or worse than mine I haven't checked performance yet) so I spent more time studying Ramda and have come up with a solution of my own I like a little better. However I've only been doing this a little over a week so I'm sure it can be improved.

My code:

const datesValuesReducer = (accum, curr) => {
  if (accum.hasOwnProperty(curr[0])) {
    accum[curr[0]] = accum[curr[0]].concat(curr[1])
  } else {
    accum[curr[0]] = [curr[0], curr[1]]
  }
  return accum
}

const res = R.pipe(
  R.pluck('timeline_map'),
  R.map(R.toPairs),
  R.flatten,
  R.splitEvery(2),
  R.reduce(datesValuesReducer, {}),
  R.values
)

console.log(res(data))

Three points of possible concern:

1) Omitting R.flatten and R.splitEvery(2) get pretty close to what Is being output directly from R.map(R.toPairs) so maybe there is a way to deal with that data more directly and those methods can be omitted.

2) datesValuesReducer is pretty complex, maybe it could be simplified. I'm not sure if having the accum and curr means this solution isn't entirely "point free". Thoughts?

3) Also, I use pipe in favor over compose; I just find it reads more naturally. Maybe someone has some opinions about that.

JSBIN

\$\endgroup\$
4
  • \$\begingroup\$ I can see why you might prefer using a library like rambda or underscore to do this, but this would be relatively simple algorithm without the use of libraries. If you'd allow me, I'll write one as an answer. \$\endgroup\$
    – Neil
    Jul 27, 2017 at 7:09
  • \$\begingroup\$ @Neil this is more of an exercise in ramda. The original question I asked a while back was provided a good solution using reduce and forEach that was much faster than this, though I thought a little difficult to unpack mentally. I'd be eager to see your solution if you'd like to take a stab, but that probably belongs on the original question more than it does here. Cheers. \$\endgroup\$
    – 1252748
    Jul 27, 2017 at 14:11
  • \$\begingroup\$ ramda requires 4 or 5 lines to do many things "vanilla" js does in a single line. i just spent 30 minutes reading about it after seeing this question and i cannot for the life of me understand why anyone would use that library. \$\endgroup\$ Jul 27, 2017 at 14:44
  • \$\begingroup\$ @Iwrestledabearonce. I think it is very descriptive, or at least that's meant to be the idea. It allows you to manipulate data without writing heaps of ad hoc loops. You just compose its functions in a particular order your data is manipulated by a very human-readable series of commands. At least that's what I'm liking about it so far. \$\endgroup\$
    – 1252748
    Jul 27, 2017 at 15:33

1 Answer 1

1
\$\begingroup\$

I like your pipeline approach in general!

1) Omitting R.flatten and R.splitEvery(2) get pretty close to what Is being output directly from R.map(R.toPairs) so maybe there is a way to deal with that data more directly and those methods can be omitted.

Yup, you want something like R.chain here.

R.chain(R.toPairs)

is equivalent here to

R.pipe(
  R.map(R.toPairs),
  R.flatten,
  R.splitEvery(2)
)

effectively performing a non-recursive flatten on the output of the chained function. Clojure calls this mapcat.


2) datesValuesReducer is pretty complex, maybe it could be simplified. I'm not sure if having the accum and curr means this solution isn't entirely "point free". Thoughts?

Yeah, named arguments are essentially the "points" you're trying to avoid in the pointfree style.

I see two directions here. One is to shorten the function but add more points via destructuring:

R.reduce((acc, [k, v]) => R.assoc(k, (acc[k] || [k]).concat(v), acc),
         {})

The other is to fall further into the Ramda world and do something like this:

const f = R.pipe(
  R.pluck('timeline_map'),
  R.chain(R.toPairs),
  R.reduceBy(R.flip(R.useWith(R.append, [R.nth(1)])), [], R.head),
  R.toPairs,
  R.map(R.unnest)
)

(The last two functions could probably be turned into a reduce as well if you're determined.)

This is fully pointfree, but seems kinda deliberately obtuse, even if the reduceBy reducer were to be pulled out into a named function. If you want an opinion I'd say stick the entire thing in a function that takes in the base keyword (eg. 'timeline_map'), and then use whatever you find most readable inside that function, without striving for total pointfree purity.

If you want more opinions, arguably there's a sweet spot of, say, 60-70% pointfree where you gain the most from the style. Beyond that, things can become increasingly contorted, and you end up with, well, R.reduceBy(...R.useWith...

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks so much for this! Chain definitely seems a good way to tighten this up. Though honestly I don't get the name, Ramda docs say it's known as flatMap in other libraries which makes more sense. I think sticking to my slightly more wordy reduce callback is more understandable than some of the voodoo required to do it entirely point-free. Thanks again for all the feedback. \$\endgroup\$
    – 1252748
    Aug 9, 2017 at 15:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.