Amit Erandole
Amit Erandole

Reputation: 12263

What is my applicative functor not working with Ramda's ap?

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

Answers (3)

MFave
MFave

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

Martin Seeler
Martin Seeler

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

ephrion
ephrion

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

Related Questions