Data Structure Operations

We're now going to take map, filter, and reduce and think about them at the more general data structure level (not just arrays).

Monads

Monads are functional-friendly data structures.

As data structures, monads hold one discrete value. The point of doing this is to wrap behaviour around that value, making that value easier to inter-operate with other monads.

Specifically, monads turn values into functors: values that you can transform (map), include (filter), and combine (reduce).

Implementing the just monad

High-level, a monad in implementation is just a function that is passed a value and returns these methods (and more): map, chain, and ap. These methods can then be used to access and work with the closed-over value passed in.

function Just(val) {
  return { map, chain, ap };

  // - Applies function to value
  // - Returns another monad
  // - Just like how mapping over an array,
  // returns an array, mapping over a monad returns a monad
  function map(fn) {
    return Just(fn(val));
  }

  // - Sometimes called bind or flatMap
  // - Flattens a monad
  // - For simplicity, we return mapped value without wrapping monad
  function chain(fn) {
    return fn(val);
  }

  // - Calls the map of another monad
  // - Requires the value passed in to be a function
  function ap(anotherMonad) {
    return anotherMonad.map(val);
  }
}

These monadic behaviours obey 3 monadic rules. (We won't go into the rules though.)

Here's some use cases to wrap your head around how it works:

const ten = Just(10);
const eleven = ten.map(x => x + 1);

ten.chain(x => x); // 10
eleven.chain(x => x); // 11

// -----

const user1 = Just('Dan');
const user2 = Just('John');

const tuple = curry(2, (x, y) => [x, y]);

// The map method returns a monad with the curried tuple waiting for 1 more input
// The ap method takes that curried tuple and passes the last input via user2.map
const users = user1.map(tuple).ap(user2);

users.chain(x => x); // ["Dan", "John"]

Note: We are cheating when we pass the identity function to chain. The only acceptable function for a chain is a function that returns a monad. That's because returning the value inside the monad is considered a side effect.

The maybe monad

One of the most common use cases for monads is the maybe monad. This monad solves the problem of accessing deeply nested object properties where you don't know if you'll get back undefined for any of those properties.

To start, you need a Nothing monad that continually returns back more Nothing monads. We'll use this monad if we hit undefined.

function Nothing() {
  return { map: Nothing, chain: Nothing, ap: Nothing };
}

Now we want to create our Maybe monad and our prop helper function.

const Maybe = { Nothing, of: Just };

// Returns either a Nothing or a Just monad
function fromNullable(val) {
  if (val === null || val === undefined) return Maybe.Nothing();
  return Maybe.of(val);
}

// Passes property value to fromNullable
const prop = curry(2, function (key, obj) {
  return fromNullable(obj[key]);
});

Finally, we can access our deeply nested property by chaining chain.

Maybe.of(obj)
  .chain(prop('someProp'))
  .chain(prop('thatIsNested'))
  .chain(prop('prettyDeeply'));

Note: This works because prop is curried, so it's waiting for an object to be passed into it before it is invoked. And chain passes that object. Then the Just monad returned with that property value closed over it calls chain again.

If at any point fromNullable finds a property that returns undefined or null, it returns a Nothing monad, making all future chains return a Nothing monad as well.

More on Monads

Here are some kinds of monads:

  • Just

  • Nothing

  • Maybe

  • Either

  • IO

Last updated