Learning Objectives

  • Understand what functors are and the functor laws
  • Implement functors with map
  • Learn applicative functors and their laws
  • Apply wrapped functions to wrapped values
  • Use functors and applicatives in real-world scenarios

What is a Functor?

A functor is a container that implements a map method. The map method applies a function to the value(s) inside the container and returns a new container with the transformed value(s).

Think of it as a box with a value inside. You can transform what's in the box without opening it.

Functor Implementation

class Box {
    constructor(value) {
        this.value = value;
    }

    map(fn) {
        return new Box(fn(this.value));
    }

    inspect() {
        return `Box(${this.value})`;
    }
}

// Usage
const box = new Box(5);
const result = box
    .map(x => x * 2)
    .map(x => x + 10);

console.log(result.inspect()); // Box(20)
How it works: Each map transforms the value inside the box and returns a new box. You never directly access the value - you always work through map.

The Functor Laws

For something to be a functor, it must follow two laws:

1. Identity Law

// Mapping with the identity function should do nothing
const identity = x => x;

box.map(identity) === box.map(x => x)
// Both return Box(value) unchanged

2. Composition Law

// Mapping with composed functions should be the same as
// mapping with each function separately
const f = x => x * 2;
const g = x => x + 10;
const compose = (f, g) => x => f(g(x));

box.map(compose(f, g)) === box.map(g).map(f)
// Both give the same result

Real-World Functor Example: Safe Division

class Maybe {
    constructor(value) {
        this.value = value;
    }

    static of(value) {
        return new Maybe(value);
    }

    isNothing() {
        return this.value === null || this.value === undefined;
    }

    map(fn) {
        return this.isNothing() 
            ? Maybe.of(null) 
            : Maybe.of(fn(this.value));
    }

    getOrElse(defaultValue) {
        return this.isNothing() ? defaultValue : this.value;
    }
}

// Safe calculation chain
function calculate(a, b, c) {
    return Maybe.of(a)
        .map(x => x / b)  // Could be division by zero
        .map(x => x * c)
        .map(x => x + 100)
        .getOrElse(0);
}

console.log(calculate(10, 2, 5)); // 125
console.log(calculate(10, 0, 5)); // 0 (safe!)

What is an Applicative Functor?

An applicative functor is a functor that can apply a wrapped function to a wrapped value. It has two key methods:

Applicative Implementation

class Box {
    constructor(value) {
        this.value = value;
    }

    static of(value) {
        return new Box(value);
    }

    map(fn) {
        return Box.of(fn(this.value));
    }

    ap(boxWithFunction) {
        return boxWithFunction.map(fn => fn(this.value));
    }

    inspect() {
        return `Box(${this.value})`;
    }
}

// Usage
const add = a => b => a + b;

const result = Box.of(5)
    .map(add)  // Box(b => 5 + b)
    .ap(Box.of(10));  // Box(15)

console.log(result.inspect()); // Box(15)
How it works: map(add) creates a box containing a partially applied function. ap applies that function to another boxed value.

Lifting Functions with Applicatives

Applicatives let you "lift" regular functions to work with wrapped values:

// Regular function
const add3 = (a, b, c) => a + b + c;

// Curried version for applicatives
const add3Curried = a => b => c => a + b + c;

// Lift it to work with Box values
const result = Box.of(add3Curried)
    .ap(Box.of(1))
    .ap(Box.of(2))
    .ap(Box.of(3));

console.log(result.inspect()); // Box(6)

// Or using map + ap
const result2 = Box.of(1)
    .map(add3Curried)
    .ap(Box.of(2))
    .ap(Box.of(3));

console.log(result2.inspect()); // Box(6)

Real-World Example: Form Validation

class Validation {
    constructor(value, isValid = true) {
        this.value = value;
        this.isValid = isValid;
    }

    static success(value) {
        return new Validation(value, true);
    }

    static failure(error) {
        return new Validation(error, false);
    }

    map(fn) {
        return this.isValid 
            ? Validation.success(fn(this.value))
            : this;
    }

    ap(validationWithFn) {
        if (!this.isValid) return this;
        if (!validationWithFn.isValid) return validationWithFn;
        return this.map(validationWithFn.value);
    }

    static of(value) {
        return Validation.success(value);
    }
}

// Validation functions
const validateEmail = email => 
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
        ? Validation.success(email)
        : Validation.failure('Invalid email');

const validateAge = age =>
    age >= 18
        ? Validation.success(age)
        : Validation.failure('Must be 18 or older');

const validateName = name =>
    name.length >= 2
        ? Validation.success(name)
        : Validation.failure('Name too short');

// Create user object from validated inputs
const createUser = name => email => age => ({
    name,
    email,
    age,
    createdAt: new Date()
});

// Combine validations
const user = validateName('John')
    .map(createUser)
    .ap(validateEmail('john@example.com'))
    .ap(validateAge(25));

console.log(user);
// Validation { value: { name: 'John', email: '...', age: 25, ... }, isValid: true }

// With invalid data
const invalidUser = validateName('J')
    .map(createUser)
    .ap(validateEmail('invalid-email'))
    .ap(validateAge(16));

console.log(invalidUser);
// Validation { value: 'Name too short', isValid: false }

Applicative Laws

Applicatives must follow four laws:

1. Identity

// Applying the identity function wrapped does nothing
Box.of(x).ap(Box.of(identity)) === Box.of(x)

2. Homomorphism

// Applying a wrapped function to a wrapped value is the same as
// applying the function to the value and wrapping the result
Box.of(x).ap(Box.of(f)) === Box.of(f(x))

3. Interchange

// The order of application doesn't matter
Box.of(y).ap(u) === u.ap(Box.of(f => f(y)))

4. Composition

// Composing wrapped functions works as expected
const compose = f => g => x => f(g(x));
u.ap(v.ap(w)) === u.ap(v.map(compose).ap(w))

Practical Example: Async Operations

class Task {
    constructor(fork) {
        this.fork = fork;
    }

    static of(value) {
        return new Task((reject, resolve) => resolve(value));
    }

    map(fn) {
        return new Task((reject, resolve) => 
            this.fork(reject, value => resolve(fn(value)))
        );
    }

    ap(taskWithFn) {
        return new Task((reject, resolve) => {
            let fn, val;
            let fnDone = false, valDone = false;

            const guardResolve = () => {
                if (fnDone && valDone) {
                    resolve(fn(val));
                }
            };

            taskWithFn.fork(reject, f => {
                fn = f;
                fnDone = true;
                guardResolve();
            });

            this.fork(reject, v => {
                val = v;
                valDone = true;
                guardResolve();
            });
        });
    }
}

// Simulate async operations
const fetchUser = id => new Task((reject, resolve) => {
    setTimeout(() => resolve({ id, name: 'John' }), 100);
});

const fetchPosts = userId => new Task((reject, resolve) => {
    setTimeout(() => resolve([{ userId, title: 'Post 1' }]), 100);
});

// Combine async operations
const combineData = user => posts => ({ user, posts });

const result = fetchUser(1)
    .map(combineData)
    .ap(fetchPosts(1));

result.fork(
    error => console.error('Error:', error),
    data => console.log('Success:', data)
);
// Success: { user: { id: 1, name: 'John' }, posts: [...] }

Key Differences: Functor vs Applicative vs Monad

Functor:
  • Has map
  • Transforms values inside containers
  • Example: Box(5).map(x => x * 2)
Applicative:
  • Has map and ap
  • Applies wrapped functions to wrapped values
  • Example: Box(5).map(add).ap(Box(10))
Monad:
  • Has map, ap, and flatMap
  • Flattens nested containers
  • Example: Box(5).flatMap(x => Box(x * 2))

When to Use What

Use Functors when:
  • You need to transform a single wrapped value
  • You're chaining simple transformations
  • You don't need to combine multiple wrapped values
Use Applicatives when:
  • You need to combine multiple wrapped values
  • You're validating multiple fields
  • You're running independent async operations in parallel
Use Monads when:
  • Operations depend on previous results
  • You need to flatten nested containers
  • You're chaining dependent async operations

Common Pitfalls

Pitfall 1: Not currying functions
// Wrong - can't use with ap
const add = (a, b) => a + b;

// Right - curried for ap
const add = a => b => a + b;
Pitfall 2: Using flatMap when map + ap would work
// Overcomplicated with flatMap
Box.of(5).flatMap(a => Box.of(10).map(b => a + b));

// Simpler with applicative
Box.of(5).map(a => b => a + b).ap(Box.of(10));

Key Takeaways