What is fully ramda.js equivalent of this code?

Viewed 70

i need create function that updates item with certain id in list by merging payload object

in ramda.js way

export const updateItem = <T extends { id: I }, I>(items: T[], id: I, payload: Partial<T>) =>
  items.map(
    item => item.id === id
      ? ({ ...item, ...payload })
      : item
  )
2 Answers

We can certainly write a fairly readable version in Ramda:

const updateItem = (id, payload) => map (
  when (propEq ('id', id), mergeLeft (payload))
)


const items = [{foo: 'a', id: 1}, {foo: 'b', id: 2}, {foo: 'c', id: 1}, {foo: 'd', id: 4}]

console .log (updateItem (1, {bar: 'x'}) (items)) // updates all with id of 1
console .log (updateItem (2, {bar: 'x'}) (items))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {map, when, propEq, mergeLeft} = R                        </script>

We can read this as map over our input a function that when the id property equals (propEq) the id supplied, perform a mergeLeft on the payload and the current value. when has an implicit understanding that if the condition doesn't match, it returns the value intact.

If you want an entirely point-free version of the initial approach, we could use the somewhat obscure useWith, like this:

const updateItems = compose (map, useWith (when, [propEq ('id'), mergeLeft]))

I would probably prefer to write that like this, though:

const updateItem = useWith (when, [propEq ('id'), mergeLeft])
const updateItems = compose (map, updateItem)

If you want to supply all arguments together, we could do this:

const updateItem = (id, payload, xs) => 
  map (when (propEq ('id', id), mergeLeft (payload)), xs)
// ...
updateItem (1, {bar: 'x'}, items)

OOPS!: The following is completely wrong; see the comment on another answer

All of these make the assumption that we are testing every element. It does not assume that the id is unique. If you want to do that, the answer from Chad S. would work, but I would prefer to do it slightly differently:

// ***********************************************
// * NOTE: This is entirely broken.  Do not use. *
// ***********************************************

const updateItems = (id, payload, xs) => 
  adjust (findIndex (propEq ('id', id), xs), mergeLeft (payload), xs)


const items = [{foo: 'a', id: 1}, {foo: 'b', id: 2}, {foo: 'c', id: 1}, {foo: 'd', id: 4}]

console .log (updateItems (1, {bar: 'x'}, items)) // only the first one.
console .log (updateItems (2, {bar: 'x'}, items))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {adjust, findIndex, propEq, mergeLeft} = R                </script>

We could also move toward point-free on this version, but I think it would end up pretty ugly. We could make xs implicit trivially, but anything else would make it less readable to my mind.

Update

After that poor answer, I thought I owed an attempt at creating an only-the-first-match solution that actually works. I personally wouldn't try to do this nearly so close to point-free, but here's one appoach:

const updateItems = (id, payload) =>  chain (
  ifElse (equals (-1), nthArg (1), flip (adjust) (mergeLeft (payload))),
  findIndex (propEq ('id', id))
)

const items = [{foo: 'a', id: 1}, {foo: 'b', id: 2}, {foo: 'c', id: 1}, {foo: 'd', id: 4}]

console .log (updateItems (1, {bar: 'x'}) (items)) // only the first of multiple matches.
console .log (updateItems (2, {bar: 'x'}) (items)) // one match
console .log (updateItems (7, {bar: 'x'}) (items)) // no matches
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {chain, ifElse, equals, nthArg, flip, adjust, mergeLeft, findIndex, propEq} = R </script>

I would still choose the version at the top unless I found actual performance problems with it.

So first we want to find the index of the item we want to change:

R.findIndex(R.propEq('id', id))

If that's -1 then we don't have to do anything (return the items). Otherwise we have to adjust the items by merging the payload into the item at that index.

R.pipe(
  R.findIndex(R.propEq('id', id)), 
  R.ifElse(
    R.equals(-1), 
    R.always(R.identity), 
    R.adjust(R.__, R.mergeLeft(payload))
  )
)

Finally we just apply items to that pipe, and then apply items again to the result of the pipe:

const updateItem = R.curry((id, payload, items) => {
  return R.converge(R.applyTo, [
    R.identity,
    R.pipe(
      R.findIndex(R.propEq('id', id)), 
      R.ifElse(
        R.equals(-1), 
        R.always(R.identity), 
        R.adjust(R.__, R.mergeLeft(payload))
      )
    )
  ])(items)
});
Related