Practical introduction to Functional Programming with JS

AndreaZanin
3,305 views

Open Source Your Knowledge, Become a Contributor

Technology knowledge has to be shared and made accessible for free. Join the movement.

Create Content

Functors

Remember when we talked about map? We only used map on arrays, but you can actually implement map on other data structures, e.g. trees, streams, promises...

Any type that has a map function is a functor.

You should have understood the concept of map by now, but if you a real nitpicker, you will now ask "How do we define map?". One of the best definitions is given by haskell's functor laws; for those of you who are interested, I will translate them into JavaScript:

// identity
myFunctor.map( x => x) === myFunctor

// composition === chaining
myFunctor.map( x => f(g(x)) ) === myFunctor.map(g).map(f)

In the code above, I used === to keep it simple, but I should have checked for deep equality. If you don't know what this means, don't worry, you should be able to understand the meaning anyway. Nonetheless, if you want to learn more about it, this StackOverflow thread is a good place to start.

The advantage of using a functor is that the container is now abstracted away. E.g., in streams, we don't need to care about asynchronous data handling, we can just use a stream like an array.

Another cool feature of functors is that you can chain map calls, because the map function returns another functor.

Chaining
1
2
3
4
5
6
7
8
9
10
const arr = [1,2,3,4]
// ADD 2 THEN DOUBLE
// WITHOUT CHAINING
const arr2 = arr.map( x => x + 2)
const final = arr2.map( x => 2 * x)
// WITH CHAINING
const chained = arr.map( x => x + 2).map( x => 2 * x)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

So why don't we create our own functor?

The Tuple Functor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const newTuple = (a,b) => {
let tuple = {a: a, b: b}
tuple.map = callback => newTuple(callback(tuple.a),callback(tuple.b))
return tuple
}
const tuple = newTuple(4,3)
const tupleIdentity = tuple.map( x => x )
console.log("IDENTITY\n", JSON.stringify(tuple), "\n", JSON.stringify(tupleIdentity), "\n")
const double = x => x*2
const increase = x => x+1
const tupleComposedFunctions = tuple.map( x => double(increase(x)) )
const tupleChainedMaps = tuple.map(increase).map(double)
console.log("Composition === Chaining\n", JSON.stringify(tupleComposedFunctions), "\n", JSON.stringify(tupleChainedMaps))
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Monads

Giving a definition of monad is somewhat tedious and requires a bit of theory, so we are first going to build an intuition for them through examples.

Array Monad

Let's write a function that duplicates every item in an array (e.g. [1,2,3] --> [1,1,2,2,3,3]).
Let's first try with map:

map duplicate
1
2
3
const duplicate = x => [x,x]
const arr = [1,2,3]
console.log(arr.map(duplicate))
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Running the code, you can see that the result is not quite what we wanted: [[1,1],[2,2],[3,3]]. Wouldn't it be great if we had a function that automatically "flattened" the array we returned into [1,1,2,2,3,3]? That's flatMap!

flatMap duplicate
1
2
3
const duplicate = x => [x,x]
const arr = [1,2,3,4]
console.log(arr.flatMap(duplicate));
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The flatMap function was implemented by me, because it's not provided by javascript yet; we'll deal with the details later.

As you can see, flatMap's callback returns another monad and flatMap's job is handling the unpacking. If you are interested in the details of the implementation, here's my code:

flatMap implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function flatMap(callback) {
let newArr = []
// the flatMap function builds the new arr by
// iterating through the original array and
// concatenating the result of applying
// the callback function on every item
this.forEach(x => {
newArr = newArr.concat(callback(x))
})
return newArr
}
Array.prototype.flatMap = flatMap; // we add flatMap as a function available to the array prototype
// example
const duplicate = x => [x,x]
const arr = [1,2,3,4]
console.log(arr.flatMap(duplicate));
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Although the array's implementation of flatMap is very interesting and useful, it's not the only one: we can, for example, use flatMap to maintain knowledge of previous states of a system. As always, let's start with an example.

Gambler Monad

Let's say we want to create a model for a gambler: the gambler plays multiple times at the roulette; sometimes he wins, sometimes he loses. But if he goes bankrupt, he gets kicked out of the casino and cannot recover. We want to simulate a number of games and see if at the end the gambler still has money or not.

We cannot simply add up wins and losses, because we would ignore intermediate states. The following is a wrong solution:

map implementation
1
2
3
4
5
6
7
8
const initialMoney = 1000
const transactions = [500,-2000,+1000]
const final = transactions.reduce( (x,y) => x+y , initialMoney)
console.log(final)
// Summing we ignore the third state in which he is bankrupt
// $1000 --> $1500 --> $-500 --> $500
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

What we need is a model that stops computing the transactions after he went bankrupt, like the following:

map implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const newGambler = type =>{
let gambler = {}
gambler.flatMap = callback =>
gambler.type === "Playing" ? // is the gambler still playing?
callback(gambler.money) : // if yes apply the transaction
newGambler("Bankrupt") // otherwise return a bankrupt gambler
if (type === "Bankrupt") { // if we initialize a bankrupt gambler we don't need other parameters
gambler.type = "Bankrupt"
return gambler
} else { // if we initialize a playing gambler we need to know how much money he has so we return another function
return money => {
gambler.type = "Playing"
gambler.money = money
return gambler
}
}
}
const transaction = amount => // a transaction function getting the transaction and returning a new gambler
gamblersMoney =>
gamblersMoney + amount > 0 ? // does the gambler still have money after the transaction?
newGambler("Playing")(gamblersMoney + amount) : // if so return a new gambler with the new money
newGambler("Bankrupt") // otherwise return a bankrupt gambler
let gambler = newGambler("Playing")(1000) // create a new Gambler with $1000
gambler = gambler.flatMap(transaction(500)).flatMap(transaction(-2000)).flatMap(transaction(1000)) // applies all the transactions
console.log(gambler) // returns correctly a bankrupt gambler
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

This time, we used flatMap to remember past states, but again the callback returned a new monad and flatMap handled the unwrapping of the original monad content.

Wrapping up

Monads are a broad definition, but they all share one characteristic: they implement flatMap, which in term is useful to maintain knowledge of the context in which a function is being executed.

Open Source Your Knowledge: become a Contributor and help others learn. Create New Content