🎭 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.