Skip to content

Instantly share code, notes, and snippets.

@thelissimus
Created April 5, 2024 21:08
Show Gist options
  • Save thelissimus/608216c0fa24e458b09e7547a5d4ae49 to your computer and use it in GitHub Desktop.
Save thelissimus/608216c0fa24e458b09e7547a5d4ae49 to your computer and use it in GitHub Desktop.
An implementation of an Option with Functor, Applicative and Monad functions in TypeScript.
// NOTE: Provided Haskell signatures were simplified for learning purposes.
// Such as: renaming `<*>` as `apply`. Operators may unnecessarily confuse the
// reader of this file. Check out the documentation if you want to apply this
// knowledge in real Haskell.
type Option<A> =
| { _tag: "None" }
| { _tag: "Some", value: A }
const none = <A>(): Option<A> => ({ _tag: "None" });
const some = <A>(value: A): Option<A> => ({ _tag: "Some", value });
// Functor - transformation that preserves the structure.
//
// Current hierarchy:
// ╭───────────╮
// │ Functor │
// ╰───────────╯
//
// Pseudo Haskell signature:
// map :: (a -> b) -> Functor a -> Functor b
const map = <A, B>(f: (a: A) => B, fa: Option<A>): Option<B> => {
switch (fa._tag) {
case "Some":
// Wrapping in Some because `f` takes `A` and returns just `B`.
return some(f(fa.value));
case "None":
return none();
}
}
// Applicative - a context that allows applying a function within that context
// to the value in that context with `apply`. It also provides mechanism for
// wrapping a value into the context with `pure`. Structure must also be Functor
// in order to be able to be considered an Applicative.
//
// Current hierarchy:
// ╭───────────╮ ╭─────────────╮
// │ Functor │->│ Applicative │
// ╰───────────╯ ╰─────────────╯
//
// Pseudo Haskell signature:
// pure :: a -> Applicative a
// apply :: Applicative (a -> b) -> Applicative a -> Applicative b
const pure = <A>(value: A): Option<A> => some(value);
const apply = <A, B>(f: Option<(a: A) => B>, fa: Option<A>): Option<B> => {
switch (f._tag) {
case "Some":
// `f.value` - a function. We can re-use the `map`, because we
// implemented Functor before.
return map(f.value, fa);
// Otherwise we could have to write (which is just a duplicate of the
// above Functor implementation):
/*
switch (fa._tag) {
case "Some":
return some(f.value(fa.value));
case "None":
return none();
}
*/
case "None":
return none();
}
}
// Monad - a structure, that if nested can be flattened. Structure must also be
// Applicative in order to be able to be considered a Monad.
//
// Current hierarchy:
// ╭───────────╮ ╭─────────────╮ ╭─────────╮
// │ Functor │->│ Applicative │->│ Monad │
// ╰───────────╯ ╰─────────────╯ ╰─────────╯
//
// Pseudo Haskell signature:
// flatMap :: (a -> Monad b) -> Monad a -> Monad b
//
// Notice how similar it is to the signature of `map`? See for yourself:
// map :: (a -> b) -> Functor a -> Functor b
// flatMap :: (a -> Monad b) -> Monad a -> Monad b
const flatMap = <A, B>(f: (a: A) => Option<B>, fa: Option<A>): Option<B> => {
switch (fa._tag) {
case "Some":
// Now we don't need to wrap in Some because `f` takes `A` and returns
// `Option<B>` already.
return f(fa.value);
// The difference in signature also makes difference in implementation.
// return some(f(fa.value)); // Functor
// return f(fa.value); // Monad
case "None":
return none();
}
}
// That is it. Now you know the basics of a Functor, an Applicative and a Monad.
// No need for overly complex math-o-category-theoretic Quatsch :)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment