Created
April 5, 2024 21:08
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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