Skip to main content

🎭 Functor Functions & Mappable Structures

Understanding functors: containers that can be mapped over while preserving structure

Table of Contents

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

  1. Functors provide safe transformation - apply functions without unwrapping values
  2. Obey functor laws - identity and composition laws ensure predictable behavior
  3. JavaScript has built-in functors - Arrays, Promises, and Functions
  4. Maybe handles null values - prevents null pointer exceptions elegantly
  5. Either handles errors - functional error handling without exceptions
  6. IO manages side effects - compose effectful operations while deferring execution
  7. Pointed functors lift values - the of method brings values into functor context
  8. 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.