Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Map and filter an object using Ramda

I'm learning Ramda and I'm a little confused how to build this lodash chain below using Ramda. Ramda returns functions for it's operations instead of actual values, and this seems to be the focal point of functional programming, however in this example I have a second parameter localRegex that isn't a primary argument. It seems that it wouldn't be possible to get this to be completely duplicated without wrapping the Ramda function and using .apply() or .call() to propagate the wrapped function arguments to the Ramda function, which seems more complicated then using lodash.

var _ = require("lodash")
var R = require("ramda")

var localRegex = /^.\.\/|^.\/|^\//

function getRecursiveDeps(deps, localRegex){
  return _.chain(deps)
    .map(function(dep){
      return dep.source.value
    })
    .filter(function(dep){
      return dep.match(localRegex)
    })
    .value()
}

var items = [
  {
    "source": {
      "value": "./foo"
    }
  },
  {
    "source": {
      "value": "bar"
    }
  }
]

console.log(getRecursiveDeps(items, localRegex))

Here's what I've got and its not working.

var getRecursiveDeps = R.chain(
  R.map(function(dependency){
    return dependency.source.value
  }),
  R.filter(function(value){
    return value.match(localRegex)
  })
)

Is there a way to have Ramda use a main variable for chaining and also pass down localRegex? Is there a way to duplicate the getRecursiveDeps that uses lodash in Ramda?

There's a lot of talk about how Ramda is functional and underscore and lodash aren't. But in this case the getRecursiveDeps is a function that returns a value from lodash. When you create functions like this from lodash or underscore the result is the same, there's just more work when it comes to wrapping it, in this case what would be the perk using Ramda over Lodash?

like image 691
ThomasReggi Avatar asked Jul 29 '15 13:07

ThomasReggi


2 Answers

R.chain does something completely different from _.chain. Its type, according to the current documentation, is (a -> [b]) -> [a] -> [b], though its actual type is more general. Think of it as a "flat-map" function.

What you actually want here is R.compose or its left-to-right equivalent R.pipe.

If the goal of the function is to find local dependencies, it seems appropriate to me to embed the pattern in the function. I would thus write:

// getLocalDeps :: [{ source :: { value :: String }}] -> [String]
const getLocalDeps =
R.pipe(R.map(R.path(['source', 'value'])),
       R.filter(R.test(/^[.]{0,2}[/]/)));

getLocalDeps(items);  // => ['./foo']

I'm a bit confused by the name getRecursiveDeps as the function isn't recursive. getLocalDeps seems more appropriate.


If you'd like to parameterize the pattern, I suggest breaking getLocalDeps into smaller pieces:

// isLocal :: String -> Boolean
const isLocal = R.test(/^[.]{0,2}[/]/);

// getDeps :: [{ source :: { value :: String }}] -> [String]
const getDeps = R.map(R.path(['source', 'value']));

// getLocalDeps :: [{ source :: { value :: String }}] -> [String]
const getLocalDeps = R.pipe(getDeps, R.filter(isLocal));

You could then define other functions in terms of these building blocks:

// getNonLocalDeps :: [{ source :: { value :: String }}] -> [String]
const getNonLocalDeps = R.pipe(getDeps, R.reject(isLocal));

// getLocalJsonDeps :: [{ source :: { value :: String }}] -> [String]
const getLocalJsonDeps = R.pipe(getLocalDeps, R.filter(R.test(/[.]json$/)));
like image 83
davidchambers Avatar answered Oct 25 '22 11:10

davidchambers


Another way to do this, points-free and preserving your existing API is like this:

// getLocalDeps :: [{ source :: { value :: String }}] -> RegExp -> [String]
const getLocalDeps =  R.useWith(
  R.flip(R.call),
  R.map(R.path(['source', 'value'])),
  R.pipe(R.unary(R.test), R.filter)
);

localDeps(items, localRegex); //=> ["./foo"]

The last line of the function feels a bit unfortunate to me, and this question has caused me to open an issue about reverting some recent changes to the library. There are several variants that could be used:

// ...
R.pipe(regex => item => R.test(regex, item), R.filter)
//...

or

// ...
regex => R.filter(R.test(regex))
//...

But before a recent change to Ramda, it would have been simply

// ...
R.pipe(R.test, R.filter)
//...

One thing, though, is that Ramda tries hard to keep parameters in a logical order: with those less likely to change coming before those more likely to change. With that in mind, I would prefer something like this:

// getLocalDeps :: RegExp -> [{ source :: { value :: String }}] -> [String]
var getLocalDeps2 =  R.useWith(
  R.call,
  R.pipe(R.unary(R.test), R.filter),
  R.map(R.path(['source', 'value']))
);

localDeps2(localRegex, items); //=> ["./foo"]

And that feels cleaner still. Moreover, it lets you predefine the function and use it separately:

myDeps = localDeps2(localRegex);
myDeps(items); //=> ["./foo"]

And that is a good part of what Ramda is about.

like image 37
Scott Sauyet Avatar answered Oct 25 '22 10:10

Scott Sauyet