Lesson 8 of 8

Functors & Monads

Understanding functors, monads, Maybe, Either, and handling side effects functionally

What is a Functor?

A Functor is any data type that implements map and follows certain rules. It's a container that can be mapped over. You already use functors all the time - arrays are functors!

// Arrays are functors - they implement map
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

// Functor Laws:
// 1. Identity: x.map(a => a) === x
[1, 2, 3].map(x => x);  // [1, 2, 3] ✓

// 2. Composition: x.map(f).map(g) === x.map(x => g(f(x)))
const f = x => x + 1;
const g = x => x * 2;
[1, 2, 3].map(f).map(g);           // [4, 6, 8]
[1, 2, 3].map(x => g(f(x)));       // [4, 6, 8] ✓

// Create a simple functor (Box)
const Box = value => ({
  map: fn => Box(fn(value)),
  fold: fn => fn(value),    // Extract value
  value,
});

const result = Box(5)
  .map(x => x + 1)
  .map(x => x * 2)
  .fold(x => x);  // 12

The Maybe Monad

Maybe (also called Optional) handles null/undefined safely. It's either Just(value) (has a value) or Nothing (no value).

// Problem: Null checks are tedious and error-prone
const user = { name: 'Alice', address: { city: 'NYC' } };
const city = user && user.address && user.address.city; // 😫

// Maybe to the rescue!
const Maybe = {
  Just: value => ({
    map: fn => Maybe.Just(fn(value)),
    flatMap: fn => fn(value),
    getOrElse: () => value,
    isNothing: false,
  }),
  Nothing: () => ({
    map: () => Maybe.Nothing(),
    flatMap: () => Maybe.Nothing(),
    getOrElse: defaultValue => defaultValue,
    isNothing: true,
  }),
  fromNullable: value =>
    value == null ? Maybe.Nothing() : Maybe.Just(value),
};

// Safe property access
const prop = key => obj => Maybe.fromNullable(obj[key]);

const getCity = user =>
  Maybe.fromNullable(user)
    .flatMap(prop('address'))
    .flatMap(prop('city'));

getCity({ address: { city: 'NYC' }}).getOrElse('Unknown'); // 'NYC'
getCity({ address: {} }).getOrElse('Unknown');              // 'Unknown'
getCity(null).getOrElse('Unknown');                         // 'Unknown'

// Chain operations safely
Maybe.fromNullable(user)
  .map(u => u.name)
  .map(name => name.toUpperCase())
  .getOrElse('ANONYMOUS'); // 'ALICE'

The Either Monad

Either represents a value that can be one of two types: Left (typically error/failure) or Right (success). It's great for error handling.

// Either implementation
const Either = {
  Right: value => ({
    map: fn => Either.Right(fn(value)),
    flatMap: fn => fn(value),
    fold: (leftFn, rightFn) => rightFn(value),
    isRight: true,
    isLeft: false,
  }),
  Left: value => ({
    map: () => Either.Left(value),  // Don't map over Left
    flatMap: () => Either.Left(value),
    fold: (leftFn, rightFn) => leftFn(value),
    isRight: false,
    isLeft: true,
  }),
  tryCatch: fn => {
    try {
      return Either.Right(fn());
    } catch (e) {
      return Either.Left(e);
    }
  },
};

// Example: Parse JSON safely
const parseJSON = str =>
  Either.tryCatch(() => JSON.parse(str));

parseJSON('{"name": "Alice"}')
  .map(obj => obj.name)
  .fold(
    err => `Error: ${err.message}`,
    name => `Hello, ${name}`
  ); // 'Hello, Alice'

parseJSON('invalid json')
  .map(obj => obj.name)
  .fold(
    err => `Error: ${err.message}`,
    name => `Hello, ${name}`
  ); // 'Error: Unexpected token...'

// Validation example
const validateEmail = email =>
  email.includes('@')
    ? Either.Right(email)
    : Either.Left('Invalid email');

const validatePassword = password =>
  password.length >= 8
    ? Either.Right(password)
    : Either.Left('Password too short');

validateEmail('user@example.com')
  .flatMap(() => validatePassword('abc'))
  .fold(
    err => ({ success: false, error: err }),
    () => ({ success: true })
  ); // { success: false, error: 'Password too short' }

What is a Monad?

A Monad is a functor that also implements flatMap (also called chain or bind). It allows you to chain operations that return wrapped values.

// The problem: nested containers
const nested = Maybe.Just(Maybe.Just(5));
// We want Just(5), not Just(Just(5))

// map creates nesting
Maybe.Just(5).map(x => Maybe.Just(x + 1)); // Just(Just(6)) 😫

// flatMap unwraps one level
Maybe.Just(5).flatMap(x => Maybe.Just(x + 1)); // Just(6) ✓

// Monad Laws:
// 1. Left Identity: M.of(a).flatMap(f) === f(a)
// 2. Right Identity: m.flatMap(M.of) === m
// 3. Associativity: m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))

// Practical use: chaining async-like operations
const getUser = id => Maybe.fromNullable(users[id]);
const getAddress = user => Maybe.fromNullable(user.address);
const getCity = address => Maybe.fromNullable(address.city);

// Without flatMap - nested nightmare
getUser(1).map(user =>
  getAddress(user).map(address =>
    getCity(address))); // Maybe(Maybe(Maybe(city)))

// With flatMap - clean chain
getUser(1)
  .flatMap(getAddress)
  .flatMap(getCity)
  .getOrElse('Unknown'); // 'NYC' or 'Unknown'

Practical FP Error Handling

// Real-world example: API call with validation
const fetchUserData = async userId => {
  const validateId = id =>
    id > 0 
      ? Either.Right(id) 
      : Either.Left('Invalid user ID');

  const fetchUser = async id => {
    try {
      const res = await fetch(`/api/users/${id}`);
      if (!res.ok) return Either.Left('User not found');
      return Either.Right(await res.json());
    } catch (e) {
      return Either.Left('Network error');
    }
  };

  const validateAge = user =>
    user.age >= 18
      ? Either.Right(user)
      : Either.Left('User must be 18+');

  // Chain it all together
  const result = validateId(userId);
  if (result.isLeft) return result;
  
  const userResult = await fetchUser(userId);
  if (userResult.isLeft) return userResult;
  
  return validateAge(userResult.fold(() => null, u => u));
};

// Usage
const result = await fetchUserData(123);
result.fold(
  error => console.error(error),
  user => console.log('Valid user:', user)
);

Libraries for FP in JavaScript

// fp-ts - Full FP in TypeScript
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';

const result = pipe(
  O.some({ name: 'Alice' }),
  O.map(user => user.name),
  O.map(name => name.toUpperCase()),
  O.getOrElse(() => 'ANONYMOUS')
);

// Ramda - Practical FP utilities
import * as R from 'ramda';

const processUsers = R.pipe(
  R.filter(R.propEq('active', true)),
  R.map(R.prop('name')),
  R.take(5)
);

// Sanctuary - FP with strict types
import S from 'sanctuary';

S.pipe([
  S.map(x => x + 1),
  S.filter(x => x > 5),
])(S.Just([1, 2, 3, 4, 5, 6]));

✅ When to Use These Patterns

  • • Maybe: When a value might not exist (null/undefined handling)
  • • Either: When operations can fail with meaningful errors
  • • Functors: When you need to transform values in a container
  • • Monads: When chaining operations that return containers