Skip to content

Instantly share code, notes, and snippets.

@DrBoolean
Last active July 30, 2018 18:15
Show Gist options
  • Save DrBoolean/7aeb3f352561afc23d26 to your computer and use it in GitHub Desktop.
Save DrBoolean/7aeb3f352561afc23d26 to your computer and use it in GitHub Desktop.
Debugging Functional post

Debugging Functional

What's the problem

When working with ramda or lodash-fp, one may run into some rather cryptic error messages. When trying to console.log a function we're presented with the guts of some internal curry or compose implementation that tells us nothing. At Loop/Recur we enjoy working with Functional JavaScript so I'd like to see this improve.

This post will demonstrate a simple solution that can go a long way to enhance the debugging experience in functional JavaScript applications.

Test subjects

Here are some simple functions to use in our tests. Two are named and curried, one is anonymous.

var add = curry(function add(x, y){ return x + y });
var map = curry(function map(f, xs){ return xs.map(f) });
var head = function(xs){ return xs[0] };

Fixing Curry

Here's a simple curry implementation I took from Erin Swenson-Healey's post

function curry(fx) {
  var arity = fx.length;

  function f1() {
    var args = Array.prototype.slice.call(arguments, 0);
    if (args.length >= arity) return fx.apply(null, args);

    function f2() {
      return f1.apply(null, args.concat(Array.prototype.slice.call(arguments, 0))); 
    }
    return f2;
  };
  return f1;
}

Let's make an inc function that just adds 1, then we'll inspect it with console.log

var inc = add(1);

console.log(inc)
// function f2() {
//   return f1.apply(null, args.concat(Array.prototype.slice.call(arguments, 0)));
// }

Riiiiigghhhht. That's not really all that helpful. It would be lovely is to see add and its argument 1 somewhere so I know what's going on.

Let's fix this up. The trick is to overwrite toString() and have it refer to the original function (called fx here). Then we'll log out the args that are applied.

Here's the important bit (I'll show it all together when we're finished):

f2.toString = function() {
  return fx.toString()+'('+args.join(', ')+')';
}

In the words of daft punk, one more time:

console.log(inc)
// function add(x,y){ return x + y }(1)

Wow, that's so much more helpful than displaying curry's dirty secrets. What if we don't apply it at all?

console.log(add)
//  function f1() {
//    var args = Array.prototype.slice.call(arguments, 0);
//    if (args.length >= arity) return fx.apply(null, args);
//
//    function f2() {
//      return f1.apply(null, args.concat(Array.prototype.slice.call(arguments, 0))); 
//    }
//    return f2;
//  };

Doh! We'll need to also overwrite f1.toString(), which will simply be fx.toString().

f1.toString = function() { return fx.toString(); }
console.log(add)
// function add(x, y){ return x + y }

Great! We can inspect the actual underlying function in all cases. Now, let's look at a slightly more complicated example:

var incAll = map(inc);
console.log(incAll)
// function map(f, xs){ return xs.map(f) }(function add(x, y){ return x + y }(1))

Here, map and inc are both partially applied functions. The output is alright, but like a retro contact lens, it's a little hard on the eyes. If we named the functions, why not show that instead?

Here's a helper:

function fToString(f) {
  return f.name ? f.name : f.toString();
}

Now we can output a very nice inspection of the currying going down:

console.log(incAll)
// map(add(1))

Here's the improved curry in all its debuggable glory:

function curry(fx) {
  var arity = fx.length;

  function f1() {
    var args = Array.prototype.slice.call(arguments, 0);
    if (args.length >= arity) return fx.apply(null, args);

    function f2() {
      return f1.apply(null, args.concat(Array.prototype.slice.call(arguments, 0))); 
    }

    f2.toString = function() {
      return fToString(fx)+'('+args.join(', ')+')';
    }
    return f2;
  };

  f1.toString = function() { return fToString(fx); }
  return f1;
}

Fixing Compose

We can inspect functions made via compose() with the same trick.

I'll steal a simple compose from Blake Embrey's post. By the way, I chose these two implementations of curry and compose because they were the first search results and seemed simple enough. The ones in ramda and lodash are optimized and thus more complicated.

var compose = function() {
  var fns = arguments;

  function f(result) {
    for (var i = fns.length - 1; i > -1; i--) {
      result = fns[i].call(this, result);
    }
    return result;
  };

  return f;
};

We end up in compose's messy bedroom if we try to inspect our function:

var incFirst = compose(head, map(inc))

console.log(incFirst)
// function f(result) {
//   for (var i = fns.length - 1; i > -1; i--) {
//     result = fns[i].call(this, result);
//   }
//   return result;
// }

And... toString() to the rescue:

f.toString = function() {
  return 'compose('+[].slice.call(fns).map(function(f){ return f.toString() }).join(', ')+')';
}

This is a little more complicated because we have to slice the arguments object and stringify each function. Let's take it for a spin:

var incFirst = compose(head, incAll)

console.log(incFirst)
// compose(function (xs){ return xs[0] }, map(add(1)))

Since map and add are curried, they are using our previous toString(). We just output the normal toString() for head.

This approach works particularly well if we are composing other compositions - as one does in this paradigm.

var incFirstThenAdd = compose(add(2), incFirst)

console.log(incFirstThenAdd)
// compose(add(2), compose(function (xs){ return xs[0] }, map(add(1))))

It inlines our composition nicely. We can do extra fancy tricks to pretty print, but this is already a huge improvement over the opaque innards of compose.

Approaching perfection

There's one very common error when using compose. Often times, we give it something other than a function. This typically occurs when we either forget to partially apply a function of multiple arguments or we accidentally apply a function with all its arguments.

To demonstrate:

compose(add(1, 2), head)(xs)
// /Users/blonsdorf/Documents/blog/debug.js:57
//          result = fns[i].call(this, result)
//
// TypeError: undefined is not a function
//     at f (/Users/blonsdorf/Documents/blog/debug.js:57:23)
//     at Object.<anonymous> (/Users/blonsdorf/Documents/blog/debug.js:96:46)

We've called add with two arguments, returning a value rather than a function to compose (yes I know functions are values too...).

Let's catch this error and throw a better message:

for (var i = fns.length - 1; i > -1; i--) {
  try {
    result = fns[i].call(this, result);
  } catch(e) {
    e.message = f.toString()+' blew up on '+fns[i].toString()
    throw(e)
  }
}

Let's try this again:

compose(add(1, 2), head)(xs)
// /Users/blonsdorf/Documents/blog/debug.js:61
//         throw(e)
//
// TypeError: compose(3, function (xs){ return xs[0] }) blue up on 3
//     at f (/Users/blonsdorf/Documents/blog/debug.js:57:23)
//     at Object.<anonymous> (/Users/blonsdorf/Documents/blog/debug.js:96:46)

Much nicer to see what's going on here. Of course, types would prevent this in the first place, but then I'd suggest using Elm or PureScript instead of messing with any of this stuff.

In closing

We can overwrite the string values in order to display better errors. This is a significant improvement over the current state of affairs and I think all JavaScript FP libraries should incorporate better debugging in general.

We could use this to show memoized functions in a sane way. The same technique can be used to display the inner value of functors and monads as well.

When I feel writey, I'm still working on the Mostly Adequate Guide if you're into this junk. G'day.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment