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:
of(orpure) - Wraps a value in the functorap(apply) - Applies a wrapped function to a wrapped value
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
mapandap - Applies wrapped functions to wrapped values
- Example:
Box(5).map(add).ap(Box(10))
Monad:
- Has
map,ap, andflatMap - 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
- Functors implement
mapto transform wrapped values - Functors must follow identity and composition laws
- Applicatives add
apto apply wrapped functions to wrapped values - Applicatives let you combine multiple wrapped values
- Use applicatives for parallel operations and independent validations
- Curry functions to use them with applicatives
- Every monad is an applicative, every applicative is a functor