Monads are a fundamental concept in functional programming that provide a way to handle computations and data transformations in a structured manner. There are various types of monads, each designed to solve specific problems and handle different kinds of data and effects.
What is a Monad?
A monad is an abstraction that allows for the chaining of operations on wrapped values. It is defined by three primary properties:
-
Unit (also called
of
orreturn
): A function that takes a value and wraps it in a monad. -
Bind (also called
flatMap
orchain
): A function that takes a monadic value and a function that returns a monad, applies the function to the wrapped value, and returns a new monad. - Associativity: The composition of monadic operations should be associative.
Common Types of Monads
- Maybe Monad
- Either Monad
- Promise Monad
- List Monad
- Reader Monad
- Writer Monad
- State Monad
1. Maybe Monad
The Maybe Monad is used to handle optional values. It represents a computation that might fail or return null
or undefined
.
Implementation
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() ? this : Maybe.of(fn(this.value));
}
flatMap(fn) {
return this.isNothing() ? this : fn(this.value);
}
}
// Usage
const maybeValue = Maybe.of('hello')
.map(str => str.toUpperCase())
.flatMap(str => Maybe.of(`${str} WORLD`));
console.log(maybeValue); // Maybe { value: 'HELLO WORLD' }
2. Either Monad
The Either Monad is used to handle computations that can return either a success value (Right
) or an error value (Left
).
Implementation
class Either {
constructor(value, isRight = true) {
this.value = value;
this.isRight = isRight;
}
static right(value) {
return new Either(value, true);
}
static left(value) {
return new Either(value, false);
}
map(fn) {
return this.isRight ? Either.right(fn(this.value)) : this;
}
flatMap(fn) {
return this.isRight ? fn(this.value) : this;
}
}
// Usage
const rightValue = Either.right(5)
.map(x => x + 1)
.flatMap(x => Either.right(x * 2));
console.log(rightValue); // Either { value: 12, isRight: true }
const leftValue = Either.left('error')
.map(x => x + 1)
.flatMap(x => Either.right(x * 2));
console.log(leftValue); // Either { value: 'error', isRight: false }
3. Promise Monad
The Promise Monad is used to handle asynchronous computations.
Usage
const fetchData = url => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Data from ${url}`);
}, 1000);
});
};
// Usage
fetchData('https://api.example.com')
.then(data => {
console.log(data); // 'Data from https://api.example.com'
return fetchData('https://api.example.com/2');
})
.then(data => {
console.log(data); // 'Data from https://api.example.com/2'
})
.catch(error => {
console.error(error);
});
4. List Monad
The List Monad is used to handle computations that produce a list of values.
Implementation
class List {
constructor(values) {
this.values = values;
}
static of(values) {
return new List(values);
}
map(fn) {
return List.of(this.values.map(fn));
}
flatMap(fn) {
return List.of(this.values.flatMap(value => fn(value).values));
}
}
// Usage
const list = List.of([1, 2, 3])
.map(x => x + 1)
.flatMap(x => List.of([x, x * 2]));
console.log(list); // List { values: [ 2, 4, 3, 6, 4, 8 ] }
5. Reader Monad
The Reader Monad is used to handle computations that depend on some shared environment or configuration.
Implementation
class Reader {
constructor(fn) {
this.fn = fn;
}
static of(value) {
return new Reader(() => value);
}
map(fn) {
return new Reader(env => fn(this.fn(env)));
}
flatMap(fn) {
return new Reader(env => fn(this.fn(env)).fn(env));
}
run(env) {
return this.fn(env);
}
}
// Usage
const config = { baseURL: 'https://api.example.com' };
const fetchUser = new Reader(env => `${env.baseURL}/user`);
const fetchPosts = new Reader(env => `${env.baseURL}/posts`);
const fetchUserAndPosts = fetchUser.flatMap(userURL =>
fetchPosts.map(postsURL => ({ userURL, postsURL }))
);
console.log(fetchUserAndPosts.run(config));
// { userURL: 'https://api.example.com/user', postsURL: 'https://api.example.com/posts' }
6. Writer Monad
The Writer Monad is used to handle computations that produce a value along with a log or additional data.
Implementation
class Writer {
constructor(value, log) {
this.value = value;
this.log = log;
}
static of(value) {
return new Writer(value, '');
}
map(fn) {
const result = fn(this.value);
return new Writer(result.value, this.log + result.log);
}
flatMap(fn) {
const result = fn(this.value);
return new Writer(result.value, this.log + result.log);
}
tell(log) {
return new Writer(this.value, this.log + log);
}
}
// Usage
const writer = Writer.of(3)
.map(value => new Writer(value + 1, 'Incremented\n'))
.flatMap(value => new Writer(value * 2, 'Doubled\n'));
console.log(writer);
// Writer { value: 8, log: 'Incremented\nDoubled\n' }
7. State Monad
The State Monad is used to handle computations that maintain state.
Implementation
class State {
constructor(runState) {
this.runState = runState;
}
static of(value) {
return new State(state => [value, state]);
}
map(fn) {
return new State(state => {
const [value, newState] = this.runState(state);
return [fn(value), newState];
});
}
flatMap(fn) {
return new State(state => {
const [value, newState] = this.runState(state);
return fn(value).runState(newState);
});
}
run(initialState) {
return this.runState(initialState);
}
}
// Usage
const increment = new State(state => [state + 1, state + 1]);
const result = increment
.flatMap(() => increment)
.flatMap(() => increment)
.run(0);
console.log(result); // [3, 3]
Conclusion
Monads provide a structured and predictable way to handle computations and data transformations in functional programming. Each type of monad serves a specific purpose, from handling optional values with the Maybe Monad to managing asynchronous operations with the Promise Monad.
Top comments (0)