Skip to content

Instantly share code, notes, and snippets.

@mrosata
Last active April 22, 2024 07:16
Show Gist options
  • Save mrosata/fd2779cad9ec70f856890144d8e10312 to your computer and use it in GitHub Desktop.
Save mrosata/fd2779cad9ec70f856890144d8e10312 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);
}
}
@wayneseymour
Copy link

@mrosata possible to beg for a classless version? :)
I can impl it myself, but I'm wondering if there are any gotchas my naive mind has overlooked, that would reveal a need to use classes over just functions. Btw, this is an on going debate in my head...for years now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment