Skip to content

Instantly share code, notes, and snippets.

@miladvafaeifard
Forked from mrosata/fp-either-monad.js
Created September 21, 2023 16:51
Show Gist options
  • Save miladvafaeifard/af8b2e29be4d3a218edc078f14d72d29 to your computer and use it in GitHub Desktop.
Save miladvafaeifard/af8b2e29be4d3a218edc078f14d72d29 to your computer and use it in GitHub Desktop.
Functional JavaScript Monad Classes - (Maybe Just Nothing) - (Either Left Right) (IOMonad) and my type checking utils
import is from './is-util';
/**
* Either Monad class (from Functional Programming in JavaScript)
*/
class Either {
constructor(value) {
this._value = value;
}
get value () {
return this._value;
}
static fromNullable (a) {
// Book doesn't check if a is undefined. Am I wrong to check for undefined?
return !is.defined(a) || is.none(a) ? Either.left(a) : Either.right(a);
}
static left (a) {
return new Left(a);
}
static right (a) {
return new Right(a);
}
static of(a) {
return Either.right(a);
}
}
/*
* Left class is meant to handle errors or possible unexpected results.
* (Left is a bit similar to Nothing)
*/
class Left extends Either {
constructor(unexpectedResult) {
super(unexpectedResult);
}
map (fn) {
return this;
}
get value () {
throw new TypeError(`Can't extract value from a Left() monad`);
}
getOrElse (other) {
return other;
}
orElse (fn) {
return fn(this._value);
}
getOrElseThrow (a) {
throw new Error(a);
}
filter (fn) {
return this;
}
get isNothing () {
return true;
}
toString () {
return `[object Either.Left] (value: ${this._value})`;
}
}
/**
* Right monad used for handling valid/expected types of results
*/
class Right extends Either {
constructor (value) {
super(value);
}
get value () {
return this._value;
}
getOrElse () {
return this.value;
}
orElse () {
return this;
}
getOrElseThrow (a) {
return this.value;
}
chain (fn) {
return fn(this.value);
}
filter (fn) {
return Either.fromNullable(fn(this.value) ? this.value : null);
}
toString() {
return `[object Either.Right] (value: ${this._value})`;
}
}
import is from './is-util';
/**
* The IOMonad class (from Functional Programming in JavaScript)
*/
export class IOMonad {
constructor (effect) {
if ( !is.callable(effect) ) {
throw new Error(`IOMonad expects a callable effect! Saw ${typeof effect}`);
}
this.effect = effect;
}
static of (a) {
return new IOMonad( () => a );
}
static from (fn) {
return new IOMonad( fn );
}
map (fn) {
return new IOMonad( () => fn(this.effect()) );
}
chain (fn) {
return fn( this.effect );
}
run () {
return this.effect();
}
}
import is from './is-util';
/**
* This is the Maybe monad class (from Functional Programming in JavaScript).
* (Errata found in code has been fixed)
* Factory `Maybe.fromNullable` lifts a value into either a `Just`
* or a `Nothing` monad class.
*/
class Maybe {
constructor() {}
static fromNullable (a) {
return !is.defined(a) || is.none(a) ? Maybe.nothing() : Maybe.just(a);
}
static nothing() {
return new Nothing();
}
static just(a) {
return new Just(a);
}
static of(a) {
return Maybe.just(a);
}
get isNothing() {
return false;
}
get isJust() {
return false;
}
}
/*
* Nothing class wraps undefined or null values and prevents errors
* that otherwise occur when mapping unexpected undefined or null
* values
*/
class Nothing extends Maybe {
get value () {
throw new TypeError(`Can't extract value from Nothing`);
}
getOrElse (other) {
return other;
}
map (fn) {
return this;
}
filter () {
return this.value;
}
get isNothing () {
return true;
}
toString () {
return `[object Maybe.Nothing] (value: ${this._value})`;
}
}
/**
* Monad Just used for valid values
*/
class Just extends Maybe {
constructor (value) {
super(...arguments);
this._value = value;
}
get value () {
return this._value;
}
getOrElse () {
return this.value;
}
map (fn) {
return Maybe.of(fn(this.value));
}
filter (fn) {
return Maybe.fromNullable(fn(this.value) ? this.value : null);
}
get isJust () {
return true;
}
toString() {
return `[object Maybe.Just] (value: ${this._value})`;
}
}
/**
* Base Wrapper Monad class (from Functional Programming in JavaScript)
* (Errata found in code has been fixed)
*/
class Wrapper {
constructor (value) {
this._value = value;
}
static of (a) {
return new Wrapper(a);
}
map (f) {
return Wrapper.of( f(this._value) );
}
join () {
return this._value instanceof Wrapper ? this.value.join() : this;
}
toString () {
return `[object Wrapper] (value: ${this._value})`
}
}
/**
* Utilities for testing values for type
* ```
* if ( is.none(val) ) { "Value is null" }
* if ( is.object(val) ) { "Value is a real object" }
* if ( is.objectType(val) ) { "Value is of type object" }
* if ( is.array(val) ) { "Value is an Array" }
* if ( is.callable(val) ) { "Value is a Function" }
* if ( is.defined(val) ) { "Value is not undefined" }
* if ( is.string(val) ) { "Value is a String" }
* if ( is.number(val) ) { "Value is a number" }
* if ( is.a(val, B) ) { "Value is of type B" }
* if ( is.thenable(val) ) { "Value is has then function" }
* ```
*/
export default class is {
static none(testValue) {
return !testValue && typeof testValue === "object";
}
static objectType(testValue) {
return !!testValue && typeof testValue === "object";
}
static object(testValue) {
return is.objectType(testValue) && testValue.constructor !== Array;
}
static array(testValue) {
return is.objectType(testValue) && testValue.constructor === Array;
}
static callable(testValue) {
return typeof testValue === "function";
}
static defined(testValue) {
return typeof testValue !== "undefined";
}
static string(testValue) {
return typeof testValue === "string";
}
static number(testValue) {
return typeof testValue === "number" && !Number.isNaN(testValue);
}
// Class
static a(testValue, typeConstructor) {
return (is.objectType(testValue) || is.callable(testValue)) && testValue.constructor === typeConstructor;
}
// Promise
static thenable(testValue) {
return is.objectType(testValue) && is.callable(testValue.then);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment