🎭 Functor Functions & Mappable Structures
Understanding functors: containers that can be mapped over while preserving structure
Table of Contents
- What is a Functor?
- Functor Laws
- Built-in JavaScript Functors
- Creating Custom Functors
- Maybe Functor
- Either Functor
- IO Functor
- Pointed Functors
- Real-World Applications
- Advanced Functor Patterns
What is a Functor?
A functor is a type that implements a map method which applies a function to the wrapped value(s) while preserving the container's structure. Think of it as a "mappable" container.
Mathematical Definition
In category theory, a functor F must satisfy:
F.map(id) = id(Identity law)F.map(compose(f, g)) = compose(F.map(f), F.map(g))(Composition law)
Basic Functor Interface
// Generic functor interface
class Functor {
constructor(value) {
this.value = value;
}
// The map method is what makes it a functor
map(fn) {
return new Functor(fn(this.value));
}
// Static constructor (pointed functor)
static of(value) {
return new Functor(value);
}
}
// Usage
const result = Functor.of(5)
.map((x) => x * 2)
.map((x) => x + 1)
.map((x) => `Result: ${x}`);
console.log(result.value); // "Result: 11"
Why Functors Matter
Functors provide a consistent way to:
- Apply transformations without unwrapping values
- Chain operations safely
- Handle context (null values, errors, async operations)
- Maintain structure while transforming content
Functor Laws
All functors must obey two fundamental laws:
1. Identity Law
Mapping the identity function should return the original functor.
const identity = (x) => x;
// Law: functor.map(identity) === functor
const original = Functor.of(42);
const mapped = original.map(identity);
console.log(original.value === mapped.value); // true
2. Composition Law
Mapping a composition should equal composing the maps.
const compose = (f, g) => (x) => f(g(x));
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const functor = Functor.of(5);
// These should be equivalent:
const composed = functor.map(compose(double, addOne));
const chained = functor.map(addOne).map(double);
console.log(composed.value === chained.value); // true
Verifying Functor Laws
// ✅ Law verification utility
const verifyFunctorLaws = (FunctorType, value, f, g) => {
const functor = FunctorType.of(value);
const identity = (x) => x;
const compose = (fn1, fn2) => (x) => fn1(fn2(x));
// Identity law
const identityTest = functor.map(identity).value === functor.value;
// Composition law
const composed = functor.map(compose(f, g));
const chained = functor.map(g).map(f);
const compositionTest = composed.value === chained.value;
return {
identity: identityTest,
composition: compositionTest,
valid: identityTest && compositionTest,
};
};
// Test our Functor
const result = verifyFunctorLaws(
Functor,
10,
(x) => x * 2,
(x) => x + 5,
);
console.log(result); // { identity: true, composition: true, valid: true }
Built-in JavaScript Functors
JavaScript has several built-in functors that you use every day:
Array as a Functor
// ✅ Arrays are functors
const numbers = [1, 2, 3, 4, 5];
const result = numbers
.map((x) => x * 2) // [2, 4, 6, 8, 10]
.map((x) => x + 1) // [3, 5, 7, 9, 11]
.map((x) => `${x}!`); // ["3!", "5!", "7!", "9!", "11!"]
console.log(result);
// Arrays preserve structure and apply function to each element
const nested = [
[1, 2],
[3, 4],
[5, 6],
];
const flattened = nested.map((arr) => arr.map((x) => x * 2));
console.log(flattened); // [[2, 4], [6, 8], [10, 12]]
Promise as a Functor
// ✅ Promises are functors
const asyncValue = Promise.resolve(42);
const result = asyncValue
.then((x) => x * 2) // Still a Promise
.then((x) => x + 8) // Still a Promise
.then((x) => `${x}!`); // Still a Promise
result.then(console.log); // "92!"
// Promises preserve the async context while transforming the value
Function as a Functor
// ✅ Functions can be functors (function composition)
const addTen = (x) => x + 10;
const multiplyByTwo = (x) => x * 2;
// Function composition is like mapping over functions
const composedFunction = (value) => multiplyByTwo(addTen(value));
console.log(composedFunction(5)); // 30
// More explicit functor-like interface for functions
class FunctionFunctor {
constructor(fn) {
this.fn = fn;
}
map(g) {
return new FunctionFunctor((x) => g(this.fn(x)));
}
run(input) {
return this.fn(input);
}
static of(fn) {
return new FunctionFunctor(fn);
}
}
const pipeline = FunctionFunctor.of((x) => x + 5)
.map((x) => x * 2)
.map((x) => x - 3);
console.log(pipeline.run(10)); // ((10 + 5) * 2) - 3 = 27
Creating Custom Functors
Container Functor
// ✅ Basic container functor
class Container {
constructor(value) {
this.value = value;
}
map(fn) {
return Container.of(fn(this.value));
}
static of(value) {
return new Container(value);
}
// Utility method for debugging
inspect() {
return `Container(${this.value})`;
}
}
// Usage
const container = Container.of('Hello World')
.map((s) => s.toUpperCase())
.map((s) => s.split(' '))
.map((arr) => arr.reverse())
.map((arr) => arr.join(' '));
console.log(container.inspect()); // Container(WORLD HELLO)
Box Functor with Debugging
// ✅ Box functor with enhanced debugging
class Box {
constructor(value) {
this.value = value;
this.history = [];
}
map(fn, description = 'transform') {
const newValue = fn(this.value);
const newBox = Box.of(newValue);
newBox.history = [
...this.history,
{
operation: description,
from: this.value,
to: newValue,
function: fn.toString(),
},
];
return newBox;
}
static of(value) {
return new Box(value);
}
fold(fn) {
return fn(this.value);
}
inspect() {
return `Box(${JSON.stringify(this.value)})`;
}
trace() {
console.log('Transformation history:');
this.history.forEach((step, i) => {
console.log(`${i + 1}. ${step.operation}: ${step.from} -> ${step.to}`);
});
return this;
}
}
// Usage with tracing
const result = Box.of(10)
.map((x) => x + 5, 'add 5')
.map((x) => x * 2, 'multiply by 2')
.map((x) => x - 3, 'subtract 3')
.trace()
.fold((x) => `Final: ${x}`);
console.log(result); // "Final: 27"
Maybe Functor
The Maybe functor handles null/undefined values gracefully, preventing null pointer exceptions.
Basic Maybe Implementation
// ✅ Maybe functor for null safety
class Maybe {
constructor(value) {
this.value = value;
}
isNothing() {
return this.value === null || this.value === undefined;
}
isSomething() {
return !this.isNothing();
}
map(fn) {
return this.isNothing() ? Maybe.nothing() : Maybe.of(fn(this.value));
}
flatMap(fn) {
return this.isNothing() ? Maybe.nothing() : fn(this.value);
}
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this.value;
}
filter(predicate) {
return this.isNothing() || predicate(this.value) ? this : Maybe.nothing();
}
static of(value) {
return new Maybe(value);
}
static nothing() {
return new Maybe(null);
}
static some(value) {
return value === null || value === undefined ? Maybe.nothing() : Maybe.of(value);
}
inspect() {
return this.isNothing() ? 'Maybe.Nothing' : `Maybe.Some(${this.value})`;
}
}
// Usage examples
const safeDiv = (a, b) => (b === 0 ? Maybe.nothing() : Maybe.of(a / b));
const result1 = Maybe.of(20)
.flatMap((x) => safeDiv(x, 2))
.map((x) => x + 5)
.map((x) => `Result: ${x}`)
.getOrElse('Error: Division by zero');
console.log(result1); // "Result: 15"
const result2 = Maybe.of(20)
.flatMap((x) => safeDiv(x, 0)) // Returns Maybe.nothing()
.map((x) => x + 5) // Skipped
.map((x) => `Result: ${x}`) // Skipped
.getOrElse('Error: Division by zero');
console.log(result2); // "Error: Division by zero"
Maybe with Real-World Examples
// ✅ Real-world Maybe usage
const users = [
{ id: 1, name: 'John', address: { street: '123 Main St', city: 'Boston' } },
{ id: 2, name: 'Jane', address: null },
{ id: 3, name: 'Bob' },
];
// Safe property access
const getUserCity = (user) =>
Maybe.of(user)
.map((u) => u.address)
.map((addr) => addr.city)
.getOrElse('Unknown City');
users.forEach((user) => {
console.log(`${user.name} lives in: ${getUserCity(user)}`);
});
// John lives in: Boston
// Jane lives in: Unknown City
// Bob lives in: Unknown City
// Safe API response handling
const processApiResponse = (response) =>
Maybe.of(response)
.filter((r) => r.success)
.map((r) => r.data)
.map((data) => data.users)
.map((users) => users.filter((u) => u.active))
.getOrElse([]);
const apiResponse1 = { success: true, data: { users: [{ active: true, name: 'Alice' }] } };
const apiResponse2 = { success: false, error: 'Server error' };
console.log(processApiResponse(apiResponse1)); // [{ active: true, name: 'Alice' }]
console.log(processApiResponse(apiResponse2)); // []
Either Functor
The Either functor handles computations that might fail, representing either a success (Right) or failure (Left).
Either Implementation
// ✅ Either functor for error handling
class Either {
constructor(value, isRight = true) {
this.value = value;
this.isRight = isRight;
}
isLeft() {
return !this.isRight;
}
map(fn) {
return this.isLeft() ? this : Either.right(fn(this.value));
}
mapLeft(fn) {
return this.isLeft() ? Either.left(fn(this.value)) : this;
}
flatMap(fn) {
return this.isLeft() ? this : fn(this.value);
}
fold(leftFn, rightFn) {
return this.isLeft() ? leftFn(this.value) : rightFn(this.value);
}
getOrElse(defaultValue) {
return this.isLeft() ? defaultValue : this.value;
}
static left(value) {
return new Either(value, false);
}
static right(value) {
return new Either(value, true);
}
static of(value) {
return Either.right(value);
}
static try(fn) {
try {
return Either.right(fn());
} catch (error) {
return Either.left(error.message);
}
}
inspect() {
return this.isLeft() ? `Either.Left(${this.value})` : `Either.Right(${this.value})`;
}
}
// Usage examples
const parseJSON = (str) => Either.try(() => JSON.parse(str));
const processData = (jsonString) =>
parseJSON(jsonString)
.map((obj) => obj.user)
.map((user) => user.name)
.map((name) => name.toUpperCase())
.map((name) => `Hello, ${name}!`)
.fold(
(error) => `Error: ${error}`,
(result) => result,
);
console.log(processData('{"user":{"name":"alice"}}')); // "Hello, ALICE!"
console.log(processData('invalid json')); // "Error: Unexpected token..."
Either for Validation
// ✅ Either for validation chains
const validateEmail = (email) => {
if (!email) return Either.left('Email is required');
if (!email.includes('@')) return Either.left('Email must contain @');
if (email.length < 5) return Either.left('Email too short');
return Either.right(email);
};
const validateAge = (age) => {
if (age === undefined) return Either.left('Age is required');
if (age < 0) return Either.left('Age must be positive');
if (age > 150) return Either.left('Age must be realistic');
return Either.right(age);
};
const createUser = (email, age) =>
validateEmail(email).flatMap((validEmail) =>
validateAge(age).map((validAge) => ({
email: validEmail,
age: validAge,
id: Math.random(),
})),
);
// Test validation
console.log(createUser('john@example.com', 25)); // Either.Right({...})
console.log(createUser('invalid', 25)); // Either.Left('Email must contain @')
console.log(createUser('john@example.com', -5)); // Either.Left('Age must be positive')
IO Functor
The IO functor wraps side-effectful operations, allowing you to compose them while deferring execution.
IO Implementation
// ✅ IO functor for side effects
class IO {
constructor(effect) {
this.effect = effect;
}
map(fn) {
return new IO(() => fn(this.effect()));
}
flatMap(fn) {
return new IO(() => fn(this.effect()).effect());
}
run() {
return this.effect();
}
static of(value) {
return new IO(() => value);
}
static from(effect) {
return new IO(effect);
}
}
// Side-effect functions wrapped in IO
const readFile = (filename) =>
IO.from(() => {
console.log(`Reading file: ${filename}`);
return `Contents of ${filename}`;
});
const writeFile = (filename, content) =>
IO.from(() => {
console.log(`Writing to file: ${filename}`);
console.log(`Content: ${content}`);
return `Written to ${filename}`;
});
const log = (message) =>
IO.from(() => {
console.log(`LOG: ${message}`);
return message;
});
// Compose IO operations without executing them
const fileOperation = readFile('input.txt')
.map((content) => content.toUpperCase())
.flatMap((content) => log(`Processing: ${content.slice(0, 20)}...`))
.map((content) => content + '\n\nProcessed by IO Functor')
.flatMap((content) => writeFile('output.txt', content));
// Execute the composed operation
console.log('About to run IO operations...');
const result = fileOperation.run();
console.log('Final result:', result);
Pointed Functors
A pointed functor is a functor with an of method (also called pure or return) that can lift a value into the functor context.
Pointed Functor Interface
// ✅ Generic pointed functor
class Pointed {
constructor(value) {
this.value = value;
}
map(fn) {
return Pointed.of(fn(this.value));
}
// The 'of' method makes it a pointed functor
static of(value) {
return new Pointed(value);
}
// Additional utility methods
flatMap(fn) {
return fn(this.value);
}
apply(functor) {
return functor.map(this.value);
}
}
// All the functors we've seen are pointed functors
console.log(Container.of); // Function
console.log(Maybe.of); // Function
console.log(Either.of); // Function
console.log(IO.of); // Function
Applicative Functor Pattern
// ✅ Applicative functor for multi-argument functions
class Applicative {
constructor(value) {
this.value = value;
}
map(fn) {
return Applicative.of(fn(this.value));
}
apply(functor) {
return functor.map(this.value);
}
static of(value) {
return new Applicative(value);
}
}
// Lift multi-argument functions
const add = (a) => (b) => (c) => a + b + c;
const result = Applicative.of(add)
.apply(Applicative.of(1))
.apply(Applicative.of(2))
.apply(Applicative.of(3));
console.log(result.value); // 6
// More practical example with validation
const validateAndCreate = (name) => (email) => (age) => ({
name,
email,
age,
valid: true,
});
const user = Maybe.of(validateAndCreate)
.apply(Maybe.of('John'))
.apply(Maybe.of('john@example.com'))
.apply(Maybe.of(25));
console.log(user.inspect()); // Maybe.Some({name: 'John', ...})
Real-World Applications
Safe DOM Manipulation
// ✅ Safe DOM operations with Maybe
const $ = (selector) => Maybe.of(document.querySelector(selector));
const updateUserProfile = (userId) =>
$(`#user-${userId}`)
.map((element) => element.querySelector('.name'))
.map((nameEl) => nameEl.textContent)
.map((name) => name.trim())
.filter((name) => name.length > 0)
.map((name) => `Welcome, ${name}!`)
.map((welcome) => {
$(`#welcome-${userId}`).map((el) => (el.textContent = welcome));
return welcome;
})
.getOrElse('User not found');
// Safe event handling
const handleClick = (selector, handler) =>
$(selector)
.map((element) => element.addEventListener('click', handler))
.getOrElse(console.warn(`Element ${selector} not found`));
Async Operations with Promise Functors
// ✅ Promise as functor for async operations
const fetchUser = (id) => fetch(`/api/users/${id}`).then((response) => response.json());
const fetchUserPosts = (userId) =>
fetch(`/api/users/${userId}/posts`).then((response) => response.json());
// Compose async operations
const getUserWithPosts = (id) =>
fetchUser(id)
.then((user) => fetchUserPosts(id).then((posts) => ({ ...user, posts })))
.then((userData) => ({
...userData,
summary: `${userData.name} has ${userData.posts.length} posts`,
}))
.catch((error) => ({ error: error.message }));
// Usage
getUserWithPosts(123)
.then((result) => console.log(result))
.catch((error) => console.error(error));
Configuration Pipeline
// ✅ Configuration with Either functor
const loadConfig = () =>
Either.try(() => {
// Simulate config loading
return {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
};
});
const validateConfig = (config) => {
if (!config.apiUrl) return Either.left('API URL is required');
if (config.timeout < 1000) return Either.left('Timeout too low');
if (config.retries < 0) return Either.left('Retries cannot be negative');
return Either.right(config);
};
const enrichConfig = (config) =>
Either.right({
...config,
version: '1.0.0',
timestamp: new Date().toISOString(),
});
const initializeApp = () =>
loadConfig()
.flatMap(validateConfig)
.flatMap(enrichConfig)
.fold(
(error) => console.error(`Config error: ${error}`),
(config) => {
console.log('App initialized with config:', config);
return config;
},
);
initializeApp();
Advanced Functor Patterns
Functor Composition
// ✅ Compose functors
const compose = (F, G) => ({
of: (value) => F.of(G.of(value)),
map: (fn) => (functor) => F.map(G.map(fn))(functor),
});
// Compose Maybe and Array
const MaybeArray = compose(Maybe, Array);
const result = Maybe.of([1, 2, 3, 4])
.map((arr) => arr.map((x) => x * 2))
.map((arr) => arr.filter((x) => x > 4));
console.log(result.inspect()); // Maybe.Some([6, 8])
Functor Transformers
// ✅ Functor transformer for nested contexts
class MaybeT {
constructor(innerFunctor) {
this.innerFunctor = innerFunctor;
}
map(fn) {
return new MaybeT(this.innerFunctor.map((maybe) => maybe.map(fn)));
}
static lift(innerFunctor) {
return new MaybeT(innerFunctor.map(Maybe.of));
}
run() {
return this.innerFunctor;
}
}
// Usage with Promise<Maybe<T>>
const asyncMaybe = MaybeT.lift(Promise.resolve(42));
const result = asyncMaybe.map((x) => x * 2).map((x) => x + 10);
result.run().then((maybe) => console.log(maybe.inspect())); // Maybe.Some(94)
Covariant and Contravariant Functors
// ✅ Covariant functor (normal functor)
class Covariant {
constructor(value) {
this.value = value;
}
map(fn) {
return new Covariant(fn(this.value));
}
static of(value) {
return new Covariant(value);
}
}
// ✅ Contravariant functor (transforms input, not output)
class Contravariant {
constructor(predicate) {
this.predicate = predicate;
}
contramap(fn) {
return new Contravariant((value) => this.predicate(fn(value)));
}
test(value) {
return this.predicate(value);
}
static of(predicate) {
return new Contravariant(predicate);
}
}
// Example: string length validator
const minLength = (min) => Contravariant.of((str) => str.length >= min);
const emailValidator = minLength(5).contramap((email) => email.trim());
console.log(emailValidator.test(' test@example.com ')); // true
console.log(emailValidator.test(' a@b ')); // false
🎯 Key Takeaways
- Functors provide safe transformation - apply functions without unwrapping values
- Obey functor laws - identity and composition laws ensure predictable behavior
- JavaScript has built-in functors - Arrays, Promises, and Functions
- Maybe handles null values - prevents null pointer exceptions elegantly
- Either handles errors - functional error handling without exceptions
- IO manages side effects - compose effectful operations while deferring execution
- Pointed functors lift values - the
ofmethod brings values into functor context - Composition is powerful - combine functors for complex data transformations
Functors are essential for functional programming, providing a consistent interface for transforming values while preserving structure and handling context.