7

I'm trying to find an equivalent function to Lodash's merge using Ramda that does a recursive object key-based "merge" or "extend". The behavior is similar to the following:

let merged = R.someMethod(
  { name: 'Matt', address: { street: 'Hawthorne', number: 22, suffix: 'Ave' }},
  { address: { street: 'Pine', number: 33 }}
);

console.log(merged);

// => { name: 'Matt', address: { street: 'Pine', number: 33, suffix: 'Ave' }}

I noticed in the following pull request that R.set was briefly introduced, but then rolled back soon thereafter. Has this functionality been captured by the Ramda library since?

Is this functionality available in Ramda?

Himmel
  • 3,629
  • 6
  • 40
  • 77

5 Answers5

10

A relatively simple recursive function can be created using R.mergeWith.

function deepMerge(a, b) {
  return (R.is(Object, a) && R.is(Object, b)) ? R.mergeWith(deepMerge, a, b) : b;
}

deepMerge({ name: 'Matt', address: { street: 'Hawthorne', number: 22, suffix: 'Ave' }},
          { address: { street: 'Pine', number: 33 }});

//=> {"address": {"number": 33, "street": "Pine", "suffix": "Ave"}, "name": "Matt"}
Scott Christopher
  • 6,458
  • 23
  • 26
  • I suppose this depends on how you would like such a function to work, but I prefer any key/value passed as the second argument to overwrite a matching key/value passed as the first argument, despite the value's type. Such functionality is captured by `(R.is(Object, b)) ? ...` rather than `(R.is(Object,a)) ?...`. – Himmel Jul 01 '16 at 23:18
  • 1
    If you're potentially dealing with different types at the same key then you should check that both `a` and `b` are objects before calling `mergeWith`. I've updated the example to also reflect that. – Scott Christopher Jul 02 '16 at 00:32
  • Slightly modified version to work with simple arrays ```const deepMerge = (a, b) => R.isArrayLike(b) && !R.is(Object, b[0]) ? b : (R.is(Object, a) && R.is(Object, b)) ? R.mergeWith(deepMerge, a, b) : b``` – baio Feb 11 '17 at 18:10
9

Ramda does not include such a function at the moment.

There have been several attempts to create one, but they seem to founder on the notion of what's really required of such a function.

Feel free to raise an issue if you think it's worth adding.

Update

(Two years later.) This was eventually added, in the form of several functions: mergeDeepLeft, mergeDeepRight, mergeDeepWith, and mergeDeepWithKey.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • I think it's worth creating an issue, but I'd hate to rehash the same old bits about how to deal with undefined values, merging arrays, etc. I don't really bring any nuanced opinions to the table, and I'm not sure the desire to have the functionality itself is speaking very loudly at this point... – Himmel Jul 01 '16 at 03:37
  • As you like, of course. I think there is an ongoing, but fairly low pressure to add such a function. But many, including me, have resisted, because no API seems fantastic, especially in the face of prototype chains, `undefined`, and cyclic structures. But it's probably a case of the perfect being the enemy of the good. – Scott Sauyet Jul 03 '16 at 03:50
3

Ramda now has several merge functions: mergeDeepLeft, mergeDeepRight, mergeDeepWith, mergeDeepWithKey.

Microcipcip
  • 655
  • 9
  • 21
1

const { unapply, mergeDeepRight, reduce } = R
const mergeDeepRightAll = unapply(reduce(mergeDeepRight, {}))

console.log(mergeDeepRightAll({a:1, b: {c: 1}},{a:2, d: {f: 2}},{a:3, b: {c:3}}))
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
cstuncsik
  • 2,698
  • 2
  • 16
  • 20
1

from scratch

Newer functions in the Ramba library mean you don't have to do this on your own, but what if the maintainers never go around to it? You don't want to be stuck waiting on someone else to write your code when you need a feature or behavior right now.

Below, we implement our own recursive merge

const isObject = x =>
  Object (x) === x

const merge = (left = {}, right = {}) =>
  Object.entries (right)
    .reduce
      ( (acc, [ k, v ]) =>
          isObject (v) && isObject (left [k])
            ? { ...acc, [k]: merge (left [k], v) }
            : { ...acc, [k]: v }
      , left
      )

Our merge function also works generically and accepts any two objects as input.

const x =
  { a: 1, b: 1, c: 1 }

const y =
  { b: 2, d: 2 }

console.log (merge (x, y))
// { a: 1, b: 2, c: 1, d: 2 }

In the event each object contains a property whose value is also an object, merge will recur and merge the nested objects as well.

const x =
  { a: { b: { c: 1, d: 1 } } }

const y =
  { a: { b: { c: 2, e: 2 } }, f: 2 }

console.log (merge (x, y))
// { a: { b: { c: 2, d: 1, e: 2 } }, f: 2 }

arrays are people too

To support arrays in merge, we introduce a mutation helper mut which assigns a [ key, value ] pair to a given object, o. Arrays are considered objects too, so we can update both arrays and objects using the same mut function

Note, Ramda's merging functions do not attempt to merge arrays. The primary advantage to writing your own functions is you can easily augment their behavior to meet your program's ever-evolving requirements.

const mut = (o, [ k, v ]) =>
  (o [k] = v, o)

const merge = (left = {}, right = {}) =>
  Object.entries (right)
    .map
      ( ([ k, v ]) =>
          isObject (v) && isObject (left [k])
            ? [ k, merge (left [k], v) ]
            : [ k, v ]
      )
    .reduce (mut, left)

Shallow merges work as expected

const x =
  [ 1, 2, 3, 4, 5 ]

const y =
  [ 0, 0, 0 ]

const z =
  [ , , , , , 6 ]

console.log (merge (x, y))
// [ 0, 0, 0, 4, 5 ]

console.log (merge (y, z))
// [ 0, 0, 0, <2 empty items>, 6 ]

console.log (merge (x, z))
// [ 1, 2, 3, 4, 5, 6 ]

And deep merges too

const x =
  { a: [ { b: 1 }, { c: 1 } ] }

const y =
  { a: [ { d: 2 }, { c: 2 }, { e: 2 } ] }

console.log (merge (x, y))
// { a: [ { b: 1, d: 2 }, { c: 2 }, { e: 2 } ] }

variadic merge

Maybe we want a merge function that is not limited to two inputs; mergeAll

const Empty =
  {}

const mergeAll = (first = Empty, ...rest) =>
  first === Empty
    ? first
    : merge (first, mergeAll (...rest))

mergeAll ({ a: 1 }, { b: 2 }, { c: 3 })
// { a: 1, b: 2, c: 3 }

This answer is an excerpt from another question: How to compare two objects and get key-value pairs of their differences?

Mulan
  • 129,518
  • 31
  • 228
  • 259