Search code examples
javascriptfunctional-programmingreducepointfreeramda.js

Refactor R.filter and R.map to just R.reduce in pointfree style


I've started learning Ramda.js and functional programming and am very interested in functional composition using the pointfree style of programming, but I am having difficulty getting my head around some of it and I'm hoping someone can help illustrate:

Suppose I have a list of people - I want to only get the people between an inclusive age range of 13-19 (teenagers). Then, I want to map each person to either the return value of the person's getName() method (if it exists), else their name property. Then, I want to call .toUpperCase() on the names.

If I were using regular JS prototype methods, I wouldn't use Array.prototype.filter to get the teenage users and then Array.prototype.map. For performance reasons, I'd use Array.prototype.reduce, the body of which would be guarded by a conditional that checks if each iterated item meets the criteria for a teenager. This way, I'm iterating one less time. Elijah Manor has an article about this on his blog.

Here's the pointfree Ramda code I've come up with using R.filter and R.map (this works as expected):

var people = [
  { name: 'Bob', gender: 'male', age: 22 },
  { name: 'Jones', gender: 'male', age: 15 },
  { name: 'Alice', gender: 'female', age: 19 },
  { name: 'Carol', gender: 'female', age: 32 },
  { name: 'Odu', gender: 'male', age: 25 },
  { name: 'Fred', gender: 'male', age: 55 },
  { name: 'Nicole', gender: 'female', age: 29 },
  { getName: function() { return 'David' }, gender: 'male', age: 23 }
]

var getUpcasedTeenagerNames = R.pipe(
  R.filter(
    R.propSatisfies(R.both(R.lte(13), R.gte(19)), 'age')
  ),
  R.map(
    R.pipe(
      R.ifElse(
        R.propIs(Function, 'getName'),
        R.invoker(0, 'getName'),
        R.prop('name')
      ),
      R.toUpper
    )
  )
)

getUpcasedTeenagerNames(people) // => ['JONES', 'ALICE']

My question is - how would I re-write the following native version of the above algorithm to use pointfree Ramda.js?

var getUpcasedTeenagerNames = function(people) {
  return people
    .reduce(function(teenagers, person) {
      var age = person.age
      if (age >= 13 && age <= 19) {
        var name
        if (typeof (name = person.getName) === 'function') {
          name = name()
        } else {
          name = person.name
        }
        teenagers.push(name.toUpperCase())
      }
      return teenagers
    }, [])
}

I've tried doing it with R.scan, looked at using R.reduced and R.when but I'm afraid I might be missing the point a little bit.

For your convenience, I've included this code on the Ramda REPL: http://goo.gl/6hBi5k


Solution

  • First of all, I would break the problem down a little differently. I would use Ramda's R.__ placeholder to fill in the first argument to R.lte and R.gte, so that they read better. I like to alias this with a plain underscore, so this will read R.both(R.gte(_, 13), R.lte(_, 19)), which I find more readable. Then I would separate out the function which finds the name on a person. That's clearly a stand-alone bit of code, and pulling that out makes the main code much more readable.

    Finally, and here's the big thing, if you learn a bit about Transducers, you will learn a trick that stops the need to worry about the intermediate collections that are the possible performance problem in the initial technique.

    var _ = R.__;
    var findName = R.ifElse(
      R.propIs(Function, 'getName'),
      R.invoker(0, 'getName'),
      R.prop('name')
    ); 
    
    var getUpcasedTeenagerNames = R.into([], R.compose(
      R.filter(
        R.propSatisfies(R.both(R.gte(_, 13), R.lte(_, 19)), 'age')
      ),
      R.map(R.pipe(findName, R.toUpper))
    ));
    
    getUpcasedTeenagerNames(people); //=> ["JONES", "ALICE"]
    

    Now I wouldn't worry about the performance at all, unless I've found a performance problem and shown that this section of code is a major culprit. But if I had, then I could fix it by using transducers, and since both map and filter already work, all I need to do is to switch the direction of my composition (here by changing from pipe to compose) and wrap it up with into([]).

    If you're interested, here's an article on user transducers in Ramda, and another good intro to transducers.

    If I find a little time, I will see if I can convert your code into a points-free solution, but please don't make a fetish out of points-free. It's a useful technique in some circumstance, but we shouldn't feel driven to use it where it doesn't easily fit.