How to Debug Your Point-Free Code with Ramda

Jacob Worrel
ITNEXT
Published in
4 min readJan 21, 2019

--

About six months ago, I started to use Ramda, a library for writing functional JavaScript, in my day-to-day life and overall I think it’s helped me write code that’s simpler, easier to reason about, and less error-prone. There are various reasons for this, notably that immutability is at the heart of its design and it encourages reusability through functional composition. However, no matter how good of a programmer you are, you’re inevitably going to run into bugs and when you do, you generally want that experience to be as frictionless as possible.

If you’re like me, and you like to stick console.log statements everywhere to figure out what’s going on (I know, I know — I should be using my debugger), then the paradigm shift from an imperative way of programming to a declarative one might throw you off at first, especially if you’re trying to do things point-free. Today, we’ll learn a simple trick that allows you to peer into your program and observe a value at any given step along the way.

Stick that in your Pipe and Smoke It

In Ramda, you’ll frequently find yourself piping the return value of one function into another in order to apply a series of transformations to a given input. Ramda provides a handy pipe function to accomplish this.

const add1AndMultiplyBy2 = R.pipe(
x => x + 1,
x => x * 2,
);
add1AndMultiplyBy2(1); // returns 4

Let’s pretend these two lambdas are actually reusable and define them outside our pipe as standalone utilities.

const add1 = x => x + 1;
const multiplyBy2 = x => x * 2;

We can then make our add1AndMultiplyBy2 function entirely point-free.

const add1AndMultiplyBy2 = R.pipe(
add1,
multiplyBy2,
);

Great- now we have clean, elegant code that’s transparent about what’s happening at each step of the way. What could possibly go wrong?

Well, what if, unbeknownst to us, our add1AndMultiplyBy2 function gets called with the string'1' instead of the number 1 ?

add1AndMultiplyBy2('1'); // returns 22

That’s right — this returns the number 22. Hats off to anyone who knew this instinctively. For the rest of you wondering what’s going on, let’s first take a look at how our function might be written without the pipe, in plain old vanilla JavaScript.

function add1AndMultiplyBy2 (num) {
const plus1 = add1(num);
const plus1Times2 = multiplyBy2(plus1);
return plus1Times2;
}

To get to the bottom of this mysterious 22, the first thing you might do is stick a console.log after the call to add1 to observe its return value.

function add1AndMultiplyBy2 (num) {
const plus1 = add1(num);
console.log(plus1); // logs '11'
const plus1Times2 = multiplyBy2(plus1);
return plus1Times2;
}

Aha! Tricksy, tricksy JavaScript! It’s concatenating the number 1 with the string '1' , resulting in '11', which then gets passed in to our multiplyBy2 function, and through the magic of type coercion results in our final return value, 22. Mystery solved.¹

Great- but how does one come to this less-than-obvious conclusion when using Ramda. If we go back to our more terse version of add1AndMultiplyBy2 using our nifty pipe, we’ll notice there’s no place we can stick a console.log.

R.tap() to the Rescue!

Luckily, Ramda provides a function called tap which runs a given function with a supplied value, then returns the value, unchanged.

Think of it like an identity function (which does nothing but return the value it was provided), but with the ability to perform some side effect.

So how is this useful? Now we can console.log the value at any given step in our pipe to observe what’s going on when things go awry.

const add1AndMultiplyBy2 = R.pipe(
add1,
R.tap(x => console.log('WTF', x)), // logs 'WTF' and '11'
multiplyBy2,
);
add1AndMultiplyBy2('1'); // returns 22

All this, without breaking our pipe.

This last point is important since if we simply passed console.log as the next function in our pipe after the call to add1, we would actually change the final result since console.log returns undefined.

const add1AndMultiplyBy2 = R.pipe(
add1,
console.log, // returns undefined
multiplyBy2, // undefined * 2??
);
add1AndMultiplyBy2('1'); // returns NaN, duh

This will likely lead to more confusion, especially since returning undefined in the middle of a pipe will often throw a big fat error, at which point you might be tempted to start pulling your hair out.

Wrapping Things Up

Since using tap to log something to the console is so practical for debugging, let’s go ahead and create a wrapper for this particular use case.

const log = msg => R.tap(x => console.log(msg, x));

Now we can just use our handy log function anywhere in a pipe function much like we would a regular console.log without even having to remember how tap works.

const add1AndMultiplyBy2 = R.pipe(
add1,
log('value after add1: '), // logs "value after add1: 11"
multiplyBy2,
);
add1AndMultiplyBy2('1');

And that’s a wrap! 😎

  1. For more on type coercion in JavaScript, check out Kyle Simpson’s Types and Grammar book.

--

--