-1

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
  )
RomanGodMode
  • 325
  • 3
  • 11
  • What's your best attempt so far? Also, what do you mean by "in (the) ramda.js way"? Are you looking for an entirely point-free solution, or do you just want, for some reason, to use Ramda functions in place of native ones? – Scott Sauyet Sep 19 '22 at 12:13
  • @ScottSauyet yes, maybe ramda js has function that apply func to list element that matches condition – RomanGodMode Sep 19 '22 at 14:09
  • Have you tried this yourself? What didn't work? – Scott Sauyet Sep 19 '22 at 14:36

2 Answers2

2

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.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • Fascinating! I've tried in the past to get rid of arguments and did not actually consider `useWith`. This is eye opening :D – VLAZ Sep 21 '22 at 14:11
  • @VLAZ: I generally find `useWith` and -- to a lesser extent -- `converge` to be trying too hard. If it doesn't come naturally as point-free, then don't bother. But they have their uses, and `lift` feels perfectly fine to me. Don't ask me to explain these feelings, though; it's pretty fuzzy. – Scott Sauyet Sep 21 '22 at 14:32
1

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)
});
Chad S.
  • 6,252
  • 15
  • 25
  • 1
    Two things: First, `when` and `unless` are good alternatives to `ifElse` when you want to perform some action on one branch but just return the input value intact otherwise. Second, you don't really even need to branch here, since `adjust` is a no-op when supplied an index not in the array, such as `-1`. See my answer for a simpler version. – Scott Sauyet Sep 21 '22 at 14:00
  • 1
    You misunderstood why I used `R.always(R.identity)`. R.adjust WILL work just fine on -1. It will adjust the last item in the list which is not the one you want to adjust. I didn't use R.when because I wanted to RETURN R.identity (not call it). – Chad S. Sep 22 '22 at 15:56
  • 1
    Wow, one of the founders of Ramda, and I misremembered that fact about `adjust`. I think I'll have to go, um, *adjust* my own answer! – Scott Sauyet Sep 22 '22 at 16:08
  • No worries. It's hard to keep track of the specific details for such a large collection of functions. :) – Chad S. Sep 22 '22 at 17:13