ramda: Better error messaging and type-checking

I tried once before to do a lot of wholesale change to make the library ready to go to 1.0. It didn’t work. Everything fizzled out.

This time, I want to treat one issue at a time. Please make suggestions for other issues to discuss. But let’s make one issue at a time the priority. The idea is not necessarily to get these changes into 1.0, but to make sense of what our roadmap should be. I’d actually prefer to publish 1.0 soon, and start making these changes in subsequent versions. But I’m far from certain about this.


Error Messaging and Type-Checking

The first topic is error messaging and type-checking

Ramda has long been a low-level library that was built on the philosophy of letting things fail when provided bad data. @buzzdecafe described it as a library that didn’t hold your hands.

That is all well and good, and made sense for the learning library that we first wrote. But Ramda has grown since then. We’re now sometimes mentioned in state of the platform discussions. It’s used in 133K GitHub repositories and is a dependency in over 9000 NPM projects. Lately we’ve been averaging a million NPM downloads per day, just above Underscore, and a quarter that of lodash. This is – for better or worse – no longer just the little playground that @buzzdecafe and I started.

And our error messaging is, frankly, awful. This is one of the easier ones to follow, and it’s pretty bad:

> map('foo', [1, 2, 3])
ramda.min.js:1 Uncaught TypeError: t is not a function
    at x (ramda.min.js:1)
    at ramda.min.js:1
    at ramda.min.js:1
    at t (ramda.min.js:1)
    at <anonymous>:1:1

They get progressively worse from there.

So it might make sense to have better error messaging. To support that we’d probably need to do more type-checking than we have historically done.

Several recent issues/pull requests go to the heart of this.

#2997 asks what happens when reduce is passed a nil value as the final argument. So far, I’ve been perfectly willing to answer: “Just return the accumulator”. It seems perfectly reasonable to me. It adds a quick type-check to that argument.

#2993 asks what happens when pipe is supplied something other than a function. This PR suggests type-checking and throwing a specific error in that case. I’ve long been hesitant to do this. It’s not the sort of code I’m particularly interested in writing or maintaining; it bloats the library; and it can easily lead to a false sense of security. But it might be time for me to move on from this. If Ramda is becoming an everyday workhorse for so many people, then it probably should act like one.

Proposal

In some version of Ramda soon (2.0?) we start type-checking the arguments supplied to our functions. So pipe for instance need to be passed only functions. We should test that all arguments are functions, and craft a useful error message if they’re not.

Sanctuary has taken the lead on this, and has some of the best error messages I’ve run across. But I don’t propose to go as far as Sanctuary does. I would expect to check that pipe receives functions, but not to check for instance that pipe(foo, bar) will fail because the return of foo doesn’t match the input of bar. It’s possible that we’ll add a better error message for that when someone tries to call the function created, but we would not try to catch that at the call to pipe. I believe that anything that attempts to do so will have to ignore the ergonomics of JS by imposing a much stronger types system than is generally comfortable.

Implementation

I haven’t spent any real time on how we’ll do this yet. We could do something like what sanctuary-def does and use a new function to actually create our functions. (This might let us skip our internal curry functions, and might let us gain some other benefits such as those described in Brian Lonsdorf’s Debuging Functional.) Or we could add it function-by-function on a more ad hoc basis. Or we might take a middle ground in which nearly every function begins with a call to an internal checkParameters function, something like

const map = curry2 ((fn, list) => {
  const typeError = checkParameters([fn, list], Function, [Array, Object, Function, FLMap])
  // handleTypeError
  // body of map
})

(or if we don’t mind another function on our stack traces, let the checkParameters function actually throw the exception.)

I’m not thrilled with any of these options, and would welcome an alternative.

Transition

I’m not sure that there’s a consistent transition plan; it will probably depend on the implementation we’ve chosen. But we will need to know if we want to release such changes all at once or in point-releases. And it would need to be scheduled around another major change I’d like to see: the reimplementation with modern JS, another change that will affect every one of our functions. While I would have no problem doing these together, it might be a very large bite to take.

Alternatives

  1. There is at least one clearly viable alternative. @davidchambers has often suggested that Ramda simply adopt sanctuary-def in Ramda. It would involve using a battle-tested tool instead of trying to grow our own.

    I’m reluctant for a few reasons. First, I very much like that Ramda is dependency-free. Second, sanctuary-def is built around a much stronger notion of typing than feels comfortable to me for Ramda. Third, I really believe that communities are better served by competition; while they might eventually settle down on a small number of tools, I would rather see the alternatives first.

    None of this means that I reject it out of hand. But I would take some real convincing.

  2. Doing nothing. We absolutely do not have to do this. Ramda is being used by many, and people have certainly been willing to overlook this flaw. Perhaps there’s no reason to shake things up.

  3. Other? Any other suggestions?

Feedback

Feel free to add your feedback as comments here. Anyone is welcome to comment. But people who’ve contributed to Ramda are especially invited. We’d love to hear what you think.

@ramda/core: We could especially use your thoughts.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 8
  • Comments: 26 (26 by maintainers)

Most upvoted comments

Are there any plans to start this?

In ramda/ramda gitter, I have seen many problems people ask for help, are just caused by incorrect use of ramda apis, such as:

  • passing string instead of array as R.path first argument;
  • wrong ways of using pipe/compose:
    • compose the result of executed functions;
    • compose functions of wrong arity; ,etc.

If ramda have a better error messageing and type-checking, I believe it will be of great help to:

  • improve developers’ development experience;
  • give people more meaningful information of runtime errors in product environment;
  • reduce the learning curve of ramda and the entry barrier;

In order to help people easy to learn, write and debug with ramda, I think this is an urgent and important thing to do now.

I would prefer to have the API for our prop / path related functions set before 1.0, but that is the only thing I can think of.

I think starting a 1.x branch is a good idea, but I would want to release 1.0 at the same time so that we can focus on 2.0 instead of pushing for 2 releases.

Once we have that branch we can use it for bug fixes and minor stuff while focusing on the ES6 codebase.

I’ve played with several partial implementations, but have nothing complete yet.

I would like to try this. But I think there is a major question about versions. I would really rather implement this in ES6+ code. So I’m feeling that if we do this, it would be after 1.0. How do you feel about that?

I don’t know what else we want to do before 1.0. I’m hoping it’s very little. But I think it might be time to create a 1.x branch for work we want in our ES3/ES5 code-base, and make master available for work like this on 2.0±versions. Does that make sense?

Just for comparison, I’ve been thinking about something more like this:

const validateArgs = (name, types, args) => {
  // A dummy for now.  A real implementation would compare `types` and `args`
  if (isNil (args [2])) {
    throw TypeError (`Improper type supplied to '${name}'.  Expecting ArrayLike at index 2.`)
  }
}

// This function might also replace `curry1`, `curry2`, etc.
// I have many dreams about what might go in here.
const def = (name, types, fn) => {
  const res = (...args) => {
    validateArgs (name, types, args) // stack-trace fix required?
    return fn (...args)
  }
  res.toString = () => fn .toString ()
  Object .defineProperty (res, 'name', {value: name, /* writable, etc.*/})
  return res
}

const foldl = def ('foldl', ['Function', 'Any', 'ArrayLike'], (fn, init, xs) => 
  xs .reduce ((a, x) => fn (a, x), init)
)

foldl ((a, n) => a + n, 0, [1, 2, 3]) //=> 6
foldl ((a, n) => a + n, 0, undefined)
//~> Improper type supplied to 'foldl'.  Expecting ArrayLike at index 2.

We might well be able to combine your try {happyPath} catch {validate} technique with this.

I haven’t really thought through our few remaining variadic functions like pipe. That might be more difficult to handle.

I don’t mean this to go as far as Sanctuary does with types – I can’t see testing for types like Array(NonZeroInteger) or any such. In fact I think we could just ignore the possible parameterization of types. Bur this might still be an improvement.

@adispring:

I don’t think Typescript can help with this problem. We’re writing a library to be used by JS code-bases. Even if every function had strong TS typings (which last I knew wasn’t even possible yet), this would offer nothing at all to the JS user, unless we tried to insist that all users write in TS and use its compiler to do their type-checking.

That would be a very different library.

Are you already working on an implementation for these things?

sanctuary-def does all the things being discussed. I suggest using that as a starting point and deleting code relating to features you do not wish to provide. I am happy to provide assistance. 😃

Maybe Typescript is another good choice for type checking.

The problem with checking types in runtime is that “typeof” is expensive. Only “isNil” has no overhead. Sancutary is an example of code that is beautiful, but medium useful in practice. This is the slowest functional library in JS. It would be nice to have type exceptions, but only in the case of developer code. There is a special NODE_ENV in React, which in developer mode adds additional hook validation, type validation, etc. Maybe it’s worth doing the same here?

Something like this:

const map = curry2 ((fn, list) => {
  if (process.env.NODE_ENV === 'development') {
    const typeError = checkParameters(...)
    // handleTypeError
  }
  // body of map
})

An additional advantage of this approach is cutting this piece of code through Webpack.

@ku8ar:

The discussion here is about adding more type-checking, not removing it.

To me, path is mostly about gracefully handling the nil values in a chain of props.

In order to have something to talk about, here’s pipe:

function pipeImplementation() {
  //Creates the function that composes the input functions
}

function validateArguments(err) {
  const args = Array.from(arguments).slice(1);
  var idx = 0;
  while (idx < args.length) {
    if (!_isFunction(args[idx])) {
      //To Discuss - I think we should have our own errors for composability purposes. 
      //To Discuss - This doesn't handle fixing the stack trace, which I think we should do and most likely can do. 
      throw new TypeError(
        'TypeError: ' + toString(args[idx]) + ' is not a function\n All arguments ... argument ' + idx + ' blah';
      );
    }
    idx += 1;
  }
  throw err;  // If our validation doesn't come up with a valid reason for the failure, just pass it up.
}

function pipe() {
  var pipedFns = pipeImplementation(...arguments);
  return function() {
    try {
      return pipedFns(...arguments);
    } catch (err) {
      return validateArguments(err, ...arguments);
    }
  }
}

There are a lot of open issues here, and the code is just typed in the textbox so forgive me…

Also pipe is a bit trickier because it returns a function. I think the basic design is there though.

Our validateArguments could handle determining if a reasonable default should be returned instead of throwing an error.

I think we can look at a reusable way to create these validators and ‘compile’ the pieces together. That could lead to additional benefits when it comes to our discussions around dispatching, currying, transducers, and other project wide initiatives.

The happy path is mainly unaffected by the checking of the arguments - it’s only in the case that the inner function throws that we take the validation hit.

@Bradcomp:

Magic bugs are way harder to find than exceptions. Exceptions are obvious. If we start making assumptions based on invalid data I think we are making our own lives and our users lives harder than they need to be.

This is of course the biggest concern with all of this. And I still haven’t wrapped my head around it. I could well see us simply choosing to throw on any unexpected input. I worry about that because I’m often writing for browser-based user interfaces, where code that might bring down the whole system is much worse than code that might have occasional incorrect data. I really don’t like exceptions. But I can easily see us making that choice.