简体   繁体   中英

Ramda.js pipe that sets a property based in a previous parameter

Currently, I have the following code (which works):

 const double = R.multiply(2); const piped = R.pipe( (obj) => R.assoc('b', double(obj.a))(obj), (obj) => R.assoc('c', double(obj.b))(obj) ); console.log( piped({ a: 1 }) );
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

However I think that due to that (obj) at the end of each pipe function, I guess that I could refactor it to something better in the "Ramda world".

I'm still new to this library, so I yet don't know all the methods and tricks.

Is there a better way to do so using Ramda?

My "real" code is this:

function getScripts() {
  const tryRequire = tryCatch((path) => require(path).run, always(null));

  const addPathAndRunProps = pipe(
    // Note: The `scriptsPath` function is a bound path.join function.
    // It just returns a string.
    (dir) => assoc('path', scriptsPath(dir.name, 'index.js'))(dir),
    (dir) => assoc('run', tryRequire(dir.path))(dir)
  );

  const addModuleRunAndFilterInvalid = pipe(
    map(addPathAndRunProps),
    filter((dir) => typeof dir.run === 'function')
  );

  return addModuleRunAndFilterInvalid(
    fs.readdirSync(SCRIPTS_PATH, { withFileTypes: true })
  );
}

I think you might be over-using Ramda here. The code is a bit confusing. This would likely be easier to read in the future and more maintainable, while still being functional:

function getScripts() {
  const tryRequire = tryCatch((path) => require(path).run, always(null));

  const addPathAndRunProps = dir => {
    const path = scriptsPath(dir.name, 'index.js')

     return {
        ...dir,
        path,
        run: tryRequire(path),
     }
  }

  return pipe(
     map(addPathAndRunProps),
     filter(x => typeof x.run === 'function'),
  )(fs.readdirSync(SCRIPTS_PATH, { withFileTypes: true }))
}

Or, if you really want to keep those setters, try splitting your addPathAndRunProps function into two setters:

function getScripts() {
  const tryRequire = tryCatch((path) => require(path).run, always(null));

  const addPathProp = x => assoc('path', scriptsPath(x.name, 'index.js'), x)
  const addRunProp = x => assoc('run', tryRequire(x.path), x)

  return pipe(
    map(addPathProp),
    map(addRunProp),
    filter(x => typeof x.run === 'function'),
  )(fs.readdirSync(SCRIPTS_PATH, { withFileTypes: true }))
}

In both cases, I got rid of your addModuleRunAndFilterInvalid function. It doesn't add any clarity to your function to have addModuleRunAndFilterInvalid split out into its own function, and returning the result of the pipe clarifies the purpose of the getScripts function itself.

Also, in your code, you keep calling the object you're operating on dir . This is confusing since it implies the object has the same structure on each function call. However the variable passed to addRunProp does not have the same structure as what is passed to addPathProp (the one passed to addRunProp requires an extra path prop). Either come up with a descriptive name, or just use x . You can think of x as the thing your function is operating on. To figure out what x is, look at the function name (eg addRunProp means that x is something that will have a run property added to it).

One other potentially useful tip: I've settled on the naming convention of aug (short of "augment") for adding a property or bit of info to an object. So I'd rename your addPathProp function augPath and rename your addRunProp function augRun . Since I use it consistently, I know that when I see aug at the beginning of a function, it's adding a property.

I agree with Cully's answer -- there might not be any good reason to try to use Ramda's functions here.

But, if you're interested, there are some Ramda functions which you might choose to use.

chain and ap are fairly generic functions operating on two different abstract types . But when used with functions, they have some fairly useful behavior as combinators:

chain (f, g) (x) //=> f (g (x)) (x)
ap (f, g) (x)    //=> f (x) (g (x))

That means that you could write your function like this:

const piped = R.pipe(
  chain (assoc ('b'), pipe (prop ('a'), double)),
  chain (assoc ('c'), pipe (prop ('b'), double)),
)

I don't think this version improves on the original; the repetition involved in those internal pipe calls is too complex.

However with a helper function, this might be more reasonable:

const doubleProp = curry (pipe (prop, double))
// or doubleProp = (prop) => (obj) => 2 * obj[prop]

const piped = R.pipe(
  chain (assoc ('b'), doubleProp ('a')),
  chain (assoc ('c'), doubleProp ('b')),
);

This is now, to my mind, pretty readable code. Of course it requires an understanding of chain and how it applies to functions, but with that, I think it's actually an improvement on the original.

I frequently make the point that point-free code is a useful tool only when it makes our code more readable. When it doesn't pointed code is no less functional than point-free.


By the way, I just want to note that I'm impressed with the quality of your question. It's really nice to read questions that are well-thought out and well-presented. Thank you!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM