Find out our product. Here!

Easiest way to undestand Monad with Javascript

Discover the power of functional programming in JavaScript with monads, functors, and polymorphic types for cleaner, more flexible code.

In functional programming, monads are one of the most pattern for structuring code. But if you've just started exploring the world of functors and applicatives, understanding how monads fit into the picture might feel daunting.

In this post, we'll walk through how to implement a monad in JavaScript, using a simple example. We'll also explain how monads differ from functors and applicatives, and how applicatives use the ap method to apply wrapped functions to wrapped values.

By the end, you'll have a functional, composable monad that can elegantly handle chained operations and errors.

What Are Functors, Applicatives, and Monads?

Before diving into the monad implementation, let's quickly recap these three key concepts in functional programming:

  1. Functor: A functor is a structure that can be mapped over. In JavaScript, arrays are functors because you can apply a function to each element using map(). Functors follow the rule: F(a) => F(b).

  2. Applicative: Applicatives take the concept of functors further. They allow you to apply wrapped functions to wrapped values. Applicatives have an ap method that lets you do this: F(f) => F(a) => F(b).

  3. Monad: Monads extend functors and applicatives with the ability to flatMap (also known as bind). This allows you to chain operations that return monads themselves, without ending up with nested structures like Some(Some(value)). Monads follow the rule: F(a) => (a => F(b)) => F(b).

Implementing the Functor: Maybe

Let's start with a simple functor that we’ll later extend into an applicative and monad. This functor, called Maybe, can have two possible states: Some (representing a value) or None (representing the absence of a value).

const Maybe = () => ({
  map: (f) => new Error("Must implement the method map!"),
  pattern: (pattern) => new Error("Must implement the method pattern!"),
  ap: (other) => new Error("Must implement the method ap!"),
  flatMap: (f) => new Error("Must implement the method flatMap!")
});

This Maybe functor needs to be fleshed out with concrete implementations for Some and None.

Adding the Some and None Functors

We'll define Some as a functor that wraps a value and applies functions to it, and None as an empty functor that ignores any function.

const Some = (value) => {
  const state = { value };

  const map = (f) => Some(f(state.value));
  const patternMatching = (pattern) => pattern.some(state.value);

  return { ...state, map, pattern: patternMatching };
};

const None = () => ({
  map: (f) => None(),
  pattern: (pattern) => pattern.none()
});
  • Some has a map function that applies the provided function f to the value inside Some.
  • None ignores any map calls since there's no value to work with.

Adding Applicatives: Implementing ap

Applicatives allow us to apply functions wrapped in the functor to values also wrapped in the functor. This is where the ap method comes in.

  • In Some, ap takes another functor (which contains a function) and applies it to the wrapped value.
  • In None, ap always returns None because there’s no value to apply the function to.
const Some = (value) => {
  const state = { value };

  const map = (f) => Some(f(state.value));
  const patternMatching = (pattern) => pattern.some(state.value);

  // Applicative 'ap' method
  const ap = (other) => 
    other.pattern({
      some: (f) => Some(f(state.value)),
      none: () => None()
    });

  return { ...state, map, pattern: patternMatching, ap };
};

const None = () => ({
  map: (f) => None(),
  pattern: (pattern) => pattern.none(),

  ap: (other) => None() // Always return None
});

With ap, we can apply a function wrapped in a Some to a value wrapped in another Some.

Moving from Applicative to Monad: Adding flatMap

The key feature of a monad is the flatMap (or bind) method, which allows you to chain operations that return monads themselves. We'll now implement flatMap for both Some and None.

  • flatMap in Some: Applies a function that returns another monad.
  • flatMap in None: Always returns None.
const Some = (value) => {
  const state = { value };

  const map = (f) => Some(f(state.value));
  const patternMatching = (pattern) => pattern.some(state.value);

  const ap = (other) => 
    other.pattern({
      some: (f) => Some(f(state.value)),
      none: () => None()
    });

  // Monad 'flatMap' method
  const flatMap = (f) => f(state.value); // Apply function that returns a monad

  return { ...state, map, pattern: patternMatching, ap, flatMap };
};

const None = () => ({
  map: (f) => None(),
  pattern: (pattern) => pattern.none(),
  ap: (other) => None(),

  // Monad 'flatMap' method always returns None
  flatMap: (f) => None()
});

Test Example: Using map, ap, and flatMap

Let’s test this monad by applying transformations using map, applying functions with ap, and chaining operations using flatMap.

const some5 = Some(5);
const someFn = Some((x) => x * 2);

const none = None();

// Using 'map' to transform the value
console.log(some5.map((x) => x + 1).map((x) => x * 2)); // { value: 12 }

// Using 'ap' to apply a function wrapped in a functor
console.log(some5.ap(someFn)); // { value: 10 }
console.log(none.ap(someFn));  // None
console.log(someFn.ap(some5)); // { value: 10 }
console.log(someFn.ap(none));  // None

// Using 'flatMap' to chain monadic operations
console.log(some5.flatMap((x) => Some(x + 10))); // { value: 15 }
console.log(some5.flatMap((x) => Some(x + 10)).flatMap((x) => Some(x * 2))); // { value: 30 }
console.log(none.flatMap((x) => Some(x + 10))); // None

Real-World Example: Safe Data Fetching

Monads can be incredibly useful when dealing with operations that may fail. For example, when fetching data from an API, you can use Maybe to handle success or failure without writing imperative if/else blocks.

const fetchData = (url) => {
  const data = fetch(url).then((res) => res.ok ? Some(res.json()) : None());

  return data;
};

fetchData('https://api.example.com/data')
  .flatMap((data) => Some(data.user))
  .flatMap((user) => Some(user.name))
  .pattern({
    some: (name) => console.log(`User name: ${name}`),
    none: () => console.log('User not found.')
  });

Here, flatMap allows us to keep fetching parts of the data while handling potential failures, like missing or malformed data.

Conclusion

Monads in JavaScript provide an elegant way to chain operations while handling errors or missing values gracefully. By implementing both the ap (for applicative) and flatMap (for monad) methods, you unlock the full power of these functional tools, enabling complex workflows to remain predictable and functional.

While this post focuses on Maybe, the concepts you’ve learned can be applied to other monads like Promise, Either, or even your custom monads.

Happy coding!

Read Also :
Holla, we share any interesting view for perspective and education sharing❤️

Post a Comment

© elgharuty. All rights reserved. Developed by Jago Desain