Reputation: 763
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
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
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
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
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