DEV Community

Attila Večerek
Attila Večerek

Posted on • Edited on

Functor and Monad

Functors and monads are concepts that are often misunderstood and over-explained at the same time. Many tutorials and blog posts focus on why these concepts are useful instead of what they actually are. This is understandable since it is actually not required to know what these concepts are in order to take advantage of them.

In this post, we first look into what a functor and a monad are. After that, we switch over to what we can achieve by using them.

Functor

The word functor is the name of a specific interface which describes the following property: when we have something of type a, and we have a way to take a and map it to b, we can have something of type b [1]. Something needs to be a source of a value of some type. The word "source" is chosen deliberately to represent an abstract thing because it can be pretty much anything. Let's take a look at a couple of examples of a source of type string:

// the values are accessed by iteration
export type ArraySource = Array<string>;

// the value is accessed through property `a`
export interface RecordSource {
  a: string;
}

// the value is accessed through a function call
export interface FunctionSource {
  (): string;
}
Enter fullscreen mode Exit fullscreen mode

All of the above are sources of type string because there is a way to access the value. Array, Record, and Function are functors because they specify a way to map their enclosed values to a potentially different type. We can also think of functors as type classes specifying some kind of a map function. That function does not necessarily have to be called map but needs to behave like one. For example, the type Array is a functor because it defines a map function.

// source of values `a` (Array<number>)
const numbers = [1, 2, 3];

// mapping of `a` (number) to `b` (string)
const toString = (a: number): string => String(a);

// source of values `b` (Array<string>)
export const result = numbers.map(toString);
Enter fullscreen mode Exit fullscreen mode

Array#map has the ability to take an a (of any type) from an instance of Array by iterating over its values, apply a mapping function on each a, and produce a new instance of Array holding values of b of type that is dictated by the return type of our mapping function.

How would a Function#map look like?

interface Fn<A> {
  (): A;
}

interface FnMap {
  <A, B>(fn: (a: A) => B): (a: Fn<A>) => Fn<B>;
}

const functionMap: FnMap = (fn) => (a) => {
  return () => fn(a());
};

// source of values `a` (Fn<number>)
const makeNumber = () => 42;

// mapping of `a` (number) to `b` (string)
const toString = (a: number): string => String(a);

// source of values `b` (Function<string>)
export const result = functionMap(toString)(makeNumber);
Enter fullscreen mode Exit fullscreen mode

functionMap has the ability to take an a (of any type) from an instance of type Function by calling it, apply a mapping function on its return value, and produce a new instance of Function holding a value of b of type that is dictated by the return type of our mapping function.

In both examples, we use the exact same mapping function (toString) without having to change anything about it. The same function can then be applied to both functors: Array and Function. In fact, we can supply that mapping function to any functor's map function exactly because the term functor describes a property, and not a noun. Reusing code without additional modifications is extremely powerful.

Monad

Just like functor, the word monad is also the name of a specific interface. A monad is something that fulfils the following:

  • it is a functor, i.e. implements a map function
  • it can be chained, i.e. implements a flatMap function
  • it can be constructed, i.e. implements an of function
  • it abides by the Monad laws

Chainability

The definition of chainability is very similar to functor's mappability. The difference is highlighted in bold letters: when we have something of type a, and a way to map a onto the same type of something of type b, we can have something of type b [1]. Same as with functor, something needs to be a source of a value of some type. We can also think of monads as type classes specifying some kind of a flatMap function. That function does not necessarily have to be called flatMap but needs to behave like one. For example, the type Array, besides being a functor, is also a monad because it defines a flatMap function.

// source of values `a` (Array<string>)
const expressions = ["Functors and monads", "are easy"];

// mapping of `a` (string) to the same type of source (Array) of `b` (Array<string>)
const splitByWords = (a: string): string[] => a.split(" ");

// source of values `b` (Array<string>)
export const words = expressions.flatMap(splitByWords);
// ["Functors", "and", "monads", "are", "easy"]
Enter fullscreen mode Exit fullscreen mode

So, how does our example with flatMap fit the definition? Let's break it down:

  • "when we have something of type a": expressions - the type of the source is Array<string>.
  • "and a way to map a onto the same type of something of type b": splitByWords maps string onto Array<string> (matches the type from above).
  • "we can have something of type b": expressions.flatMap(splitByWords) produces Array<string> because flatMap knows two things:
    1. how to provide a to the mapping function,
    2. how to flatten the intermediate results produced by the mapping function.

Evaluating expressions.map(splitByWords) would produce a value of type Array<Array<string>>. Hence, things that implement a function like map but don't implement a function like flatMap cannot be called monads.

Constructability

A monad also needs to implement a function that may take a value and returns an instance of the monad encapsulating that value. Many times these functions will be called of, as in Task.of(42), but can also be called anything else, e.g.: left, right, some, none, etc.

Monad laws

The explanation of monad laws is beyond the scope of this series. However, if you're interested, you can read Monad laws for regular developers written by Miklós Martin. It provides a nice explanation using both Scala and JavaScript examples.

Why are functors and monads useful?

In my opinion, they are useful because of their following three innate characteristics:

  1. They are polymorphic.
  2. They abstract away some boilerplate code such as iteration and control flow logic.
  3. They are composable.

Polymorphism

Polymorphism just means that an object can take up multiple forms. For example, the type Array can be of strings, numbers or any other type.

Functions like map and flatMap don't care what the type of the value is. That is the responsibility of the mapping function. With map and flatMap, we can easily go from an Array<number> to an Array<string>.

const numbers = [1, 2, 3];
export const strings = numbers.map(String);
// ["1", "2", "3"]
Enter fullscreen mode Exit fullscreen mode

This is a great property to have because we don't have to implement map and flatMap functions for every possible type of Array out there.

Useful abstractions

Functors and monads abstract away some common boilerplate code such as iteration and control flow logic. By using functors and monads, we repeat less code. Also, we tend to write more declarative code instead of imperative.

Imperative code is more prescriptive, i.e. we say how a certain thing should be implemented.

Declarative code is more descriptive, i.e. we say what a certain thing should do. People who write code are humans, and humans make mistakes. So, the less we have to describe how things should be implemented, the more of the following may apply to our code:

  • has fewer bugs,
  • runs faster,
  • is more secure.

Obviously, this applies only if the functor/monad implementation is well tested, optimized for speed and implemented with security in mind.

Iteration and Control flow

The previously shown code examples demonstrate how map and flatMap both hide the iteration logic. To demonstrate the control flow logic being abstracted away, we'll need to assume something about how Array's map function is implemented.

export const map =
  <A, B>(fn: (a: A) => B) =>
  (x: A[]): B[] => {
    if (x.length === 0) return [];

    // pretend to be the rest of map's implementation
    return x.map(fn);
  };
Enter fullscreen mode Exit fullscreen mode

This is a simple and not very useful optimization but it serves its purpose well.

[]
  .map((x) => x + 1)
  .map((x) => x * 2)
  .map((x) => x % 2);

// []
Enter fullscreen mode Exit fullscreen mode

Every time map is called, an empty array is returned early skipping possibly expensive and unnecessary computations. The real value of abstracting the control flow logic will be more apparent when we'll look into the Either and Option monads in the upcoming chapters.

Function composition

Because of how functor and monad is defined, it lends itself to function composition very well. Remember how Array#map and Array#flatMap work? They both return an instance of Array. This means, we can keep calling map and flatMap in succession:

const splitByWords = (a: string): string[] => a.split(" ");
const len = (a: string): number => a.length;
const double = (a: number): number => a * 2;

const expressions = ["Functors and monads", "are easy"];
export const result = expressions.flatMap(splitByWords).map(len).map(double);
// [ 16, 6, 12, 6, 8 ]
Enter fullscreen mode Exit fullscreen mode

Function composition in this example is achieved by method chaining. If we want, we can create our own map with a slightly different interface and use pipe from the previous chapter:

import { pipe } from "fp-ts/lib/function.js";

const map =
  <A, B>(f: (_: A) => B) =>
  (a: A[]): B[] =>
    a.map(f);
const len = (a: string): number => a.length;
const double = (a: number): number => a * 2;

export const result = pipe(
  ["Functors", "and", "monads", "are", "easy"],
  map(len),
  map(double)
);
// [ 16, 6, 12, 6, 8 ]
Enter fullscreen mode Exit fullscreen mode

The advantage of redefining map's interface is that we can "chain" functions other than map and flatMap as well. Luckily for us, fp-ts already did that for both map and flatMap, so we could have written our example just as:

import { array } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";

const splitByWords = (a: string): string[] => a.split(" ");
const len = (a: string): number => a.length;
const double = (a: number): number => a * 2;

export const result = pipe(
  ["Functors and monads", "are easy"],
  array.chain(splitByWords), // flatMap is called chain in fp-ts
  array.map(len),
  array.map(double)
);
// [ 16, 6, 12, 6, 8 ]
Enter fullscreen mode Exit fullscreen mode

Wrap-up

  • Functors and monads can be thought of as interfaces.
  • Functor is an interface specifying a function similar to Array#map.
  • Monad is a functor that also:
    • specifies a function similar to Array#flatMap,
    • has a constructor: Array(),
    • abides by the Monad laws.
  • Functors and monads are useful because they are:
    • polymorphic,
    • reduce code repetition,
    • compose well.

The next part of this series delves into a very specific type of monad implemented in fp-ts: the Either monad.

Extra resources

Top comments (1)

Collapse
 
attila_vecerek profile image
Attila Večerek • Edited

It has just been brought to my attention that for something to be a monad, it also has to implement of (i.e. sort of a constructor) besides the map and flatMap functions. Furthermore, all three have to satisfy the monad laws.

Because of the above, Promise is not a monad. I will try to find time to correct these mistakes and provide better examples as soon as possible. Million thanks to Michael Arnaldi for bringing this to my attention :-)