Skip to content
Advertisement

Ramda Curry with Implicit Null

I’ve been trying to learn the Ramda library and get my head around functional programming. This is mostly academic, but I was trying to create a nice logging function that I could use to log values to the console from within pipe or compose

The thing I noticed

Once you’ve curried a function with Ramda, invoking a function without any parameters returns the same function

f() returns f

but

f(undefined) and f(null)

do not.


I’ve created a utility function that brings these calls into alignment so that

f() equals f(null) even if f is curried.

// Returns true if x is a function
const isFunction = x => 
  Object.prototype.toString.call(x) == '[object Function]';

// Converts a curried fuction so that it always takes at least one argument
const neverZeroArgs = fn => (...args) => {
  let ret = args.length > 0 ?
    fn(...args) :
    fn(null)

  return isFunction(ret) ?
    neverZeroArgs(ret) :
    ret
}

const minNullCurry = compose(neverZeroArgs, curry);

Here it is in use:

const logMsg = minNullCurry((msg, val) => {
  if(isNil(msg) || msg.length < 1) console.log(val);
  else if(isNil(val)) console.log(msg);
  else console.log(`${msg}: `, val);
});

const logWithoutMsg = logMsg();
logWithoutMsg({Arrr: "Matey"})

Then if I want to use it in Ramda pipes or composition, I could do this:

// Same as logMsg, but always return the value you're given
const tapLog = pipe(logMsg, tap);

pipe(
  prop('length'),
  tapLog() // -> "5"
)([1,2,3,4,5]);

pipe(
  prop('length'),
  tapLog('I have an thing of length') // -> "I have an thing of length: 5"
)([1,2,3,4,5]);

pipe(
  always(null),
  tapLog('test') // -> "test"
)([1,2,3,4,5]);

I’ve just started with Ramda and was wondering if it comes with anything that might make this a bit easier/cleaner. I do realise that I could just do this:

const logMsg = msg => val => {
  if(isNil(msg)) console.log(val);
  else if(isNil(val)) console.log(msg);
  else console.log(`${msg}: `, val);
});

and I’m done, but now I have to forever apply each argument 1 at a time.

Which is fine, but I’m here to learn if there are any fun alternatives. How can I transform a curried function so that f() returns f(null) or is it a code smell to even want to do that?

Advertisement

Answer

(Ramda founder and maintainer here).

Once you’ve curried a function with Ramda, invoking a function without any parameters returns the same function

f() returns f

but

f(undefined) and f(null)

do not.

Quite true. This is by design. In Ramda, for i < n, where n is the function length, calling a function with i arguments and then with j arguments should have the same behavior as if we’d called it originally with i + j arguments. There is no exception if i is zero. There has been some controversy about this over the years. The other co-founder disagreed with me on this, but our third collaborator agreed we me, and it’s been like this ever since. And note that the other founder didn’t want to treat it as though you’d supplied undefined/null, but to throw an error. There is a lot to be said for consistency.

I’m here to learn if there are any fun alternatives. How can I transform a curried function so that f() returns f(null) or is it a code smell to even want to do that?

It is not a code smell, not at all. Ramda does not supply this to you, and probably never will, as it doesn’t really match the rest of the library. Ramda needs to be able to distinguish an empty call from one with a nil input, because for some users that might be important. But no one ever said that all your composition tools had to come from a particular library.

I see nothing wrong with what you’ve done.

If you are interested in a different API, something like this might possibly be interesting:

const {pipe, prop, always} = R

const tapLog = Object .assign (
  (...val) => console .log (...val) || val,
  {
    msg: (msg) => (...val) => console .log (`${msg}:`, ...val) || val,
    val: (...val) => (_) => console .log (...val) || _
  }
)

tapLog ({Arrr: "Matey"})

pipe(
  prop('length'),
  tapLog // -> "5"
)([1,2,3,4,5]);

pipe(
  prop('length'),
  tapLog.msg('I have an thing of length') // -> "I have an thing of length: 5"
)([1,2,3,4,5]);

pipe(
  always(null),
  tapLog.val('test') // -> "test"
)([1,2,3,4,5]);
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement