Skip to content

Instantly share code, notes, and snippets.

@moatorres
Last active June 27, 2021 13:49
Show Gist options
  • Save moatorres/deb3bf797c3bc5ca63c5a7919508b295 to your computer and use it in GitHub Desktop.
Save moatorres/deb3bf797c3bc5ca63c5a7919508b295 to your computer and use it in GitHub Desktop.
Maybe monad in JS/Node (without classes)
const { Maybe } = require('./Maybe')
// let's say we have a function that could return null or undefined
function divide(a, b) {
if (b === 0) return
return (a / b)
}
let resultsSomething = divide(4, 1)
let resultsNothing = divide(4, 0)
let mayJust = Maybe.fromNullable(divide(4, 1))
console.log('type:', mayJust.type) // => Symbol(:just)
console.log('has:', mayJust.has()) // => true
console.log('isJust:', mayJust.isJust()) // => true
console.log('isNothing:', mayJust.isNothing()) // => false
console.log('unwrap:', mayJust.unwrap()) // => 4
console.log('map:', mayJust.map((v) => v * 1303).getOrExecute((v) => 'what?')) // => 5212
console.log('flatMap:', mayJust.flatMap((v) => v + 100)) // => 104
console.log('filter:', mayJust.filter((v) => typeof v !== 'number').unwrap()) // → Nothing()
console.log('toString:', mayJust.toString()) // => 'Just(1)'
console.log('toArray:', mayJust.toArray()) // => [ 4 ]
console.log('or:', mayJust.or('Else')) // => 4
console.log('getOrElse:', mayJust.getOrElse('Else')) // => 4
console.log('getOrExecute:', mayJust.getOrExecute(() => 'Nada')) // => 4
console.log('getOrThrow:', mayJust.getOrThrow(new Error('Ops!'))) // => 4
let tapper = { nothing: () => 'Nada', just: (v) => v }
console.log('tap:', mayJust.tap(tapper)) // => 4
console.log('match:', mayJust.map((v) => (v = undefined)).match(tapper)) // => Nada
// .of and .fromValue are aliases of .fromNullable
let mayNothing = Maybe.of(divide(4, 0))
console.log('type:', mayNothing.type) // => Symbol(:nothing)
console.log('has:', mayNothing.has()) // => false
console.log('isJust:', mayNothing.isJust()) // => false
console.log('isNothing:', mayNothing.isNothing()) // => true
console.log('unwrap:', mayNothing.unwrap()) // → Nothing { ... }
console.log('map:', mayNothing.map((v) => v + 100).unwrap()) // → Nothing { ... }
console.log('flatMap:', mayNothing.flatMap((v) => v + 100)) // → Nothing { ... }
console.log('filter:', mayNothing.filter((v) => typeof v !== 'number').unwrap()) // → Nothing { ... }
console.log('toString:', mayNothing.toString()) // 'Nothing()'
console.log('toArray:', mayNothing.toArray()) // => []
console.log('or:', mayNothing.or('Else indeed')) // => 'Else indeed'
console.log('getOrElse:', mayNothing.getOrElse('Else')) // => 'Else'
console.log('getOrExecute:', mayNothing.getOrExecute(() => 'Nada')) // => 'Nada'
console.log('getOrThrow:', mayNothing.getOrThrow(new Error('Ops!'))) // => Error: Ops!
type NotNullOrUndefined = string | number | boolean | symbol | object
interface IMaybe<T> {
type: symbol
has(): boolean
isJust(): boolean
isNothing(): boolean
unwrap(): T | never
map<U>(fn: (value: T) => U): IMaybe<U>
flatMap<U>(fn: (value: T) => U): T
filter<U>(fn: (value: T) => U): IMaybe<U>
andThen<U>(fn: (value: T) => IMaybe<U>): T
and<U>(option: IMaybe<U>): IMaybe<U> | T
toString(): string
toArray(): any[]
valueOrUndefined(): T | undefined
valueOrNull(): T | null
or<U>(option: IMaybe<U>): IMaybe<T | U>
getOrElse<U>(option: IMaybe<U> | NotNullOrUndefined): IMaybe<T | U>
getOrThrow<U>(option: IMaybe<U> | Error): IMaybe<T | U> | Error
getOrExecute<U>(
fn: Function | IMaybe<U>
): IMaybe<T | U> | NotNullOrUndefined | Error
tap<U>(fn: IMatch<T, U>): U
match<U>(fn: IMatch<T, U>): U
}
interface IJust<T> extends IMaybe<T> {
unwrap(): T
valueOrUndefined(): T
valueOrNull(): T
map<U>(fn: (value: T) => U): IJust<U>
or<U>(opt: IMaybe<U>): IMaybe<T>
and<U>(opt: IMaybe<U>): IMaybe<U>
}
interface INothing<T> extends IMaybe<T> {
unwrap(): never
valueOrUndefined(): undefined
valueOrNull(): null
map<U>(fn: (value?: T) => U): INothing<U>
or<U>(opt: IMaybe<U> | U): IMaybe<U> | U
and<U>(opt: IMaybe<U>): INothing<U>
}
interface IMatch<T, U> {
just: (value: T | (() => U)) => U
nothing: (() => U) | U
}
interface IMaybeRef<T> {
just<T>(value?: any): IMaybe<T | U>
nothing<U>(value?: any): INothing<T>
fromNullable<U>(value?: any): IMaybe<T | U>
fromValue<U>(value?: any): IMaybe<T | U>
of<U>(value?: any): IMaybe<T | U>
}
export const MaybeType = {
Just: symbol,
Nothing: symbol,
}
export const Maybe: IMaybeRef<T>
const { isNumberZero, isFalse, isEmptyString, notNullOrUndefined, isValid, makeReadOnly } = require('./utils')
const MaybeType = {
Just: Symbol(':just'),
Nothing: Symbol(':nothing'),
}
const makeJust = (value) => makeReadOnly({
type: MaybeType.Just,
has: () => true,
isJust: () => true,
isNothing: () => false,
unwrap: () => value,
map: (f) => Maybe.of(f(value)),
flatMap: (f) => f(value),
filter: (f) => Maybe.fromNullable(f(value) ? value : null),
andThen: (fn) => fn(value),
and: (_) => _,
toString: () => `Just(${value})`,
toArray: () => (Array.isArray(value) ? value : [value]),
valueOrUndefined: () => value,
valueOrNull: () => value,
getOrElse: (_) => value,
getOrThrow: (_) => value,
getOrExecute: (_) => value,
or(_) {
return this
},
tap: (obj) => {
const { just } = obj
if (typeof just === 'function') {
return just()
}
return just
},
match: function (obj) {
return this.tap(obj)
},
})
const makeNothing = (value) => makeReadOnly({
type: MaybeType.Nothing,
has: () => false,
isNothing: () => true,
isJust: () => false,
unwrap: () => Maybe.nothing(),
map: (_) => Maybe.nothing(),
flatMap: () => Maybe.nothing(),
filter: () => Maybe.nothing(),
andThen: (_) => Maybe.nothing(),
and: (_) => Maybe.nothing(),
toString: () => 'Nothing()',
toArray: () => [],
valueOrUndefined: () => undefined,
valueOrNull: () => null,
or: (option) => option,
getOrExecute: (fn) => {
if (!fn)
throw new Error('Cannot call getOrExecute with a missing function.')
return fn()
},
getOrElse: (def) => {
if (!notNullOrUndefined(def))
throw new Error('Cannot call getOrElse with a missing value.')
return def
},
getOrThrow(error) {
if (notNullOrUndefined(error)) throw error
else throw new Error()
},
tap: (obj) => {
const { nothing } = obj
if (typeof nothing === 'function') {
return nothing()
}
return nothing
},
match: function (obj) {
return this.tap(obj)
},
})
const Maybe = makeReadOnly({
just: (value) => {
if (isValid(value)) return makeJust(value)
throw new Error('Just cannot have an null or undefined value')
},
nothing: () => makeNothing(),
fromNullable: function (value) {
return isValid(value) ? this.just(value) : this.nothing()
},
fromValue: function (value) {
return this.fromNullable(value)
},
of: function (value) {
return this.fromNullable(value)
},
})
module.exports = { Maybe, MaybeType }
const { MaybeType, Maybe } = require('./Maybe')
let nops, yep, tapMatch
beforeEach(() => {
nops = Maybe.fromNullable(null)
yep = Maybe.fromNullable(1)
tapMatch = {
just: () => 'Hey!',
nothing: () => 'Putz!',
}
})
describe('Maybe', () => {
it('Should be defined', () => {
expect(Maybe).toBeDefined()
})
describe('.just()', () => {
it('Should be defined', () => {
expect(Maybe.just).toBeDefined()
})
it('Should return a Just type object if the value is not null or undefined', () => {
let justa = Maybe.just(1)
expect(justa.type).toBe(MaybeType.Just)
})
it('Should throw if the value is null or undefined', () => {
expect(() => Maybe.just()).toThrowError()
})
})
describe('.nothing()', () => {
it('Should be defined', () => {
expect(Maybe.nothing).toBeDefined()
})
it('Should return a Nothing type object regardless of the value', () => {
let not = Maybe.nothing(1)
let notReally = Maybe.nothing(0)
expect(not.type).toBe(MaybeType.Nothing)
expect(notReally.type).toBe(MaybeType.Nothing)
})
})
describe('.fromNullable()', () => {
it('Should be defined', () => {
expect(Maybe.fromNullable).toBeDefined()
})
it('Should return a Just type object if the value is not null or undefined', () => {
let justa = Maybe.fromNullable(1)
expect(justa.type).toBe(MaybeType.Just)
})
it('Should return a Nothung type object the value is null or undefined', () => {
let not = Maybe.fromNullable()
expect(not.type).toBe(MaybeType.Nothing)
})
})
describe('.fromValue() → alias of .fromNullable()', () => {
it('Should be defined', () => {
expect(Maybe.fromValue).toBeDefined()
})
it('Should return a Just type object if the value is not null or undefined', () => {
let justa = Maybe.fromValue(1)
expect(justa.type).toBe(MaybeType.Just)
})
it('Should return a Nothung type object the value is null or undefined', () => {
let not = Maybe.fromValue()
expect(not.type).toBe(MaybeType.Nothing)
})
})
describe('.of() → alias of .fromNullable()', () => {
it('Should be defined', () => {
expect(Maybe.of).toBeDefined()
})
it('Should return a Just type object if the value is not null or undefined', () => {
let justa = Maybe.of(1)
expect(justa.type).toBe(MaybeType.Just)
})
it('Should return a Nothung type object the value is null or undefined', () => {
let not = Maybe.of()
expect(not.type).toBe(MaybeType.Nothing)
})
})
})
describe('Just', () => {
describe('.type', () => {
it('Should be defined', () => {
expect(yep.type).toBeDefined()
})
it('Should be of Just type', () => {
expect(yep.type).toBe(MaybeType.Just)
})
})
describe('.has()', () => {
it('Should be defined', () => {
expect(yep.has).toBeDefined()
})
it('Should return "true"', () => {
expect(yep.has()).toBeTrue()
})
})
describe('.isJust()', () => {
it('Should be defined', () => {
expect(yep.isJust).toBeDefined()
})
it('Should return "true"', () => {
expect(yep.isJust()).toBeTrue()
})
})
describe('.isNothing()', () => {
it('Should be defined', () => {
expect(yep.isNothing).toBeDefined()
})
it('Should return "false"', () => {
expect(yep.isNothing()).toBeFalse()
})
})
describe('.unwrap()', () => {
it('Should be defined', () => {
expect(yep.unwrap).toBeDefined()
})
it('Should return an unwrapped value', () => {
expect(yep.unwrap()).toEqual(1)
})
})
describe('.map()', () => {
it('Should be defined', () => {
expect(yep.map).toBeDefined()
})
it('Should map over the value and return a Just or a Nothing type object', () => {
let res1 = yep.map((v) => v + 1)
expect(res1.type).toBe(MaybeType.Just)
expect(res1.unwrap()).toBe(2)
let res2 = yep.map((v) => (v = undefined))
expect(res2.type).toBe(MaybeType.Nothing)
})
})
describe('.flatMap()', () => {
it('Should be defined', () => {
expect(yep.flatMap).toBeDefined()
})
it('Should map over the value and return the mapped function result', () => {
let res = yep.flatMap((v) => v + 1)
expect(res).toEqual(2)
})
})
describe('.filter()', () => {
it('Should be defined', () => {
expect(yep.filter).toBeDefined()
})
it('Should return "this" if the result of the predicate function is truthy', () => {
let res = yep.filter((v) => typeof v === 'number')
expect(res.type).toBe(MaybeType.Just)
expect(res.unwrap()).toBe(1)
})
it('Should return a Nothing type object if the predicate function result is falsy', () => {
let res = yep.filter((v) => typeof v !== 'number')
expect(res.type).toBe(MaybeType.Nothing)
})
})
describe('.andThen()', () => {
it('Should be defined', () => {
expect(yep.andThen).toBeDefined()
})
it('Should execute the provided function and return its result', () => {
let res = yep.andThen(() => 'A-ha!')
expect(res).toEqual('A-ha!')
})
})
describe('.and()', () => {
it('Should be defined', () => {
expect(yep.and).toBeDefined()
})
it('Should return the provided and argument', () => {
let res = yep.and(10)
expect(res).toEqual(10)
})
})
describe('.toString()', () => {
it('Should be defined', () => {
expect(yep.toString).toBeDefined()
})
it('Should return a string representation of "this"', () => {
let res = yep.toString()
expect(typeof res).toBe('string')
expect(res).toEqual('Just(1)')
})
})
describe('.toArray()', () => {
it('Should be defined', () => {
expect(yep.toArray).toBeDefined()
})
it('Should return an array with the enclosed value', () => {
expect(yep.toArray()).toEqual([1])
})
})
describe('.valueOrUndefined()', () => {
it('Should be defined', () => {
expect(yep.valueOrUndefined).toBeDefined()
})
it('Should return an unwrapped value', () => {
expect(yep.valueOrUndefined()).toEqual(1)
})
})
describe('.valueOrNull()', () => {
it('Should be defined', () => {
expect(yep.valueOrNull).toBeDefined()
})
it('Should return an unwrapped value', () => {
expect(yep.valueOrNull()).toEqual(1)
})
})
describe('.or()', () => {
it('Should be defined', () => {
expect(yep.or).toBeDefined()
})
it('Should return "this"', () => {
let res = yep.or()
expect(res.type).toBe(MaybeType.Just)
expect(res.unwrap()).toEqual(1)
})
})
describe('.getOrElse()', () => {
it('Should be defined', () => {
expect(yep.getOrElse).toBeDefined()
})
it('Should return the unwrapped value', () => {
expect(yep.getOrElse()).toEqual(1)
})
})
describe('.getOrThrow()', () => {
it('Should be defined', () => {
expect(yep.getOrThrow).toBeDefined()
})
it('Should return the unwrapped value', () => {
expect(yep.getOrThrow()).toEqual(1)
})
})
describe('.getOrExecute()', () => {
it('Should be defined', () => {
expect(yep.getOrExecute).toBeDefined()
})
it('Should return the unwrapped value', () => {
expect(yep.getOrExecute()).toEqual(1)
})
})
describe('.tap()', () => {
it('Should be defined', () => {
expect(yep.tap).toBeDefined()
})
it('Should allow us to execute a .just matching function', () => {
expect(yep.tap(tapMatch)).toEqual('Hey!')
})
it('Should execute a .nothing matching function if the value becomes null or undefined', () => {
expect(yep.map((v) => (v = null)).tap(tapMatch)).toEqual('Putz!')
})
})
describe('.match() → alias of .tap()', () => {
it('Should be defined', () => {
expect(yep.match).toBeDefined()
})
it('Should allow us to execute a .just matching function', () => {
expect(yep.match(tapMatch)).toEqual('Hey!')
})
it('Should execute a .nothing matching function if the value becomes null or undefined', () => {
expect(yep.map((v) => (v = null)).match(tapMatch)).toEqual('Putz!')
})
})
})
describe('Nothing', () => {
describe('.type', () => {
it('Should be defined', () => {
expect(nops.type).toBeDefined()
})
it('Should be of Nothing type', () => {
expect(nops.type).toBe(MaybeType.Nothing)
})
})
describe('.has()', () => {
it('Should be defined', () => {
expect(nops.has).toBeDefined()
})
it('Should return "false"', () => {
expect(nops.has()).toBeFalse()
})
})
describe('.isJust()', () => {
it('Should be defined', () => {
expect(nops.isJust).toBeDefined()
})
it('Should return "true"', () => {
expect(nops.isJust()).toBeFalse()
})
})
describe('.isNothing()', () => {
it('Should be defined', () => {
expect(nops.isNothing).toBeDefined()
})
it('Should return "false"', () => {
expect(nops.isNothing()).toBeTrue()
})
})
describe('.unwrap()', () => {
it('Should be defined', () => {
expect(nops.unwrap).toBeDefined()
})
it('Should return "this"', () => {
let res = nops.unwrap()
expect(res.type).toBe(MaybeType.Nothing)
})
})
describe('.map()', () => {
it('Should be defined', () => {
expect(nops.map).toBeDefined()
})
it('Should always return a Nothing type object', () => {
let res = nops.map((v) => (v = 1))
expect(res.type).toBe(MaybeType.Nothing)
})
})
describe('.flatMap()', () => {
it('Should be defined', () => {
expect(nops.flatMap).toBeDefined()
})
it('Should always return a Nothing type object', () => {
let res = nops.flatMap((v) => (v = 1))
expect(res.type).toBe(MaybeType.Nothing)
})
})
describe('.filter()', () => {
it('Should be defined', () => {
expect(nops.filter).toBeDefined()
})
it('Should always return a Nothing type object', () => {
let res = nops.filter((v) => typeof v === 'number')
expect(res.type).toBe(MaybeType.Nothing)
})
})
describe('.andThen()', () => {
it('Should be defined', () => {
expect(nops.andThen).toBeDefined()
})
it('Should always return a Nothing type object', () => {
let res = nops.andThen(1000)
expect(res.type).toBe(MaybeType.Nothing)
})
})
describe('.and()', () => {
it('Should be defined', () => {
expect(nops.and).toBeDefined()
})
it('Should always return a Nothing type object', () => {
let res = nops.and(1000)
expect(res.type).toBe(MaybeType.Nothing)
})
})
describe('.toString()', () => {
it('Should be defined', () => {
expect(nops.toString).toBeDefined()
})
it('Should return a string representation of "this"', () => {
let res = nops.toString()
expect(typeof res).toBe('string')
expect(res).toEqual('Nothing()')
})
})
describe('.toArray()', () => {
it('Should be defined', () => {
expect(nops.toArray).toBeDefined()
})
it('Should return an empty array', () => {
expect(nops.toArray()).toEqual([])
})
})
describe('.valueOrUndefined()', () => {
it('Should be defined', () => {
expect(nops.valueOrUndefined).toBeDefined()
})
it('Should return undefined', () => {
expect(nops.valueOrUndefined()).toEqual(undefined)
})
})
describe('.valueOrNull()', () => {
it('Should be defined', () => {
expect(nops.valueOrNull).toBeDefined()
})
it('Should return null', () => {
expect(nops.valueOrNull()).toEqual(null)
})
})
describe('.or()', () => {
it('Should be defined', () => {
expect(nops.or).toBeDefined()
})
it('Should return the provided option value', () => {
let res = nops.or('Bummer')
expect(typeof res).toBe('string')
expect(res).toEqual('Bummer')
})
})
describe('.getOrElse()', () => {
it('Should be defined', () => {
expect(nops.getOrElse).toBeDefined()
})
it('Should return the provided getOrElse value', () => {
let res = nops.getOrElse('Else')
expect(typeof res).toBe('string')
expect(res).toEqual('Else')
})
it('Should throw if called without a value', () => {
expect(() => nops.getOrElse()).toThrowError()
expect(() => nops.getOrElse(null)).toThrowError()
expect(() => nops.getOrElse(undefined)).toThrowError()
expect(() => nops.getOrElse('')).not.toThrowError()
expect(() => nops.getOrElse(0)).not.toThrowError()
expect(() => nops.getOrElse(false)).not.toThrowError()
})
})
describe('.getOrThrow()', () => {
it('Should be defined', () => {
expect(nops.getOrThrow).toBeDefined()
})
it('Should throw the provided value or an error', () => {
expect(() => nops.getOrThrow(1)).toThrow()
expect(() => nops.getOrThrow(new Error('Ops?'))).toThrowError()
expect(() => nops.getOrThrow()).toThrowError()
})
})
describe('.getOrExecute()', () => {
it('Should be defined', () => {
expect(nops.getOrExecute).toBeDefined()
})
it('Should throw if the provided argument is not a function', () => {
expect(() => nops.getOrExecute('1')).toThrowError()
})
it('Should return the result of the provided function', () => {
expect(nops.getOrExecute(() => 1 + 1)).toEqual(2)
expect(nops.getOrExecute(() => 'Ops')).toEqual('Ops')
})
})
describe('.tap()', () => {
it('Should be defined', () => {
expect(nops.tap).toBeDefined()
})
it('Should allow us to execute a .nothing matching function', () => {
expect(nops.tap(tapMatch)).toEqual('Putz!')
})
it('Should always execute a .nothing matching function', () => {
expect(nops.tap(tapMatch)).toEqual('Putz!')
})
})
describe('.match() → alias of .tap()', () => {
it('Should be defined', () => {
expect(nops.match).toBeDefined()
})
it('Should allow us to execute a .nothing matching function', () => {
expect(nops.match(tapMatch)).toEqual('Putz!')
})
it('Should always execute a .nothing matching function', () => {
expect(nops.map((v) => (v = 1)).match(tapMatch)).toEqual('Putz!')
})
})
})
const isNumberZero = (value) => typeof value === 'number' && value === 0
const isFalse = (value) => typeof value === 'boolean' && !value
const isEmptyString = (value) => typeof value === 'string' && value === ''
const makeReadOnly = (object) => Object.freeze(object)
function notNullOrUndefined(value) {
if (isEmptyString(value)) return true
if (isNumberZero(value)) return true
if (isFalse(value)) return true
if (!value) return false
return typeof value !== null && typeof value !== undefined
}
function isValid(value) {
return (
!!value ||
isNumberZero(value) ||
isFalse(value) ||
isEmptyString(value) ||
notNullOrUndefined(value)
)
}
module.exports = { isNumberZero, isFalse, isEmptyString, notNullOrUndefined, isValid, makeReadOnly }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment