loading...

Functors and Monads in Javascript

bonesmcginty profile image Matthew Staniscia ・7 min read

Functors and Monads

The purpose of this talk is to shed some light on some of the Functional Programming terms we see thrown about here and there, primarily Functor and Monad.

What the heck are these? Let's start with 2 phrases that I saw while scouring the internet.

"A functor is something that you can map."

"A monad is a functor that you can flatMap."

Let's dive into it.

Functors

In mathematics, specifically category theory, a functor is a map between categories.

In mathematics, a category (sometimes called an abstract category to distinguish it from a concrete category) is a collection of "objects" that are linked by "arrows".

Confused? Good.

Simply put a category is a collection of objects let's call that something, and a functor is a map between collections of objects.

So that brings us to our first statement:

"A functor is something that you can map."

Let's look at some code:

const collection1 = [1, 2, 3] // [1,2,3]
const collection2 = collection.map(x => x + 1) // [2,3,4]

Here we have an array (a collection of Ints). Since we can map collection1 to collection2 by doing x => x + 1 we can say that Arrays in JS are Functors.

Lets say we wanted to create our own functor. This functor will represent a Person Object.

const p1 = {
  firstName: 'matthew',
  lastName: 'staniscia',
  hairColor: 'brown',
  age: 37,
}

const Person = value => ({
  value,
})

Person(p1)

/*
Result
{ 
  value:{ 
    firstName: 'matthew',
    lastName: 'staniscia',
    hairColor: 'brown',
    age: 37 
  }
}
*/

This is not a functor yet because we can't yet map over it. So lets add a mapping function to it.

const p1 = {
  firstName: 'matthew',
  lastName: 'staniscia',
  hairColor: 'brown',
  age: 37,
}

const Person = value => ({
  map: fn => Person(fn(value)),
  value,
})

Person(p1)

/*
Result
{ 
  map: [Function: map],
  value:{ 
    firstName: 'matthew',
    lastName: 'staniscia',
    hairColor: 'brown',
    age: 37 
  }
}
*/

We can now map some functions to it.

const objectMapper = fn => value =>
  Object.keys(value).reduce((acc, cur, idx, arr) => ({ ...acc, [cur]: fn(value[cur]) }), value)

const makeUpper = s => (typeof s === 'string' ? s.toUpperCase() : s)

Person(p1).map(x => objectMapper(y => makeUpper(y))(x))
Person(p1).map(x => objectMapper(makeUpper)(x))
Person(p1).map(objectMapper(makeUpper))

/*
Result for all 3 calls
{ 
  map: [Function: map],
  value:{ 
    firstName: 'MATTHEW',
    lastName: 'STANISCIA',
    hairColor: 'BROWN',
    age: 37 
  }
}
*/

Let's try mapping a couple functions together.

const objectMapper = fn => value =>
  Object.keys(value).reduce((acc, cur, idx, arr) => ({ ...acc, [cur]: fn(value[cur]) }), value)

const makeUpper = s => (typeof s === 'string' ? s.toUpperCase() : s)

const checkAge = n => (typeof n === 'number' ? (n <= 35 ? [n, 'You is good.'] : [n, 'You is old.']) : n)

Person(p1)
  .map(objectMapper(makeUpper))
  .map(objectMapper(checkAge))

/*
Result
{ 
  map: [Function: map],
  value:{ 
    firstName: 'MATTHEW',
    lastName: 'STANISCIA',
    hairColor: 'BROWN',
    age: [ 37, 'You is old.' ] 
  }
}
*/

This object is now a functor because it is something that we can map. Now it's time to turn it into a Monad.

Monads

Let's go back to the definition of a Monad from earlier.

"A monad is a functor that you can flatMap."

What is flatMap?

In short when you flatMap something, you will run a map function and then flatten it.

In the case of our Person object our output will not look like Person({...stuff...}) but rather {...stuff...}.

We use flatMap to pull out the result of the map from its context. Other names for flatMap are chain and bind.

Back to code.

const Person = value => ({
  map: fn => Person(fn(value)),
  chain: fn => fn(value),
  value,
})

Well that looks simple enough. Since we're mapping and taking the value out of context we only need to return the unwrapped value. Let's see it in action.

Person(p1).chain(objectMapper(makeUpper))

/*
Result
{ 
  firstName: 'MATTHEW',
  lastName: 'STANISCIA',
  hairColor: 'BROWN',
  age: 37 
}
*/

Person(p1)
  .chain(objectMapper(makeUpper))
  .chain(objectMapper(checkAge))

/* 
Result

TypeError: Person(...).chain(...).chain is not a function
*/

Huston, we have a problem. What happening here? Why is it wrong?
It's simple. The return of the first chain is no longer a Person Monad, it's just a JSON string, so trying to chain it again won't work, if we wanted to chain on a chain we need to maintain context.

Person(p1)
  .chain(x => Person(objectMapper(makeUpper)(x)))
  .chain(objectMapper(checkAge))

/*
Result
{
  firstName: 'MATTHEW',
  lastName: 'STANISCIA',
  hairColor: 'BROWN',
  age: [ 37, 'You is old.' ]
}
*/

But isn't that the same as this?

Person(p1)
  .map(objectMapper(makeUpper))
  .chain(objectMapper(checkAge))

Yes. Since map keeps context we can map or chain on that context.

I think we've seen something like this before...

Monad laws

For an object to be a monad is must satisfy 3 monadic laws.

  • Left identity
  • Right identity
  • Associativity
// testing monad rules
const x = 'Matt'
const f = x => Person(x)
const g = x => Person(x + ' is kool')

const LI1 = Person(x).chain(f)
const LI2 = f(x)

const RI1 = Person(x).chain(Person)
const RI2 = Person(x)

const AC1 = Person(x)
  .chain(f)
  .chain(g)
const AC2 = Person(x).chain(x => f(x).chain(g))

// Left Identity
// Monad(x).chain(f) === f(x)
// f being a function returning a monad
Object.entries(LI1).toString() === Object.entries(LI2).toString()

// Right Identity
// Monad(x).chain(Monad) === Monad(x)
Object.entries(RI1).toString() === Object.entries(RI2).toString()

// Associativity
// Monad(x).chain(f).chain(g) == Monad(x).chain(x => f(x).chain(g));
// f and g being functions returning a monad
Object.entries(AC1).toString() === Object.entries(AC2).toString()

/*
Result
true
true
true
*/

In the case of our Person monad it satisfies these rules.

Why Use Monads?

You don't need to use Monads. If you do use Monads and write all of your Monads in the same manner, then you'll have a structure that you can chain together and intermix as you want. Monads are pretty much a design structure that can be used to help you track context so that your code is clear and consistent.

Let's take a look at a basic example of different monads being used together. These are very rudimentary monads but they'll get the point across.

We'll create 3 more monads Child, Teen, and Adult. These monads will have some properties that you can access if you want to be able to know if it's a Child, Teen, or Adult.

const Person = value => ({
  map: fn => Person(fn(value)),
  chain: fn => fn(value),
  value,
})

const Adult = value => ({
  map: fn => Adult(fn(value)),
  chain: fn => fn(value),
  isChild: false,
  isTeen: false,
  isAdult: true,
  value,
})

const Teen = value => ({
  map: fn => Teen(fn(value)),
  chain: fn => fn(value),
  isChild: false,
  isTeen: true,
  isAdult: false,
  value,
})

const Child = value => ({
  map: fn => Child(fn(value)),
  chain: fn => fn(value),
  isChild: true,
  isTeen: false,
  isAdult: false,
  value,
})

We'll also add the functions that we will use to map and / or chain.

const objectMapper = fn => value =>
  Object.keys(value).reduce((acc, cur, idx, arr) => ({ ...acc, [cur]: fn(value[cur]) }), value)

const makeUpper = s => (typeof s === 'string' ? s.toUpperCase() : s)

const makeLower = s => (typeof s === 'string' ? s.toLowerCase() : s)

const makeCapitalize = s => (typeof s === 'string' ? s.replace(/(?:^|\s)\S/g, a => a.toUpperCase()) : s)

const addAge = curr => add => curr + add

const setContext = obj => (obj.age < 13 ? Child(obj) : obj.age < 18 ? Teen(obj) : Adult(obj))

const agePerson = age => obj => setContext({ ...obj, age: addAge(obj.age)(age) })

Let's start playing with our monads.

const p1 = {
  firstName: 'matthew',
  lastName: 'staniscia',
  hairColor: 'brown',
  age: 10,
}

Person(p1).map(objectMapper(makeUpper))

/*
Result: This is a Person Monad
{ 
  map: [Function: map],
  chain: [Function: chain],
  value:
   { 
     firstName: 'MATTHEW',
     lastName: 'STANISCIA',
     hairColor: 'BROWN',
     age: 10 
   }
}
*/


Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)

/*
Result: This is a Child Monad
{ 
  map: [Function: map],
  chain: [Function: chain],
  isChild: true,
  isTeen: false,
  isAdult: false,
  value:
   { 
     firstName: 'MATTHEW',
     lastName: 'STANISCIA',
     hairColor: 'BROWN',
     age: 10 
   }
}
*/


Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)
  .chain(agePerson(4))
  .map(objectMapper(makeLower))

/*
Result: This is a Teen Monad
{ 
  map: [Function: map],
  chain: [Function: chain],
  isChild: false,
  isTeen: true,
  isAdult: false,
  value:
   { 
     firstName: 'matthew',
     lastName: 'staniscia',
     hairColor: 'brown',
     age: 14 
   }
}
*/


Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)
  .chain(agePerson(4))
  .map(objectMapper(makeLower))
  .chain(agePerson(4))
  .map(objectMapper(makeCapitalize))

/*
Result: This is an Adult Monad
{ 
  map: [Function: map],
  chain: [Function: chain],
  isChild: false,
  isTeen: false,
  isAdult: true,
  value:
   { 
     firstName: 'Matthew',
     lastName: 'Staniscia',
     hairColor: 'Brown',
     age: 18 
   }
}
*/

Just for fun let’s include another Monad. We’ll use the Maybe monad from the Pratica library and add a function to see if that person can drink in the US.

import { Maybe } from 'pratica'

const maybeDrinkInUS = obj => (obj.age && obj.age >= 21 ? Maybe(obj) : Maybe())

After running through the pipeline we’ll either return the data structure or a message.

Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)
  .chain(agePerson(4))
  .map(objectMapper(makeLower))
  .chain(agePerson(4))
  .map(objectMapper(makeCapitalize))
  .chain(maybeDrinkInUS) // This returns a Maybe Monad
  .cata({
    Just: v => v,
    Nothing: () => 'This Person is too young to drink in the US',
  })

/*
Result
'This Person is too young to drink in the US'
*/

Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)
  .chain(agePerson(4))
  .map(objectMapper(makeLower))
  .chain(agePerson(7)) // Changed this line to now be 21
  .map(objectMapper(makeCapitalize))
  .chain(maybeDrinkInUS) // This returns a Maybe Monad
  .cata({
    Just: v => v,
    Nothing: () => 'This Person is too young to drink in the US',
  })

/*
Result
{ 
  firstName: 'Matthew',
  lastName: 'Staniscia',
  hairColor: 'Brown',
  age: 21 
}
*/

Conclusion

In conclusion a Monad is nothing more than a wrapper/context/class that has ability to:

  • Map data within its own context.
  • Chain by mapping over its data and extracting it from its context.
  • Satisfies the 3 monadic laws.
  • It might have extra properties or methods associated to it.

Sources

The following links helped me understand Monads and be able to put it into words.

  1. https://dev.to/rametta/basic-monads-in-javascript-3el3
  2. https://www.youtube.com/watch?v=2jp8N6Ha7tY
  3. https://medium.com/front-end-weekly/implementing-javascript-functors-and-monads-a87b6a4b4d9a
  4. https://medium.com/javascript-scene/javascript-monads-made-simple-7856be57bfe8
  5. https://www.codingame.com/playgrounds/2980/practical-introduction-to-functional-programming-with-js/functors-and-monads
  6. https://medium.com/better-programming/tuples-in-javascript-57ede9b1c9d2
  7. https://hackernoon.com/functional-javascript-functors-monads-and-promises-679ce2ab8abe
  8. https://blog.klipse.tech/javascript/2016/08/31/monads-javascript.html
  9. https://github.com/getify/Functional-Light-JS
  10. https://www.youtube.com/watch?v=ZhuHCtR3xq8
  11. https://marmelab.com/blog/2018/09/26/functional-programming-3-functor-redone.html
  12. https://www.npmjs.com/package/pratica

Posted on by:

Discussion

pic
Editor guide
 

I am not fan of these cool things, problem is implementation detail of code is left anywhere else, every time I see agePerson(4), it does not tell me if it returns person with age less than 4 or more than 4 or equal to 4. This requires one to name the functions correctly which often leads to large names (have you seen names in Obj-c and Swift?).

And performance is far poor compared to plain verbose code, such huge chaining is problematic when you change one function and every chain fails.

These are best suitable for scientific calculations and fixed algorithms where names of every function/steps are well defined by some symbol or name which is constant. Such as pi, min, max, avg, ... etc. The underlying logic is deterministic.

No doubt this helps in reducing size of code, but at the same time, one must evaluate if it is worth and maintenance overhead needed in future.

They are good for Machine learning and other scientific applications, but really not good for long term changing business applications.

 

I'm not sure if that's a blanket perspective I can buy! Yes some of my variable names look like
makeSelectCheckoutStateWithTotals or isElementInViewportAndSticky but how is that bad if you ignore someone's personal preference or dogma or a habit of using short variable names.

Large apps have too much going on for one person to know or remember everything. Expressive variable names work as documentation. Also, to "age" means to grow older, but I would rely less on grammar and name it something like incrementAge.

About performance, it's valid if you write your code in a poor fashion where your compositions are just obfuscating everything for no reason. But if everything is safely composed and is type safe, I don't see any difference in a failure happening by composing something this way vs. a failure happening in the case where things are not composed. I have never been in a situation where chaining (through composition) has rendered me unable to debug a failure when one of my composed functions fails. It's in fact been easier to figure out what's wrong and fix it in my experience.

This post is a trivial example to help learn, but such compositional patterns in real software aren't used to add numbers to ages, but to isolate a lot of curried logic into its own separate bit of code. Which au contraire, makes your code more testable and less prone to failure. If something fails, you know exactly what failed instead of having to spend time figuring that out. Human error is bound to happen in any way you choose to program, but a functional approach makes it easier to pin point what's wrong due to granular separation of concerns.

Moreover, if your code is made with Justs and Nothings, the whole chain will short-circuit and none of the next functions will be called if any return a Nothing, so I really don't see any of your concerns as valid if they're coded by someone who have really embraced functional code and know what they're doing.

Only thing I'd change is using a function like 'pipe' or 'compose' to pass all these chained operators to. I'm not a big fan of .this().that().something() it's ugly in my eyes.

I use patterns like this for making web applications that thousands of people use every day, and these patterns aren't something that need to be applied to "scientific applications" only. You might wanna try Ramda on a small side-project or test app to experience it in real-world usage and see if your mind changes.

 

I tried Ramda and I don't see the benefit of writing code differently, words like thousands of people are doing it doesn't mean everyone else should do it.

I am not saying it is bad, and again scientific applications was an example, not an absolute statement, you have to evaluate what is best in terms of maintainability, availability of talent and tooling, and whether application really requires functional stuff or not.

This is the biggest problem with developers today, to make things appear cool, they want to do it things differently and I fail to realize the point from business perspective.

I'm not doing it because thousands are though, I'm doing it simply for how easy it makes development, and how lesser prone to spaghetti I am.

I don't really know much about a business perspective beyond what I'm programming to be understood and worked on by everyone else independent of me. And while training new people on my project I've found them to appreciate little things like how you can log the output of every link in the chain to literally "time travel" through how your data transducing / logic is happening per step. If code in maintainable and people understand it, that's all that one really needs. Availability of talent is valid, tooling idk. But this is just in my case with the people around me, there could be cases where this paradigm is proving useless, mine isn't such.
But I'll say It's presumptuous and unnerving for you to say that everyone appreciates something they're doing because it's "cool". There's 200 other ways to have discourse about what you appreciate and what you do not that doesn't involve personal and presumptuous remarks!