Reputation: 12263
I am trying to learn how to use applicative functors in javascript and came across the ap
method. I am trying to use it to combine three arrays like so:
const products = ['teeshirt', 'sweater']
const options = ['large', 'medium', 'small']
const colors = ['red', 'black']
So as per the documentation I am trying out this:
const buildMerch = product => option => color =>`${product}-${option}-${color}`
const merchandise = R.ap([buildMerch], [products, options, colors])
But this is giving me back three functions:
[function (option) {
return function (color) {
return product + '-' + option + '-' + color;
};
}, function (option) {
return function (color) {
return product + '-' + option + '-' + color;
};
}, function (option) {
return function (color) {
return product + '-' + option + '-' + color;
};
}]
...instead of the combined result of the arrays that I expect:
["teeshirt- large-red", "teeshirt- large-black", "teeshirt- medium-red", "teeshirt- medium-black", "teeshirt- small-red", "teeshirt- small-black", "sweater- large-red", "sweater- large-black", "sweater- medium-red", "sweater- medium-black", "sweater- small-red", "sweater- small-black"]
What am I doing wrong? How do I fix this?
Here is a jsbin with the issue: https://jsbin.com/fohuriy/14/edit?js,console
Upvotes: 3
Views: 357
Reputation: 1074
Another way to solve this problem is to add ap
to the native javascript Array. This way you're essentially turning Array into an applicative functor, you don't need a library, and it works with the same interface as any other applicative functor you may want to use.
// add ap
Array.prototype.ap = function(anotherArray) {
return this.map(el =>
anotherArray.map(el)
).flatten();
};
This relies on flatten (or 'join').
// add flatten
Array.prototype.flatten = function() {
let results = [];
this.forEach(subArray => {
subArray.forEach(item => {
results.push(item);
});
});
return results;
};
Now:
const products = ['teeshirt', 'sweater'];
const options = ['large', 'medium', 'small'];
const colors = ['red', 'black'];
const buildMerch = product => option => color =>`${product}-${option}-${color}`;
const merchandise = products.map(buildMerch).ap(options).ap(colors);
Now you could also Lift all three:
const liftA3 = (fn, functor1, functor2, functor3) =>
functor1.map(fn).ap(functor2).ap(functor3);
liftA3(buildMerch, products, options, colors) // also returns merchandise
Upvotes: 2
Reputation: 6982
AP applies a list of functions to a list of values. In your case, you will invoke buildMerch
with every element in your specified array, which is products
, then options
and after that with colors
, and not with every combination within your arrays. This does not match to your method signature, where you expect three arguments.
Upvotes: 1
Reputation: 2697
ap
applies a list of functions to a list of values, per the documentation. Your function buildMerch
has the following "type":
buildMerch :: String -> String -> String -> String
The simplest kind of ap
is map
: for any applicative functor, we get:
pure f <*> a
======
map f a
For arrays, pure
is x => [x]
. So,
R.ap([buildMerch], [foo, bar, baz])
======
R.map(buildMerch, [foo, bar, baz])
By mapping buildMerch
over the argument list, we are partially applying it to the arrays in question. The expression that does what you want is:
const merchandise = R.ap(R.ap(R.map(buildMerch, products), options), colors);
First, we map buildMerch
over the product array. This gives us an array of functions taking two arguments: [String -> String -> String]
. Then, we use R.ap
to combine that with options :: [String]
, which applies each function in the first array with each argument in the options
array. Now we have [String -> String]
, and finally we R.ap
that with colors
to get the final array of strings that you want.
Upvotes: 3