Reputation: 1259
I am learning Ramda and I am confused. I want to make a set function that works similar to the lodash.set function. However when I try the following on an path that exists in the object, it seems to work as intended, but when I use it to create a new path, it adds this weird array.
const R = require('ramda')
const set = (object, path, value) => R.set(R.lensPath(path), value, object);
const foo = {
moo: {
goo: 'goo'
}
}
set(foo, ['moo', 'goo', 'boo'], 'roo'); // { moo: { goo: { '0': 'g', '1': 'o', '2': 'o', boo: 'roo' } } }
So the result is: // { moo: { goo: { '0': 'g', '1': 'o', '2': 'o', boo: 'roo' } } }
When I expected it to be: // { moo: { goo: { boo: 'roo' } } }
Why does it add these characters by index? How do I accomplish a lodash.set function with Ramda?
Upvotes: 4
Views: 1443
Reputation: 50807
It seems like unwanted behavior. Why would anyone want Ramda to coerce the string?
I think of it as a different question. You're writing code that is supposed to do something vaguely equivalent to foo.moo.goo.boo = 'roo'
, when foo.moo.goo
is a string. That would of course throw an error such as Cannot create property 'boo' on string 'goo'
.
Lodash answers this by saying something like "Oh, you must have meant foo.moo.goo = {boo: 'roo'}
." That's a perfectly reasonable guess. But it's a guess. Should the library instead have thrown an error like the above? That would probably be the most logical thing to do.
Ramda (disclaimer: I'm one of its authors) makes a different choice. It assumes that you meant what you said. You wanted to goo
property of foo.moo
to be updated, by setting its boo
property to 'roo'
. It then does so. When it updates a property like this, though, as everywhere else, it does not mutate your original data, but builds you a new output, copying the properties of the old object except for this new path, which it sets appropriately. Well, your old object ('goo'
) has three properties, {0: 'g'}
, {1: 'o'}
, and {2: 'o'}
, which Ramda's assocPath
(the public function also used by lensPath
which does this job) then combines into a single object along with your {boo: 'roo'}
.
But I exaggerate here. Ramda never really made this choice. It's simply what fell out of the implementation. assocPath
knows about only two types: arrays and objects. It knows how to reconstruct these types only. And really it works as arrays and others. Since your foo.moo
is not an array, it treats it as an object.
If you would like to see this behavior changed, a new issue, or even better a pull request would receive a fair hearing. I can promise you that.
But I would expect a lot of pushback.
Ramda's philosophy is significantly different from that of lodash. lodash emphasizes flexibility. set
, for instance, allows you to write paths as arrays or strings, with its two examples of
_.set(object, 'a[0].b.c', 4);
_.set(object, ['x', '0', 'y', 'z'], 5);
and many functions have optional parameters. It freely mutates data supplied to it. It is designed to be about "providing quality utility methods to as many devs as possible with a focus on consistency, compatibility, customization, and performance." as its founder once said.
Ramda, in contrast, is much less worried about flexibility. A much more important goal for Ramda is simplicity. Its API has no optional parameters. When it allows multiple types for an argument, it is only because there is a higher abstraction that they share. The path supplied to assocPath
is an array, and only an array; it handles objects versus arrays by noting whether each path element is a string or an integer. And of course Ramda never mutates your input data.
Ramda also is not interested in hand-holding. The philosophy is often one of garbage in, garbage out. And this case seems to edge into that territory. Ramda will willingly tranform {moo: {goo: 'goo'}}
into {moo: {goo: {boo: 'roo'}}}
. But you have to tell it to do so more explicitly: assoc(['moo', 'goo'], {boo: 'roo'})
.
So a request to change this might be a difficult sell... but it's a friendly crowd. Feel free to bring it up there if you think it important.
I feel like I just have to import
set
function from lodash to prevent unexpected behavior.
Remember, though, just how different the behaviors are. The biggest difference is that lodash is mutating its input, which Ramda won't do. They have different ideas about which values to enhance versus which ones to replace (as in the current example.) Of course their signatures are different. They have different behavior regarding array indices. (I can't think of a way in lodash's set
to add an object with string key '0', for instance. And Ramda certainly would not inlcude a constructed Rectangle when you call something like assocPath(['grid', 'width'], newVal, myObj)
, whereas lodash would happily mutate the internal Rectangle object.)
In other words, they are different behaviors, designed for different purposes. If lodash's behavior is what you want, by all means include it. But if you're using Ramda for most of your utility work, do note how different the philosophies are.
Upvotes: 5