At the moment it is not possible to map over null while this is really well defined if you interpret it in the maybe monad, it would be nice if this was somehow supported
Discussions about this in #683 and elsewhere eventually led to the creation of Sanctuary.
Ramda kept on a different path, one more familiar to JS developers. I could see folding in Maybe and Either types with appropriate opt-in functions that could be used to replace null-checking. But I can't quite see making them the defaults for this fairly low-level library.
Are you suggesting having special handling for null that treats it as a Monad? I don't think that null in and of itself is the same as Maybe, because it can only represent a single value - it's a Nothing without a Just.
I do think that there could be a Null monad that satisfies all the laws, but it's sufficiently non-intuitive and non-interesting that I don't think Ramda needs to support it:
Per the Fantasy Land spec
u.map(a => a) is equivalent to u (identity) Null.map(identity) === Null
u.map(x => f(g(x))) is equivalent to u.map(g).map(f) (composition) Null.map(x => f(g(x))) ===Null.map(g).map(f) === Null
v.ap(u.ap(a.map(f => g => x => f(g(x))))) is equivalent to v.ap(u).ap(a) (again, we get Null)
...And so on for the Applicative and Chain typeclasses.
This suggests a few interesting things about null, but I think it would be fairly unexpected to treat null that way, given its ubiquity in JS. If you wanted a do-nothing monad, I think it's a better idea to define it explicitly so that it plays well with FL-compliant libs.
If instead you're suggesting that map(f, null) is simply a no-op, that sort of thing can make composition difficult, and lead to some tough bugs.
Ramda has R.defaultTo which can give you a more consistent way of dealing with potentially undefined values if you don't want to go all the way to Maybe. compose(map(f), defaultTo([])) would provide the safety you might be looking for, while still guaranteeing a consistent return type.
I believe what is being proposed is the following:
> R.map (R.inc) (null)
null
> R.map (R.trim) (null)
null
@Bradcomp You're completly right about the Null monad!
But I wouldn't say it isn't useful. What if we actually DO want to return null instead of a "consistent return type", i.e. think of the maybe monad. Just like David shows in his examples.
The map function now crashes at run-time. I don't see how returning null is worse.
@entropitor I think if you want the Null Monad then you could certainly do that, but I really don't know how useful it would be, and it would certainly not help with the case that is covered here.
If we consider null to be an analog of Nothing, then what is the analog for Just a? Should map (inc) (5) return 6? If not, how do we represent Just a using plain JS?
If we treat null as an ad hoc case, then it can make it very difficult to compose with functions that expect a concrete type. compose(length, filter(equals(5)), map(inc))(null) will choke, even if we update filter as well. We would have to build in null safety everywhere, which would lead to a totally different library, more similar to Sanctuary in one direction, or Underscore in the other.
On the other hand, defaultTo has an (aspirational) type of a? -> a, which can compose nicely with map as it stands:
compose(length, filter(equals(5)), map(inc), defaultTo([]))(null)
Dealing with nulls can be really tricky. I think using defaultTo or Maybe allow the user to deal with them explicitly, while silently passing them through can bury the problem and make it harder to track down in the future.
We are currently having a conversation about a similar issue in https://github.com/ramda/ramda/issues/2445. I would suggest that it isn't possible or reasonable for a function to work on any possible input. Crashing is a sign that there is a bug in your program, and it probably shouldn't be papered over. If you're getting null where you expect an Array, then something is wrong. If map just passes the buck, that wrong-ness will just be propagated further into your program.
Of course, the example you give will crash, it's because you're passing in a null to a function which doesn't accept a null. The thing is, many times the length function would be someting that DOES accept a null variable. In that case, we can't really use ramda to the full extent, just because sometimes the last function does not accept a null
The code compose(length, filter(equals(5)), map(inc))(null) at the moment already gives an error, it's just giving another one (crashing on map isntead of on length). With the help of e.g. flow-type, it could still check whether this would potentially lead to a null-error or not. So we wouldn't lose safety (it will crash on less examples and there is no code today that doesn't crash that will crash if we add this) while we win on usability and functional programming mindset.
I find
let result = null
if (x != null) {
result = compose(length, filter(equals(5)), map(inc))(null)
}
not very FP like and very verbose
@entropitor:
I guess I simply don't understand what you're proposing.
While we could easily write a map function which returned null if the functor supplied to it was null. But that doesn't help at all in compositions, unless we do that to practically all Ramda functions. And even if we did that, what would happen with other people's functions when passed into a composition? How would we deal with functions for which null is a legitimate input?
I think @Bradcomp's suggestion is quite useful. If you know you want a list, start your composition with defaultTo([]). If you want an object, perhaps defaultTo({}). Etc.
In addition to defaultTo, we also have when and unless that could be used if you _really_ want that nullto propagate through your pipeline.
unless(isNil, map(f))would pass the null through unchanged, but call map if anything else is passed in.
With all of these solutions (including Maybe), what's required is for you to consider what should happening when you get a null value. From my perspective, shielding the user from that consideration can lead to more bugs.
Ramda has provided a number of ways of dealing with null values, but in general it doesn't willingly propagate bad data. Once you go outside a function's domain the results are undefined. It might throw an error, it might just return garbage. Undefined behavior has a way of causing unexpected troubles, and so it's important to know what you are dealing with, not just blindly assume the edge cases are handled.
The developer should consider what the possible inputs for a function are, and make sure those inputs are handled properly. A big part of that is getting to a point where you know the types you're dealing with as soon as you can. Sanitizing and normalizing your input data can do wonders in making your logic easier to follow. Testing can provide assurances that you've covered the edge cases, and that changes aren't going to break your codebase. Type classes like Maybe and Either can help deal with errors and missing values. We shouldn't expect Ramda to solve all these issues as a low level functional toolkit.
First of all, I renamed the issue from Maybe monad to Nothing monad. Because as @Bradcomp pointed out, that's the one I'm talking about.
@CrossEye My point is that for map null IS a legitimate input and instead of erroring, it makes more sense (to me at least) to actually return null again.
defaultTo will not help here, the unless(isNil) will, although it's quite cumbersome to write every time.
@Bradcomp My point is that ramda is not shielding users from the consideration that the input is null. The input CAN be null at the moment, it just throws an error at run-time. So the users still have to check this!!!
You see null as a bad value, I see it as a possible value. I'd argue that map is well-defined for null and thus that null is part of the domain. Right now the result is an error. If you consider the null as bad data, then the new result would be "garbage" (but then again, according to your reasoning null is not part of the domain so it doesn't really matter).
You can already map over array, objects, ... I'd just argue to add null to there as well (to interpret null as the nothing monad, and thus implement all monad methods for null according to the Nothing monad)
So you're saying map, ap, of, and chain should be updated, but not our other functions?
Yes, indeed. For the others I agree that it would be hacky and not very clean
So what's your take on undefined?
Personally, I think the current behavior might be appropriate there as I don't think anybody is using undefined as a Nothing
Yours is the first request we've seen for this. I'm not sure how I feel about it.
I think there will be many here that would argue simply, "Let it fail". I'm a bit more sympathetic to this idea than that.
But I'm less convinced by a part-way solution that only works with the four monadic function. Why should those be special? Why wouldn't filter be included? Should adjust(inc, 3, null) simply yield null? And so on? And as to the difference between null and undefined, while I think the language does differentiate, many users do not. Why would you support just the one?
(Of course, if we were to do this for a great number of functions, it would also be a large effort...)
And what is your response to the inevitable argument that not throwing here is just burying the problem in way that makes it harder for users to debug?
@entropitor Douglas Crockford thinks null is a bad idea and we should all just use undefined.
For my part I follow his recommendation and just use undefined.
And what is your response to the inevitable argument that not throwing here is just burying the problem in way that makes it harder for users to debug?
This is one of the arguments I have been making. I think this change would be much more likely to cause bugs than it would be to help things, especially since we have numerous straightforward ways of achieving what is desired without hiding behind a special case.
@Bradcomp: I have mixed feelings about this argument. It's certainly legitimate, but there is also the question of the library causing exceptions when the user doesn't see the situation as exceptional. (The argument I've made for propOr, for instance.) I would love to find the line that makes the choice clear, but I don't see one as yet.
If you want to be more consistent, you can definitely implement it for more functions than just the monadic ones. I just think it makes a lot of sense for those ones.
As for the null/undefined debate. I wasn't aware that people were using undefined instead of null as the bottom type. In that case, it makes sense to treat them the same.
I'm not sure if it is harder to debug or not. I didn't expect Ramda to throw an exception at all. So I'd assume a null output comes from a null input and then trace back the same way I have to do now. (Just to be clear: it took me a minute to understand the exception in the first place, so I'm not sure if the exception is so much better).
And while there are other ways to deal with the problem, the trouble I'm having with the other solutions is that they are quite verbose for what they are. I'd basically have to write my own helper functions to have the map (etc.) that I want.
And while there are other ways to deal with the problem, the trouble I'm having with the other solutions is that they are quite verbose for what they are. I'd basically have to write my own helper functions to have the map (etc.) that I want.
Unfortunately, that's often the case. Ramda tries to help with many common cases, but when you step away from its center, you usually have to write your own helpers. If this becomes a common request, I would definitely reconsider, but for now my take is that this is not a common enough requirement.
:-1:
what about this @entropitor ?
this would give you a way to guard against those errors and silently fail...
const guardNil = (xs) => R.isNil(xs) ? [] : iter;
compose(length, filter(equals(5)), map(inc), guardNil)(null)
@ottonascarella That is not the same as it returns the empty array. I'd want it to return null
@entropitor Douglas Crockford thinks null is a bad idea and we should all just use undefined.
I tried both approaches ("always use null" vs "always use undefined") for considerably long periods. null turned out to be a better default overall. Enough to say that undefined disappears in JSON.stringify. Not a good advice.
As for the proposition... We already have some functions with such behavior:
> R.prop("x", null)
undefined
> R.prop("x", undefined)
undefined
what the community thinks about them ^_^? I don't see a lot of consistency here:
> R.props(["x"], null)
TypeError: Cannot read property 'x' of null
// why not [undefined] then?
> R.prepend("x", null)
[ 'x' ] -- null is treated as [] (not [null])
> R.prepend(null, "x")
[ null, 'x' ] -- "x" is treated as ["x"]
> R.prepend(null, ["x"])
[ null, 'x' ]
I agree that we should try to figure out what sort of consistency we should aim for. Something is amiss.
We already have some functions with such behavior:... what the community thinks about them ^_^?
Something is amiss.
There are a lot of things "amiss" in JavaScript. Undefined and null being one of them. I dare to say that a functional library on top of that flawed language will not make those things go away.
As I see it, the purpose of Ramda is to promote fp-style programming while being consistent in itself, but also inclusive in reaching out to userland code e.g. by working with variadic functions and regular JS data types like undefined and null too.
What I mean is that if for the average experienced JS programmer undefined and null have the same meaning of an absent value, Ramda should also not put a meaning into them.
I dare to say that a functional library on top of that flawed language will not make [
nullandundefined] go away.
A library cannot remove null and undefined from the language, but a library can ensure that it's never necessary to use these values in one's programs. ;)
As I see it, the purpose of Ramda is to promote fp-style programming while being consistent in itself, but also inclusive in reaching out to userland code e.g. by working with variadic functions and regular JS data types like undefined and null too.
While I like this sentiment, I think this is not always possible. I'm sure @davidchambers would agree.
So I think we spend a lot of time trying to find an appropriate balance. Perhaps if we could better articulate what that balance should be, we could become more consistent in these ideas. The notion that @buzzdecafe and I have pushed for a long time of not holding the user's hand is important, but it's also something of a cop-out. The whole point of a library is to hold someone's hand for some ways. We just usually find some point where we want to let go. And I think that point is not only not obvious, but is mostly ad hoc.
@ivan-kleshnin
Douglas Crockford thinks null is a bad idea and we should all just use undefined.
I tried both approaches ("always use null" vs "always use undefined") for considerably long periods. null turned out to be a better default overall. Enough to say that undefined disappears in JSON.stringify. Not a good advice.
I cannot figure out why you'd need undefined and null unless you put some different meaning into those values.
Don't let a to-string serialization function like JSON.stringify dictate what types you use.
JSON.stringify({0: undefined}) == "{}" // but
JSON.stringify([undefined]) == "[null]" // WTF????
Indeed JSON.parse(JSON.stringify({u: undefined})) == {}.
Thus my question is: What is the difference between {} and {u: undefined} with respect to the value of the property u?
var nob = {};
var ob = {u: undefined};
ob.u === ob['u'] === nob.u === nob['u'] == undefined
So I am OK with JSON.stringify omitting undefined properties.
I cannot figure out why you'd need undefined and null unless you put some different meaning into those values.
Like you, I use only one of them but it's null instead of undefined. And when I get undefined – I know the source is not my code – it's some 3-rd party library for example. Which helps to search for bugs.
Thus my question is: What is the difference between {} and {u: undefined} with respect to the value of the property u?
In JS - there is no significant difference. In other languages – it very well may be. I prefer to code in a language-agnostic way unless there's a good reason not to.
@semmel:
I cannot figure out why you'd need
undefinedandnullunless you put some different meaning into those values.
I think the language does put different meanings on them. null is the user type to describe a missing value. {foo: null} implies that the creator of that object knows about the foo property, but has no reasonable value to assign to it. undefined is the language's way of dealing with needing to return a value when the data doesn't support it. Thus let foo; foo; is undefined as us ({bar: 1})['foo']. This is just a way to avoid throwing an error when the user reaches for something that doesn't exist, but might conceivably exist.
Unfortunately, these are both first-class values, and lawful FP cannot treat them differently from other values.
@ivan-kleshnin:
Like you, I use only one of them but it's
nullinstead ofundefined. And when I getundefined– I know the source is not my code – it's some 3-rd party library for example. Which helps to search for bugs.
That's a nice approach I'd never thought of. Of course it could be the native engine instead of a 3rd-party library, but nonetheless, if I only create nulls, I know the others are external. Clever!
In practice I have to deal a lot with the browser api. E.g.
localStorage["notPresent"] === undefined;
localStorage.getItem("notPresent") === null; // why not undefined too?
Maybe there is some involved explanation for the discrepancy - but I really don't care. I just want the item.
Most helpful comment
@entropitor Douglas Crockford thinks null is a bad idea and we should all just use undefined.
For my part I follow his recommendation and just use
undefined.