Skip to content

Instantly share code, notes, and snippets.

@moatorres
Last active June 27, 2021 13:51
Show Gist options
  • Save moatorres/139e8c2abd1c55d6d9dce6d3a5ef21b1 to your computer and use it in GitHub Desktop.
Save moatorres/139e8c2abd1c55d6d9dce6d3a5ef21b1 to your computer and use it in GitHub Desktop.
Maybe monad in JS/Node (with ES6 classes)
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
}
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 }
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())
})
})
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