Functional JavaScript - Functors, Monads, and Promises

JavaScript Joel - Nov 6 '18 - - Dev Community

Person holding a box wrapped in ribbon

Some people have said a Promise is a Monad. Others have said a Promise is not a Monad. They are both wrong... and they are both right.

By the time you finish reading this article, you will understand what a Functor and Monad are and how they are similar and different from a Promise.

Why can't anyone explain a Monad?

It is difficult to explain what a Monad is without also having the prerequisite vocabulary also required to understand it.

I love this video with Richard Feynman when he is asked to describe "what is going on" between two magnets.

The whole video is amazing and mind blowing, but you can skip straight to 6:09 if you have some sort of aversion to learning.

I can't explain that attraction in terms of anything else that's familiar to you - Richard Feynman @ 6:09

So let's backup a few steps and learn the vocabulary required to understand what a Monad is.

Are we ready to understand a Functor?

Definition: A Functor is something that is Mappable or something that can be mapped between objects in a Category.

Okay... Not yet. But do not be afraid, you are already familiar with Functors if you have used Array's map function.

[1, 2, 3].map(x => x * 2) //=> [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

Before we can fully understand a Functor, we also have to understand what it means to be Mappable and to understand that we also have to understand what a Category is. So let's begin there.

Categories, Object and Maps (Morphisms)

Category theory triangle

A category consists of a collection of nodes (objects) and morphisms (functions). An object could be numbers, strings, urls, customers, or any other way you wish to organize like-things. (X, Y, and Z in the graphic are the objects.)

A map is a function to convert something from one object to another. (f, g, and fog are the maps). 🔍 Google tip: A map between objects is called a Morphism.

Example: An object in the object Number Type can be converted into the object String Type using the toString() method.

// A map of Number -> String
const numberToString = num => num.toString()
Enter fullscreen mode Exit fullscreen mode

You can also create maps back into their own objects or more complex object types.

// A map of Number -> Number
const double = num => num * 2

// A map of Array -> Number
const arrayToLength = array => array.length

// A map of URL -> Promise (JSON)
const urlToJson = url =>
  fetch(url)
    .then(response => response.json())
Enter fullscreen mode Exit fullscreen mode

So an object could be simple like a Number or a String. An object could also be more abstract like a Username, A User API URL, User API HTTP Request, User API Response, User API Response JSON. Then we can create maps or morphisms between each object to get the data we want.

Examples of morphisms:

  • Username -> User API Url
  • User API Url -> User API HTTP Request
  • User API HTTP Request -> User API Response
  • User API Response -> User API Response JSON

🔍 Google tip: Function Composition is a way to combining multiple map or morphisms to create new maps. Using Function Composition we could create a map from Username directly to User API Response JSON

Back to the Functor

Now that we understand what it means to be Mappable, we can finally understand what a Functor is.

A Functor is something that is Mappable or something that can be mapped between objects in a Category.

An Array is Mappable, so it is a Functor. In this example I am taking an Array of Numbers and morphing it into an Array of Strings.

const numberToString = num => num.toString()

const array = [1, 2, 3]
array.map(numberToString)
//=> ["1", "2", "3"]
Enter fullscreen mode Exit fullscreen mode

Note: One of the properties of a Functor is that they always stay that same type of Functor. You can morph an Array containing Strings to Numbers or any other object, but the map will ensure that it will always be an Array. You cannot map an Array of Number to just a Number.

We can extend this Mappable usefulness to other objects too! Let's take this simple example of a Thing.

const Thing = value => ({
  value
})
Enter fullscreen mode Exit fullscreen mode

If we wanted to make Thing mappable in the same way that Array is mappable, all we have to do is give it a map function.

const Thing = value => ({
  value,
  map: morphism => Thing(morphism(value))
//                 ----- -------- -----
//                /        |            \
// always a Thing          |             value to be morphed
//                         |
//             Morphism passed into map
})

const thing1 = Thing(1)               // { value: 1 }
const thing2 = thing1.map(x => x + 1) // { value: 2 }
Enter fullscreen mode Exit fullscreen mode

And that is a Functor! It really is just that simple.

Thing 1 and Thing 2 from Dr Seuse

🔍 Google tip: The "Thing" Functor we created is known as Identity.

Back to the Monad

Sometimes functions return a value already wrapped. This could be inconvenient to use with a Functor because it will re-wrap the Functor in another Functor.

const getThing = () => Thing(2)

const thing1 = Thing(1)

thing1.map(getThing) //=> Thing (Thing ("Thing 2"))
Enter fullscreen mode Exit fullscreen mode

This behavior is identical to Array's behavior.

const doSomething = x => [x, x + 100]
const list = [1, 2, 3]

list.map(doSomething) //=> [[1, 101], [2, 102], [3, 103]]
Enter fullscreen mode Exit fullscreen mode

This is where flatMap comes in handy. It's similar to map, except the morphism is also expected to perform the work of wrapping the value.

const Thing = value => ({
  value,
  map: morphism => Thing(morphism(value)),
  flatMap: morphism => morphism(value)
})

const thing1 = Thing(1)                          //=> Thing (1)
const thing2 = thing1.flatMap(x => Thing(x + 1)) //=> Thing (2)
Enter fullscreen mode Exit fullscreen mode

That looks better!

This could come in handy in a Maybe when you might need to switch from a Just to a Nothing, when for example a prop is missing.

import Just from 'mojiscript/type/Just'
import Nothing from 'mojiscript/type/Nothing'

const prop = (prop, obj) =>
  prop in obj
    ? Just(obj[prop])
    : Nothing

Just({ name: 'Moji' }).flatMap(x => prop('name', x)) //=> Just ("Moji")
Just({}).flatMap(x => prop('name', x))               //=> Nothing
Enter fullscreen mode Exit fullscreen mode

This code could be shortened to:

const Just = require('mojiscript/type/Just')
const Nothing = require('mojiscript/type/Nothing')
const { fromNullable } = require('mojiscript/type/Maybe')

const prop = prop => obj => fromNullable(obj[prop])

Just({ name: 'Moji' }).flatMap(prop('name')) //=> Just ("Moji")
Just({}).flatMap(prop('name'))               //=> Nothing
Enter fullscreen mode Exit fullscreen mode

🔍 Google tip: This code shortening is made possible with currying, partial application, and a point-free style.

Maybe you were expecting more, but that's it for a Monad! A Monad is both mappable and flat-mappable.

I hope at this point you are thinking this was an easier journey than you initially thought it would be. We have covered Functors and Monads and next up in the Promise!

The Promise

If any of that code looks familiar it's because the Promise behaves like both map and flatMap.

const double = num => num * 2

const thing1 = Thing(1)             //=> Thing (1)
const promise1 = Promise.resolve(1) //=> Promise (1)

thing1.map(double)    //=> Thing (2)
promise1.then(double) //=> Promise (2)

thing1.flatMap(x => Thing(double(x)))          //=> Thing (2)
promise1.then(x => Promise.resolve(double(x))) //=> Promise (2)
Enter fullscreen mode Exit fullscreen mode

As you can see the Promise method then works like map when an unwrapped value is returned and works like flatMap, when it is wrapped in a Promise. In this way a Promise is similar to both a Functor and a Monad.

This is also the same way it differs.

thing1.map(x => Thing(x + 1))              // Thing (Thing (2))
promise1.then(x => Promise.resolve(x + 1)) // Promise (2)

thing1.flatMap(x => x + 1) //=> 2
promise1.then(x => x + 1)  //=> Promise (2)
Enter fullscreen mode Exit fullscreen mode

If I wanted to wrap a value twice (think nested Arrays) or control the return type, I am unable to with Promise. In this way, it breaks the Functor laws and also breaks the Monad laws.

Summary

  • A Functor is something that is Mappable or something that can be mapped between objects in a Category.
  • A Monad is similar to a Functor, but is Flat Mappable between Categories.
  • flatMap is similar to map, but yields control of the wrapping of the return type to the mapping function.
  • A Promise breaks the Functor and Monad laws, but still has a lot of similarities. Same same but different.

Continue reading: NULL, "The Billion Dollar Mistake", Maybe Just Nothing

My articles show massive Functional JavaScript love. If you need more FP, follow me here or on Twitter @joelnet!

And thanks to my buddy Joon for proofing this :)

Cheers!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .