4

What is the difference between chain() (from ramda package) and map() in Javascript?

In both functions the programmer inputs an object and some lambda/function and gets a certain calculation of it. Thanks.

James Conkling
  • 3,235
  • 2
  • 25
  • 37
J. Doe
  • 95
  • 8

2 Answers2

13

Abstract Types

chain and map each operate on an abstract type. map operates on any Functor. This is any item with a map function that obeys certain laws. chain operates on a Chain element. Similarly, this is something with a lawful chain function, as well as having lawful apply and map functions.

Ramda provides map and chain functions that will work with types fulfilling these contracts. It also supplies implementation for certain built-in types (functions, arrays, and objects for map and functions and arrays for chain.)

To see how they differ, it's simple enough to compare their signatures:

// map   :: Functor f => (a → b)   → f a → f b
// chain :: Chain   m => (a → m b) → m a → m b

You can think about it like this: the function supplied to map takes an item of type A and returns one of type B. map accepts that function and a container holding type A and returns a container holding type B. The function supplied to chain by contrast takes an item of type A and returns a container holding type B. chain accepts that function and a container holding type A, returning a container holding type B.

You can think about it as though chain unwraps one level of containers compared to map.

Some Examples

For example, let's say we had a function factors, which returns the factors of an integer (factors(14) //=> [1, 2, 7, 14], for instance.) Here is how map and chain would work on a list of numbers:

map(factors, [12, 15])   //=> [[1, 2, 3, 4, 6, 12], [1, 3, 5, 15]]
chain(factors, [12, 15]) //=> [1, 2, 3, 4, 6, 12, 1, 3, 5, 15]

Or if we had a Maybe type used to simplify null-handling with the subtypes Just to signify a value and Nothing to signify some null in the computation. We might write a safe square root function such as

const sqrt = (n) => n > 0 ? Just(Math.sqrt(n)) : Nothing()

Then we see these differences between map and chain.

map(sqrt, Just(25)) //=> Just(Just(5))
chain(sqrt, Just(25)) //=> Just(5)

map(sqrt, Just(-25)) //=> Just(Nothing)
chain(sqrt, Just(-25)) //=> Nothing

And finally, for functions, for reasons described in another SO answer,

map(f, g)   //~> x => f(g(x));
chain(f, g) //~> x => f(g(x))(x); 

Summary

You can see from their signatures, that there is some relationship between map and chain, but they are distinct functions, used for very different purposes. chain is related to what is sometimes called flatMap, as it flattens out (one level) of the sort of result created by map.

But the best way to think about them is to look at the signatures and the laws tied to these functions.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
2

Update August 2019

Agree entirely with Scott's answer ;) Wished to add a simple example by way of comparison. Let’s say you had a collection of friends who all had several favourite foods. We can use chain (aka flatMap) to create an array of all the foods you should serve at your next party.

import * as R from "ramda";

const friends = [
  {
    name: "Jill",
    favoriteFoods: ["pizza", "hummus"]
  },
  {
    name: "Bob",
    favoriteFoods: ["ice cream"]
  },
  {
    name: "Alice",
    favoriteFoods: ["chicken wings", "salmon"]
  }
];

let favouriteFoods = R.map(friend => friend.favoriteFoods);

console.log(favouriteFoods(friends));
//-> [ [ 'pizza', 'hummus' ], [ 'ice cream' ], [ 'chicken wings', 'salmon' ] ]



favouriteFoods = R.chain(friend => friend.favoriteFoods);

console.log(favouriteFoods(friends));
//-> [ 'pizza', 'hummus', 'ice cream', 'chicken wings', 'salmon' ]

A common pattern is to combine a flattening function with a mapping one. You could explicitly map then flatten in 2 separate steps. Chain, however, does both for you.

arcseldon
  • 35,523
  • 17
  • 121
  • 125