To get a minimal idea, you can think about a monad as of a parametrized class: M. Its functioning follows "monad laws" that allow you to do certain things with it, and with the value(s) of T wrapped my it. In particular, you can always "map" the values:
M::map(f: (T1 -> T2)): M
List([1, 2, 3]).map(x => toString(x)) == List(["1", "2", "3"])
You can always flatten the nested structure:
M>::flatten(): M // [["a", "b"], ["c", "d"]] -> ["a", "b", "c", "d"]
This is usually expressed in a different form, more fundamental:
M::flatMap(f: (T1 => M)): M
List(["a b", "c d"]).flatMap(x => x.split()) == List(["a", "b", "c", "d"])
You can notice how that map() thing does looping over a sequence for you.
But Optional is also a monad:
let x: Optional = Some(1);
let y: Optional = Nothing;
x.map(n => n + 1).map(n => n * 2) == Some(4);
y.map(n => n + 1).map(n => n * 2) == Nothing;
As you see, the same map() (and flatMap()) does the condition checking for you. and can be chained safely.
You can also notice how chaining of map-like operations does operation sequencing:
fetch(url).then(content => content.json()).then(data => process(data))
Your language, like JS/TS, can add some
syntax sugar over it, and allow you to write it as a sequence of statements:
async () => {
const response = await fetch(url);
const data = await response.json();
process(data);
}
Promises are not
exactly monads though, a Promise> immediately transforms into Promise. But other monadic properties are still there.