From the book "Mastering Functional Programming"
Created
March 6, 2022 14:22
-
-
Save jfhector/dd0639112a2dd82c59015a9f94ba79d0 to your computer and use it in GitHub Desktop.
Implementing simple Monads
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
const VALUE = Symbol('value'); | |
class Container { | |
constructor(x) { | |
this[VALUE] = x; | |
} | |
static of(x) { | |
return new Container(x); | |
} | |
map(fn) { | |
return fn(this[VALUE]); | |
} | |
toString() { | |
return `${this.constructor.name} wrapping ${this[VALUE]}` | |
} | |
valueOf() { | |
return this[VALUE]; | |
} | |
} | |
// ***************** | |
class Functor extends Container { | |
static of(x) { | |
return new Functor(x); | |
} | |
map(fn) { | |
return Functor.of(fn(this[VALUE])); | |
} | |
} | |
// ***************** | |
class Nothing extends Functor { | |
isNothing() { | |
return true; | |
} | |
toString() { | |
return "Nothing()"; | |
} | |
map(_) { | |
return this; | |
} | |
orElse(v) { | |
return v; | |
} | |
} | |
class Just extends Functor { | |
static of(x) { | |
return new Just(x); | |
} | |
isNothing() { | |
return false; | |
} | |
map(fn) { | |
return Maybe.of(fn(this[VALUE])); | |
} | |
orElse(_) { | |
return this.valueOf(); | |
} | |
} | |
class Maybe extends Functor { | |
constructor(x) { | |
return (x === undefined || x === null) ? // !! | |
new Nothing() | |
: new Just(x); | |
} | |
static of(x) { | |
return new Maybe(x); | |
} | |
} | |
// ***************** | |
class Monad extends Functor { | |
static of(x) { | |
return new Monad(x); | |
} | |
map(fn) { | |
return Monad.of(fn(this[VALUE])); | |
} | |
flatten() { | |
const value = this[VALUE]; | |
return (value instanceof Container) ? value.flatten() : this; // !! Note we're returning `this` (the container), not the unwrapped value | |
} | |
chain(fn) { | |
return this.map(fn).flatten(); | |
} | |
} | |
// ***************** | |
// TODO: Recreate Nothing, Just and Maybe so they extend Monad rather than Functor, so they have flatten and chain | |
// ***************** | |
class Left extends Monad { | |
isLeft() { | |
return true; | |
} | |
map(_) { | |
return this; | |
} | |
} | |
class Right extends Monad { | |
isLeft() { | |
return false; | |
} | |
map(fn) { | |
return Either.of(null, fn(this[VALUE])); | |
} | |
} | |
class Either extends Monad { | |
constructor(left, right) { | |
return right == undefined ? // !! If right is neither null nor undefined, return a new Right (regardless of left) | |
new Left(left) | |
: new Right(right); | |
} | |
static of(left, right) { | |
return new Either(left, right); | |
} | |
} | |
// ***************** | |
class Try extends Either { | |
constructor(thunk, message) { | |
try { | |
return new Either(null, thunk()); | |
} catch (err) { | |
return new Either(message ?? err, null) | |
} | |
} | |
static of (thunk, message) { | |
return new Try(thunk, message); | |
} | |
} | |
// ***************** | |
const cont1 = Container.of(42); | |
const funct1 = Functor.of(42); | |
const just1 = Just.of(42); | |
const maybe1 = Maybe.of(42); | |
const maybe2 = Maybe.of(null); | |
const monad1 = Monad.of(42); | |
const monadOfMonad1 = Monad.of(monad1); | |
const either1 = Either.of(null, 42) | |
const either2 = Either.of(new Error("Some error"), 42) | |
const either3 = Either.of(new Error("Some error"), null) | |
const try1 = Try.of(() => 42); | |
const try2 = Try.of(() => {throw new Error("Oh shit")}); | |
const try3 = Try.of(() => {throw new Error("Oh shit")}, "Custom error message"); | |
console.log( | |
// cont1.toString() // Container wrapping 42 | |
// cont1.valueOf() // 42 | |
// cont1.map(x => x*2) // 84 | |
// funct1.toString() // Functor wrapping 42 | |
// funct1.map(x => x*2) // Functor { [Symbol(value)]: 84 } | |
// just1 // Just { [Symbol(value)]: 42 } | |
// maybe1 // Just { [Symbol(value)]: 42 } | |
// maybe2 // Nothing { [Symbol(value)]: undefined } | |
// maybe1.map(x => x * 2) // Just { [Symbol(value)]: 84 } | |
// maybe1.map(_ => null) // Nothing { [Symbol(value)]: undefined } | |
// maybe1.orElse("Some default or placeholder value") // 42 | |
// maybe2.orElse("Some default or placeholder value") // Some default or placeholder value | |
// monad1 // Monad { [Symbol(value)]: 42 } | |
// monadOfMonad1 // Monad { [Symbol(value)]: Monad { [Symbol(value)]: 42 } } | |
// monadOfMonad1.flatten() // Monad { [Symbol(value)]: 42 } | |
// monadOfMonad1.chain(x => x * 2) // Monad { [Symbol(value)]: 84 } | |
// either1 // Right { [Symbol(value)]: 42 } | |
// either2 // Right { [Symbol(value)]: 42 } | |
// either3 // Left { | |
// [Symbol(value)]: Error: Some error | |
// at Object.<anonymous> (/Users/jfhector/Desktop/continuation_test/monads/monads.js:153:27) | |
// at Module._compile (internal/modules/cjs/loader.js:1072:14) | |
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10) | |
// at Module.load (internal/modules/cjs/loader.js:937:32) | |
// at Function.Module._load (internal/modules/cjs/loader.js:778:12) | |
// at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12) | |
// at internal/main/run_main_module.js:17:47 | |
// } | |
// ^ NOTE: This error wasn't thrown | |
// either1.map(x => x * 2) // Right { [Symbol(value)]: 84 } | |
// either3.map(x => x * 2) // Left { | |
// [Symbol(value)]: Error: Some error | |
// at Object.<anonymous> (/Users/jfhector/Desktop/continuation_test/monads/monads.js:153:27) | |
// at Module._compile (internal/modules/cjs/loader.js:1072:14) | |
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10) | |
// at Module.load (internal/modules/cjs/loader.js:937:32) | |
// at Function.Module._load (internal/modules/cjs/loader.js:778:12) | |
// at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12) | |
// at internal/main/run_main_module.js:17:47 | |
// } | |
// ^ NOTE: This error wasn't thrown | |
// either2.chain(x => x * 2) // Right { [Symbol(value)]: 84 } | |
// try1 // Right { [Symbol(value)]: 42 } | |
// try2 // Left { | |
// [Symbol(value)]: Error: Oh shit | |
// at /Users/jfhector/Desktop/continuation_test/monads/monads.js:171:34 | |
// at new Try (/Users/jfhector/Desktop/continuation_test/monads/monads.js:147:31) | |
// at Function.of (/Users/jfhector/Desktop/continuation_test/monads/monads.js:154:12) | |
// at Object.<anonymous> (/Users/jfhector/Desktop/continuation_test/monads/monads.js:171:18) | |
// at Module._compile (internal/modules/cjs/loader.js:1072:14) | |
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10) | |
// at Module.load (internal/modules/cjs/loader.js:937:32) | |
// at Function.Module._load (internal/modules/cjs/loader.js:778:12) | |
// at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12) | |
// at internal/main/run_main_module.js:17:47 | |
// } | |
// ^ NOTE: This error wasn't thrown | |
// try3 // Left { [Symbol(value)]: 'Custom error message' } | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment