简体   繁体   中英

How to imitate lodash.set with Ramda

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?

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 founderonce 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM