Map, flip(map) and partial application in JS
An in depth example of partially applying functions, composing map and inverting function arguments.
Introduction
Please see Partial Application and map post for an introduction. It’s relatively easy to get started with these core concepts. But the real power comes when composing them together. This post uses ramda and explores some of the ways functions can be extracted.
Scenario
I have a function, which I want to apply to each property in an array, nested within an object. In this scenario I am not interested in using lenses
to return an updated root object, I just want an array of items with my executed function.
const prependVal= val => 'val: ' + prop('name')(val)const item = {
subItems: [{
name: 'sub item one'
},{
name: 'sub item two'
}]
}
Output:
[{
name: 'val: sub item one'
},{
name: 'val: sub item two'
}]
Approach 1
This is a fairly easy problem to solve using a couple of standard functional programming functions.
compose(map(prependVal), propOr([], 'subItems'))(item)
Approach 2
But what if we wanted to extract it out into its own function, say applyFnToSubItems
. We could write something like this.
const applyFnToSubItems = fn => compose(
map(fn),
propOr([], 'subItems')
);//calling
applyFnToSubItems(prependVal)(item)
This is okay, and I like the way it is called, but I don’t like the fact that we have to scope the fn
variable and explicitly call it against map
.
Approach 3
It would be nice if we could have a function that looked more like this:
const applyFnToSubItems = compose(
map,
propOr([], 'subItems')
);//calling
applyFnToSubItems(prependVal)(item)
Although, this isn’t going to work due to the fact that the first function within compose
will get executed against the function that we want applied to each element in the array, (prependVal
).
What we want to do is partially apply the function we want executed, (prependVal
), against map
.
const applyFnToSubItems = compose(
map
);//calling
applyFnToSubItems(prependVal)(item) //output {"subItems": "val: undefined"}
map
is now a function expecting an array, but we are passing it the item
object. So we need to filter the second curried item
object to get out the subItems
. We were filtering the subItems
previously, so let’s just add that back to our compose
.
const applyFnToSubItems = compose(
propOr([], 'subItems') //isn't going to work
map
);
But this isn’t going to work as we are executing the propOr
function on what’s being returned from map
, which is a function, (as it has only been partially applied).
What we need is to scope the partially applied function, and return another function, that will get passed the item
object.
const applyFnToSubItems = compose(
partiallyAppliedMap =>
item =>
partiallyAppliedMap(propOr([], 'subItems')(item)),
map
);
This will work fine, but it’s not looking that elegant at the moment.
We could extract that additional functionality into its own function, which would also work.
const thing = fn => partiallyAppliedMap => item =>
partiallyAppliedMap(fn(item));const applyFnToSubItems = compose(
thing(propOr([], 'subItems')),
map
);
But instead of creating our own thing
function we can take advantage of map
.
Approach 4
Usually we think of map
to iterate over a list of values, and apply a function. But we don’t have to, map
can be applied to other things (functors), including other map
functions, meaning they can be chained.
map(i => {
console.log('1:', i);
return i;
})(i => {
console.log('2:', i);
return ({...i, a: 1});
})(item);//output
2:
{"subItems":[{"name":"sub item one"},{"name":"sub item two"}]}
1:
{"subItems":[{"name":"sub item one"},{"name":"sub item two"}],"a":1}{"a": 1, "subItems": [{"name": "sub item one"}, {"name": "sub item two"}]}
We can think of our example as having 3 inputs:
- The
item
object - The
prependVal
function - The
propOr([], ‘subItems’)
function
So maybe we could do something like this:
map(prependVal)(propOr([], 'subItems'))(item)
But this will error given the the final map
call will pass an array, and prependVal
does not accept an array, it accepts objects of the array. Meaning that we have to map
over the array.
map(map(prependVal))(propOr([], 'subItems'))(item)
That’s fine, but applying this back to our example, the function calls are the incorrect way around. It needs to look something more like this:
map(propOr([], 'subItems'))(map(prependVal))(item)
But this just isn’t going to work, we need to flip
the map
function so that the functions can be added in this order. For this we can use flip
. By executing the flipped map
function, we accept the second argument first, and the first argument second.
flip(map)(propOr([], 'subItems'))(map(prependVal))(item)
Okay that now works again. But how can we apply this to our applyFnToSubItems
function? Let’s start by pulling bits out into our compose
function. First we’ll move flip(map)(propOr([], ‘subItems’))
const applyFnToSubItems = compose(
flip(map)(propOr([], 'subItems'))
);//calling
applyFnToSubItems(map(prependVal))(item)
And then that final map
const applyFnToSubItems = compose(
flip(map)(propOr([], 'subItems')),
map
);//calling
applyFnToSubItems(prependVal)(item)//output
[{
name: 'val: sub item one'
},{
name: 'val: sub item two'
}]
This final example looks very similar to when we introduced our thing
function in Approach 3, and it is essentially doing the same thing as flip(map)
in this example.
Conclusion
For me Approach 4 is the most elegant. But I definitely think that attacking tasks like this in a couple of different ways, helps to improve knowledge and cement understanding.