GavinoGrifoni
GavinoGrifoni

Reputation: 763

Arrays: conditionally mapping elements

I work with Redux and it often happens to me to write reducers with expressions like this one:

return users.map(user =>
  user.id !== selectedUserID ? user : {
    ... user,
    removed: false
  }
);

The intent should be clear enough: modify just the item in the array that has the given id, while copying the others as they are. Also note that ordering is important, otherwise I could just have used a filter() function.

This snippet triggers a no-confusing-arrow error on eslint, which makes sense to me. This can also be easily solved by adding round parenthesis around the arrow function body, so no big deal here:

return users.map(user => (
  user.id !== selectedUserID ? user : {
    ... user,
    removed: false
  }
));

I also want to parse this code through prettier, which automatically removes the parenthesis around the arrow function body, going back to version 1 of the snippet.

The obvious solution here is to write the arrow function the verbose way:

return users.map(user => {
  if(user.id !== selectedUserID) {
    return user; // unmodified user
  }
  return {
    ... user,
    removed: false
  };
});

but I honestly find it a bit too clumsy.

Aside from the specific context and the used tools (eslint and prettier, which can be configured differently/turned off/whatever), is there any better way to write this?

In my wildest dreams it exists a function with a signature similar to:

Array.mapIf(callback, condition)

that cycles all the elements in the array, and calls the callback function only to the ones satisfying the given condition, while returning the other elements unmodified.

I could obviously write a function like this myself, but maybe there is something already existing in other functional languages that may be worth to look at for general culture/inspiration.

Upvotes: 4

Views: 173

Answers (4)

Mulan
Mulan

Reputation: 135227

I'll provide this as a supplement to @ftor's practical answer – this snippet shows how generic functions can be used to accomplish kind of result

const identity = x =>
  x

const comp = ( f, g ) =>
  x => f ( g ( x ) )
  
const compose = ( ...fs ) =>
  fs.reduce ( comp, identity )
  
const append = ( xs, x ) =>
  xs.concat ( [ x ] )

const transduce = ( ...ts ) => xs =>
  xs.reduce ( compose ( ...ts ) ( append ), [] )

const when = f =>
  k => ( acc, x ) => f ( x ) ? k ( acc, x ) : append ( acc, x )

const mapper = f =>
  k => ( acc, x ) => k ( acc, f ( x ) )

const data0 =
  [ { id: 1, remove: true }
  , { id: 2, remove: true }
  , { id: 3, remove: true }
  ]

const data1 =
  transduce ( when ( x => x.id === 2)
            , mapper ( x => ( { ...x, remove: false } ) )
            )
            (data0)
            
console.log (data1)
// [ { id: 1, remove: true }
// , { id: 2, remove: false }
// , { id: 3, remove: true }
// ]

However, I think we could improve when with a more generic behavior, all we need is a little help from some invented types Left and Right – expand complete code snippet after this excerpt

const Left = value =>
  ({ fold: ( f, _ ) =>
       f ( value )
  })

const Right = value =>
  ({ fold: ( _, f ) =>
       f ( value )
  })

// a more generic when
const when = f =>
  k => ( acc, x ) => f ( x ) ? k ( acc, Right ( x ) ) : k ( acc, Left ( x ) )

// mapping over Left/Right
mapper ( m =>
  m.fold ( identity                         // Left branch
         , x => ( { ...x, remove: false } ) // Right branch
         ) ) 

const Left = value =>
  ({ fold: ( f, _ ) =>
       f ( value )
  })
  
const Right = value =>
  ({ fold: ( _, f ) =>
       f ( value )
  })

const identity = x =>
  x

const comp = ( f, g ) =>
  x => f ( g ( x ) )
  
const compose = ( ...fs ) =>
  fs.reduce ( comp, identity )
  
const append = ( xs, x ) =>
  xs.concat ( [ x ] )

const transduce = ( ...ts ) => xs =>
  xs.reduce ( compose ( ...ts ) ( append ), [] )

const when = f =>
  k => ( acc, x ) => f ( x ) ? k ( acc, Right ( x ) ) : k ( acc, Left ( x ) )

const mapper = f =>
  k => ( acc, x ) => k ( acc, f ( x ) )

const data0 =
  [ { id: 1, remove: true }
  , { id: 2, remove: true }
  , { id: 3, remove: true }
  ]

const data1 =
  transduce ( when ( x => x.id === 2 )
            , mapper ( m =>
               m.fold ( identity
                      , x => ( { ...x, remove: false } ) 
                      ) )
            )
            (data0)
            
console.log (data1)
// [ { id: 1, remove: true }
// , { id: 2, remove: false }
// , { id: 3, remove: true }
// ]

So your final function would probably look something like this

const updateWhen = ( f, records, xs ) =>
  transduce ( when ( f )
            , mapper ( m =>
                m.fold ( identity
                       , x => ( { ...x, ...records } )
                       ) )
            ) ( xs )

You'll call it in your reducer like this

const BakeryReducer = ( donuts = initState , action ) =>
  {
    switch ( action.type ) {
      case ApplyGlaze:
        return updateWhen ( donut => donut.id === action.donutId )
                          , { glaze: action.glaze }
                          , donuts
                          )
      // ...
      default:
        return donuts
    }
  } 

Upvotes: 0

user6445533
user6445533

Reputation:

There is no such native function because you can easily implement it yourself:

const mapWhen = (p, f) => xs => xs.map(x => p(x) ? f(x) : x);

const users = [
  {id: 1, removed: true},
  {id: 2, removed: true},
  {id: 3, removed: true}
];

console.log(
  mapWhen(
    ({id}) => id === 2,
    user => Object.assign({}, user, {removed: false})
  ) (users)
);

I've chosen mapWhen as the name instead of mapIf, because the latter would imply that there is an else branch.

With a mature functional language you would probably solve this issue with a functional lense. I think, however, mapWhen is sufficient for your case and more idiomatic.

Upvotes: 3

Aaron
Aaron

Reputation: 145

In ruby you would return something like

users.dup.select {|u| u.id == selected_user_id }.each {|u| u.removed = false }

dup is important in this case because in Redux you want to return a new array and not modify the original one, so you first have to create a copy of the original. See https://stackoverflow.com/a/625746/1627766 (notice the discussion is very similar to this one)

In your case I would use:

const users = [
  {id: 1, removed: true},
  {id: 2, removed: true},
  {id: 3, removed: true}
];

const selectedUserId = 2;

console.log(
  users.map(
    (user) => ({
      ...user,
      removed: (user.id !== selectedUserId ? false : user.removed)
    })
  )
);

Upvotes: 2

Nina Scholz
Nina Scholz

Reputation: 386620

You could use Object.assign which add the property if necessary.

return users.map(user =>
    Object.assign({}, user, user.id === selectedUserID && { removed: false })
);

Upvotes: 0

Related Questions