Last active
June 27, 2021 13:51
-
-
Save moatorres/139e8c2abd1c55d6d9dce6d3a5ef21b1 to your computer and use it in GitHub Desktop.
Maybe monad in JS/Node (with ES6 classes)
This file contains hidden or 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
type CallbackFunction<T = unknown> = (...params: unknown[]) => T | |
export class Maybe<T> { | |
private constructor(private value: T | null) {} | |
static just<T>(a: any): Maybe<T> | |
static nothing<T>(): Maybe<T> | |
static fromValue<T>(value: T | undefined | null): Maybe<T> | |
isJust(): boolean | |
isNothing(): boolean | |
toString(): string | |
has(): boolean | |
getOrElse<R = T>(defaultValue: T | R): T | R | |
getOrExecute(defaultValue: CallbackFunction<T>): T | |
map<R>(f: (wrapped: T) => R): Maybe<R> | |
tap(f: (wrapped: T) => void): Maybe<T> | |
flatMap<R>(f: (wrapped: T) => any): Maybe<R> | |
getOrThrow(error?: Error): T | |
} |
This file contains hidden or 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 { isNumberZero, isFalse, isEmptyString, isNullOrUndefined, isValid } = require('./utils') | |
class Maybe { | |
constructor(value) { | |
this.value = value | |
} | |
static just(value) { | |
if (!isValid(value)) { | |
throw new Error('Just can not have an null or undefined value') | |
} | |
return new Just(value) | |
} | |
static nothing() { | |
return new Nothing() | |
} | |
static fromNullable(a) { | |
return isValid(a) ? Maybe.just(a) : Maybe.nothing() | |
} | |
static fromValue(a) { | |
return this.fromNullable(a) | |
} | |
static of(a) { | |
return this.fromNullable(a) | |
} | |
has() { | |
return this.value !== null && this.value !== undefined | |
} | |
getOrExecute(defaultValue) { | |
return this.value === null ? defaultValue() : this.value | |
} | |
flatMap(f) { | |
if (this.value === null || this.value === undefined) return Maybe.nothing() | |
else return f(this.value) | |
} | |
getOrThrow(error) { | |
return this.value === null || this.value === undefined | |
? (() => { | |
if (error !== undefined) throw error | |
throw new Error() | |
})() | |
: this.value | |
} | |
} | |
class Just extends Maybe { | |
map(f) { | |
return Maybe.of(f(this.value)) | |
} | |
tap(f) { | |
if (isValid(this.value)) f(this.value) | |
return Maybe.fromValue(this.value) | |
} | |
getOrElse() { | |
return this.value | |
} | |
filter(f) { | |
Maybe.fromNullable(f(this.value) ? this.value : null) | |
} | |
toString() { | |
return `Maybe.Just(${this.value})` | |
} | |
isJust() { | |
return true | |
} | |
isNothing() { | |
return false | |
} | |
} | |
class Nothing extends Maybe { | |
map() { | |
return this | |
} | |
getOrElse(other) { | |
return other | |
} | |
filter() { | |
return this.value | |
} | |
tap() { | |
return Maybe.nothing() | |
} | |
toString() { | |
return 'Maybe.Nothing()' | |
} | |
isNothing() { | |
return true | |
} | |
isJust() { | |
return false | |
} | |
} | |
module.exports = { Maybe } |
This file contains hidden or 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 { Maybe } = require('./Maybe') | |
describe('Maybe', () => { | |
it('Should handle a value', () => { | |
const maybe = Maybe.just('test') | |
expect(maybe.getOrElse('')).toBe('test') | |
}) | |
it('Should handle an undefined value', () => { | |
const maybe = Maybe.fromValue(undefined) | |
expect(maybe.getOrElse('test')).toBe('test') | |
}) | |
it('Should handle a false value', () => { | |
const maybe = Maybe.fromValue(false) | |
expect(maybe.getOrElse(true)).toBe(false) | |
}) | |
it('Should handle a string value', () => { | |
const maybe = Maybe.fromValue('test') | |
expect(maybe.getOrElse('')).toBe('test') | |
}) | |
it('Should handle an empty string value', () => { | |
const maybe = Maybe.fromValue('') | |
expect(maybe.getOrElse('test')).toBe('') | |
}) | |
it('Should return a default value with a different type', () => { | |
const maybe = Maybe.fromValue(null) | |
expect(maybe.getOrElse(null)).toBe(null) | |
}) | |
it('Should handle a null value', () => { | |
const maybe = Maybe.fromValue(null) | |
expect(maybe.getOrElse('test')).toBe('test') | |
}) | |
it('Should handle a numeric value', () => { | |
const maybe = Maybe.fromValue(42) | |
expect(maybe.getOrElse(0)).toBe(42) | |
}) | |
it('Should handle the zero value as valid', () => { | |
const maybe = Maybe.fromValue(0) | |
expect(maybe.getOrElse(1)).toBe(0) | |
}) | |
it('Should tap a value', () => { | |
const maybe = Maybe.fromValue('value') | |
let actual = false | |
maybe.tap(() => { | |
actual = true | |
}) | |
expect(actual).toBe(true) | |
}) | |
it('Should not tap a value', () => { | |
const maybe = Maybe.nothing() | |
let actual = false | |
maybe.tap(() => { | |
actual = true | |
}) | |
expect(actual).toBe(false) | |
}) | |
it('Should throw an error if receives a null value', () => { | |
expect(() => { | |
Maybe.just(null) | |
}).toThrowError() | |
}) | |
it('Should handle a callback as a default value', () => { | |
const mock = jest.fn() | |
const maybe = Maybe.fromValue(null) | |
maybe.getOrExecute(mock) | |
expect(mock).not.toHaveBeenCalled() | |
}) | |
it('Should check if it has a value', () => { | |
const maybe = Maybe.fromValue('hello') | |
expect(maybe.has()).toBe(true) | |
}) | |
it('Should check if it does not have a value', () => { | |
const maybe = Maybe.fromValue(null) | |
expect(maybe.has()).toBe(false) | |
}) | |
it('Should handle nothing values', () => { | |
const maybe = Maybe.nothing() | |
expect(maybe.getOrElse('test')).toBe('test') | |
}) | |
it('Should get or throw', () => { | |
const maybe = Maybe.nothing() | |
expect(() => { | |
maybe.getOrThrow(new Error('foo')) | |
}).toThrowError('foo') | |
}) | |
it('Should be able to map existing values', () => { | |
const maybeMap = Maybe.just({ name: 'John' }) | |
expect(maybeMap.map((v) => v.name).getOrElse('Peter')).toBe('John') | |
}) | |
it('Should be able to map non existing values', () => { | |
const maybeMap = Maybe.just({ foo: Maybe.nothing() }) | |
expect( | |
maybeMap | |
.getOrExecute(() => { | |
throw new Error() | |
}) | |
.foo.map((x) => x.bar) | |
).toEqual(Maybe.nothing()) | |
}) | |
it('Should be able to flat map existing values', () => { | |
const maybeMap = Maybe.fromValue({ foo: Maybe.just({ bar: 'qux' }) }) | |
expect(maybeMap.flatMap((x) => x.foo).map((x) => x.bar)).toEqual( | |
Maybe.just('qux') | |
) | |
}) | |
it('Should be able to flat map non existing values', () => { | |
const maybeMap = Maybe.nothing() | |
expect(maybeMap.flatMap((x) => x.foo)).toEqual(Maybe.nothing()) | |
}) | |
}) |
This file contains hidden or 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 isNumberZero = (value) => typeof value === 'number' && value === 0 | |
const isFalse = (value) => typeof value === 'boolean' && !value | |
const isEmptyString = (value) => typeof value === 'string' && value === '' | |
function isNullOrUndefined(value) { | |
return value === null || value === undefined | |
} | |
function isValid(value) { | |
return ( | |
!!value || | |
isNumberZero(value) || | |
isFalse(value) || | |
isEmptyString(value) || | |
!isNullOrUndefined(value) | |
) | |
} | |
module.exports = { isNumberZero, isFalse, isEmptyString, isNullOrUndefined, isValid } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment