A monad is a monoid in the category of Endofunctors. Okay, post completed, need I say more?... Fine, okay then.
A monad is a very powerful concept allowing for data flow, transformation and control in a managed form. You can think of it as an isolated, self-contained, step within a computation stream. The key component to support this is the bind function:
(>>=) :: Monad m => m a -> (a -> m b) -> m b
You might call it map
or pipe
although both are slightly incorrect but thats just semantics.
A monad is defined as follows:
Monad, (from Greek monas “unit”), an elementary individual substance that reflects the order of the world and from which material properties are derived. The term was first used by the Pythagoreans as the name of the beginning number of a series, from which all following numbers derived.
Source: Britannica
Okay, so what about monoids vs monads?
- A monad is focused on sequencing computations and handling side effects.
- A monoid is focused on combining values, typically with an associative operation
mappend
, also known as(<>)
, and an identity initialisermempty
to create an empty instance.
What about an Endofunctor?
An Endofunctor
is a specific type of Functor
where the source and target categories are the same. In other words, it is a functor from a category to itself.
The core of all Functors is the fmap
operation:
fmap :: Functor f => (a -> b) -> f a -> f b
So where a functor might fmap
a Functor String
to a Functor Int
, an endofunctor would only ever fmap
a Functor Something
to Functor Something
.
Now you know all the components of a Monad. It's incredibly powerful and you actually use them all the time without realising it. There are many common Monads like Maybe
, Reader
, Writer
, Continuation
, etc.
An example - The Maybe
monad
The Maybe
monad is a monadic data-structure which represents the possibility of a value existing. This is useful in cases where you have computations that may return zero or one value.
For example, if I have a function stringToInt(input: string): number
then this would be wrongly typed since we cannot guarantee that the given string is a valid number and defaulting a bad string to 0
or any other number is disingenuous.
Instead we could utilise the type signature stringToInt(input: string): Maybe<number>
which is far more correct and allows us to contain the possible presence of that value cleanly for future computations to handle.
In typescript I could write the Maybe
monad as:
// Maybe Monad
class Maybe<T> {
private value: T | null;
constructor(value: T | null = null) {
this.value = value;
}
// Functor: map
map<U>(f: (value: T) => U): Maybe<U> {
return this.value !== null ? new Maybe<U>(f(this.value)) : new Maybe<U>();
}
// Monad: flatMap (bind)
flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U> {
return this.value !== null ? f(this.value) : new Maybe<U>();
}
// Example non-required methods you may wish to add
isJust(): boolean {
return this.value !== null;
}
isNothing(): boolean {
return this.value === null;
}
withDefault(fallback: T): T {
return this.value !== null ? this.value : fallback;
}
// Other more advanced methods you may wish to add
map2<U, R>(f: (ours: T, theirs: U) => Maybe<R>, other: Maybe<U>): Maybe<R> {
if(this.value !== null && other.value !== null) {
return f(this.value, other.value);
}
return new Maybe<R>();
}
}
// Example Usage
const maybeValue: Maybe<number> = new Maybe(5);
// Endofunctor example: mapping over the Maybe value
const incrementedMaybe: Maybe<number> = maybeValue.map(value => value + 1);
console.log(incrementedMaybe); // Output: Maybe { value: 6 }
// Monad example: binding Maybe with another Maybe
const doubledMaybe: Maybe<number> = maybeValue.flatMap(value => new Maybe(value * 2));
console.log(doubledMaybe); // Output: Maybe { value: 10 }
// Monad example: is empty
const empty = new Maybe<string>();
console.log(empty.isNothing()); // Output: true
console.log(empty.isJust()); // Output: false
console.log(empty.withDefault("test")); // Output: "test"
// Monad example: has value
const withValue = new Maybe<string>("abc123");
console.log(withValue.isNothing()); // Output: false
console.log(withValue.isJust()); // Output: true
console.log(withValue.withDefault("test")); // Output: "abc123"
// Combinatory example: Mapping two monadic values
const first = new Maybe<string>("abc");
const second = new Maybe<number>(123);
const third = first.map2(
(ours: string, theirs: number) => new Maybe<string>(ours + theirs.toString()),
second
);
console.log(third); // Output: Maybe { value: "abc123" }
Conclusions
Now you should understand Monads. They simply wrap values, can map between values and bind values in a computational pipeline friendly manner: When you think of a Monad, think of type transformations running over time.
I hope that you found some value in todays post and if you have any questions, comments or suggestions, feel free to leave those in the comments area below the post!
Top comments (0)