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
-
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.
-
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.
-
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)
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:
R.path
first argument;pipe/compose
:If ramda have a better error messageing and type-checking, I believe it will be of great help to:
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:
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.
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:
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 ofprop
s.In order to have something to talk about, here’s pipe:
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:
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.