DEV Community

James Robb
James Robb

Posted on

What is a Monad?

A monad is a monoid in the category of Endofunctors. Okay, post completed, need I say more?... Fine, okay then.

A monad is a very powerful concept allowing for data flow, transformation and control in a managed form. You can think of it as an isolated, self-contained, step within a computation stream. The key component to support this is the bind function:

(>>=) :: Monad m => m a -> (a -> m b) -> m b

You might call it map or pipe although both are slightly incorrect but thats just semantics.

A monad is defined as follows:

Monad, (from Greek monas “unit”), an elementary individual substance that reflects the order of the world and from which material properties are derived. The term was first used by the Pythagoreans as the name of the beginning number of a series, from which all following numbers derived.

Source: Britannica

Okay, so what about monoids vs monads?

  • A monad is focused on sequencing computations and handling side effects.
  • A monoid is focused on combining values, typically with an associative operation mappend, also known as (<>), and an identity initialiser mempty to create an empty instance.

What about an Endofunctor?

An Endofunctor is a specific type of Functor where the source and target categories are the same. In other words, it is a functor from a category to itself.

The core of all Functors is the fmap operation:

fmap :: Functor f => (a -> b) -> f a -> f b

So where a functor might fmap a Functor String to a Functor Int, an endofunctor would only ever fmap a Functor Something to Functor Something.

Now you know all the components of a Monad. It's incredibly powerful and you actually use them all the time without realising it. There are many common Monads like Maybe, Reader, Writer, Continuation, etc.

An example - The Maybe monad

The Maybe monad is a monadic data-structure which represents the possibility of a value existing. This is useful in cases where you have computations that may return zero or one value.

For example, if I have a function stringToInt(input: string): number then this would be wrongly typed since we cannot guarantee that the given string is a valid number and defaulting a bad string to 0 or any other number is disingenuous.

Instead we could utilise the type signature stringToInt(input: string): Maybe<number> which is far more correct and allows us to contain the possible presence of that value cleanly for future computations to handle.

In typescript I could write the Maybe monad as:

// Maybe Monad
class Maybe<T> {
  private value: T  | null;

  constructor(value: T | null = null) {
    this.value = value;
  }

  // Functor: map
  map<U>(f: (value: T) => U): Maybe<U> {
    return this.value !== null ? new Maybe<U>(f(this.value)) : new Maybe<U>();
  }

  // Monad: flatMap (bind)
  flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U> {
    return this.value !== null ? f(this.value) : new Maybe<U>();
  }

  // Example non-required methods you may wish to add
  isJust(): boolean {
    return this.value !== null;
  }

  isNothing(): boolean {
    return this.value === null;
  }

  withDefault(fallback: T): T {
    return this.value !== null ? this.value : fallback;
  }

  // Other more advanced methods you may wish to add
  map2<U, R>(f: (ours: T, theirs: U) => Maybe<R>, other: Maybe<U>): Maybe<R> {
    if(this.value !== null && other.value !== null) {
        return f(this.value, other.value);
    }

    return new Maybe<R>();
  }
}

// Example Usage
const maybeValue: Maybe<number> = new Maybe(5);

// Endofunctor example: mapping over the Maybe value
const incrementedMaybe: Maybe<number> = maybeValue.map(value => value + 1);
console.log(incrementedMaybe); // Output: Maybe { value: 6 }

// Monad example: binding Maybe with another Maybe
const doubledMaybe: Maybe<number> = maybeValue.flatMap(value => new Maybe(value * 2));
console.log(doubledMaybe); // Output: Maybe { value: 10 }

// Monad example: is empty
const empty = new Maybe<string>();
console.log(empty.isNothing()); // Output: true
console.log(empty.isJust()); // Output: false
console.log(empty.withDefault("test")); // Output: "test"

// Monad example: has value
const withValue = new Maybe<string>("abc123");
console.log(withValue.isNothing()); // Output: false
console.log(withValue.isJust()); // Output: true
console.log(withValue.withDefault("test")); // Output: "abc123"

// Combinatory example: Mapping two monadic values
const first = new Maybe<string>("abc");
const second = new Maybe<number>(123);
const third = first.map2(
    (ours: string, theirs: number) => new Maybe<string>(ours + theirs.toString()),
    second
);
console.log(third); // Output: Maybe { value: "abc123" }
Enter fullscreen mode Exit fullscreen mode

Conclusions

Now you should understand Monads. They simply wrap values, can map between values and bind values in a computational pipeline friendly manner: When you think of a Monad, think of type transformations running over time.

I hope that you found some value in todays post and if you have any questions, comments or suggestions, feel free to leave those in the comments area below the post!

Top comments (0)