Last active
September 19, 2019 18:03
-
-
Save chriseppstein/a5e6ae58adcb8736d808e68ccab6c3c4 to your computer and use it in GitHub Desktop.
This file contains 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
import { something, defined, TypeGuard, FunctionCall0, FunctionCall1, FunctionCall2, FunctionCall3, FunctionCall4 } from "./UtilityTypes"; | |
import { isObject, whatever } from "./index"; | |
/** | |
* Maybe.ts - A TypeScript implementation of the Maybe Monad. | |
* ========================================================== | |
* | |
* Usually a Maybe is strong type with methods, but that approach, I felt, | |
* would lead to poor performance characteristics and also not take full | |
* advantage of TypeScript's specific approach to types using type guards. | |
* | |
* Other Maybe libraries found in my research, attempt to emulate pattern | |
* matching. This approach was explicitly rejected because this would incur the | |
* cost of creating new objects and functional closures unnecessarily when | |
* simple branching via type guards will suffice with only minimal impact to | |
* code legibility. | |
* | |
* In this Maybe implementation, the None has an associated error object or | |
* error message that can be provided at the point where the None was first | |
* created. If provided, that is the error which is raised when a None is | |
* unwrapped. | |
* | |
* The error message is particularly useful in combination with `attempt`, | |
* which executes a callback and if it raises an error, the execution returns a | |
* None, with the error value set accordingly. | |
* | |
* The callMaybe function can be used to conditionally skip execution of a | |
* function if any of the arguments are a None. Normal values and Maybe values | |
* can be intermixed -- any Some values are unwrapped before being passed. | |
* | |
* ## Basic Usage: | |
* | |
* ```ts | |
* import { | |
* some, none, callMaybe, isMaybe, isSome, OptionalMaybe, Maybe, | |
* } from '@opticss/util'; | |
* const LARGENESS_THRESHOLD = 100; | |
* function getLargeNumber(n: number): Maybe<number> { | |
* if (number > LARGENESS_THRESHOLD) { | |
* return some(number); | |
* } else { | |
* return none(`number must be greater than ${LARGENESS_THRESHOLD}`); | |
* } | |
* } | |
* | |
* function formatIfLargeNumber(n: number): Maybe<string> { | |
* let largeN = getLargeNumber(n); | |
* return callMaybe(formatNumber, largeN); | |
* } | |
* | |
* let counter: number = 0; | |
* function updateCounter(n: OptionalMaybe<number>) { | |
* if (isMaybe(n)) { | |
* if (isSome(n)) { | |
* counter += unwrap(n); | |
* } | |
* } else { | |
* number += n; | |
* } | |
* } | |
* | |
* function formatNumber(n: number): string { | |
* return n.toString(16); | |
* } | |
* ``` | |
*/ | |
export type Maybe<T> = Some<T> | None; | |
export const MAYBE = Symbol("Maybe"); | |
export const NO_VALUE = Symbol("None"); | |
export type None = { [MAYBE]: symbol, error?: string | Error }; | |
export type Some<T> = { [MAYBE]: T }; | |
export type MaybeUndefined<T extends defined> = T | Maybe<T> | undefined; | |
export type OptionalMaybe<T> = T | Maybe<T>; | |
/** | |
* Passes a Maybe through. If the value is not a Maybe, undefined is converted | |
* to None, all other values are treated as Some. An error message can be | |
* provided for the eventual call to none() if the value is undefined. | |
*/ | |
export function maybe<T extends something>(v: MaybeUndefined<T>, error?: string): Maybe<T> { | |
if (v === undefined || v === null) { | |
return none(error); | |
} else if (isMaybe(v)) { | |
return v; | |
} else { | |
return some(v); | |
} | |
} | |
/** | |
* Creates a Some() value. | |
*/ | |
export function some<T>(v: T): Some<T> { | |
return {[MAYBE]: v}; | |
} | |
/** | |
* Creates a None() value. | |
*/ | |
export function none(error?: string | Error): None { | |
return {[MAYBE]: NO_VALUE, error}; | |
} | |
export type CallMeMaybe0<R> = FunctionCall0<R> | |
| FunctionCall0<Maybe<R>>; | |
export type CallMeMaybe1<A1, R> = FunctionCall1<A1, R> | |
| FunctionCall1<A1, Maybe<R>>; | |
export type CallMeMaybe2<A1, A2, R> = FunctionCall2<A1, A2, R> | |
| FunctionCall2<A1, A2, Maybe<R>>; | |
export type CallMeMaybe3<A1, A2, A3, R> = FunctionCall3<A1, A2, A3, R> | |
| FunctionCall3<A1, A2, A3, Maybe<R>>; | |
export type CallMeMaybe4<A1, A2, A3, A4, R> = FunctionCall4<A1, A2, A3, A4, R> | |
| FunctionCall4<A1, A2, A3, A4, Maybe<R>>; | |
export type CallMeMaybe<A1, A2, A3, A4, R> = CallMeMaybe0<R> | |
| CallMeMaybe1<A1, R> | |
| CallMeMaybe2<A1, A2, R> | |
| CallMeMaybe3<A1, A2, A3, R> | |
| CallMeMaybe4<A1, A2, A3, A4, R>; | |
export function callMaybe<A1, R>(fn: CallMeMaybe1<A1, R>, arg1: OptionalMaybe<A1>): Maybe<R>; | |
export function callMaybe<A1, A2, R>(fn: CallMeMaybe2<A1, A2, R>, arg1: OptionalMaybe<A1>, arg2: OptionalMaybe<A2>): Maybe<R>; | |
export function callMaybe<A1, A2, A3, R>(fn: CallMeMaybe3<A1, A2, A3, R>, arg1: OptionalMaybe<A1>, arg2: OptionalMaybe<A2>, arg3: OptionalMaybe<A3>): Maybe<R>; | |
export function callMaybe<A1, A2, A3, A4, R>(fn: CallMeMaybe4<A1, A2, A3, A4, R>, arg1: OptionalMaybe<A1>, arg2: OptionalMaybe<A2>, arg3: OptionalMaybe<A3>, arg4: OptionalMaybe<A4>): Maybe<R>; | |
/** | |
* If any argument is a None, do not invoke the function and return the first argument that is a none instead. | |
* If the function returns a maybe, pass it through. All other return values are returned as a Some. | |
*/ | |
export function callMaybe<A1, A2, A3, A4, R>(fn: CallMeMaybe<A1, A2, A3, A4, R>, arg1: OptionalMaybe<A1>, arg2?: OptionalMaybe<A2>, arg3?: OptionalMaybe<A3>, arg4?: OptionalMaybe<A4>): Maybe<R> { | |
if (isMaybe(arg1) && isNone(arg1)) { return arg1; } | |
else if (isMaybe(arg2) && isNone(arg2)) { return arg2; } | |
else if (isMaybe(arg3) && isNone(arg3)) { return arg3; } | |
else if (isMaybe(arg4) && isNone(arg4)) { return arg4; } | |
let rv: OptionalMaybe<R> = fn.call(null, | |
arg1 && unwrapIfMaybe(arg1), | |
arg2 && unwrapIfMaybe(arg2), | |
arg3 && unwrapIfMaybe(arg3), | |
arg4 && unwrapIfMaybe(arg4) | |
); | |
if (isMaybe(rv)) return rv; | |
return maybe(rv); | |
} | |
export type HasMethod<Type extends object, N extends keyof Type, PropertyType> = Pick<{ | |
[P in keyof Type]: PropertyType; | |
}, N>; | |
export function methodMaybe<N extends keyof T, T extends HasMethod<T, N, CallMeMaybe0<R>>, R>(thisObj: Maybe<T>, fnName: N): Maybe<R>; | |
export function methodMaybe<N extends keyof T, T extends HasMethod<T, N, CallMeMaybe1<A1, R>>, A1, R>(thisObj: Maybe<T>, fnName: N, arg1: OptionalMaybe<A1>): Maybe<R>; | |
export function methodMaybe<N extends keyof T, T extends HasMethod<T, N, CallMeMaybe2<A1, A2, R>>, A1, A2, R>(thisObj: Maybe<T>, fnName: N, arg1: OptionalMaybe<A1>, arg2: OptionalMaybe<A2>): Maybe<R>; | |
export function methodMaybe<N extends keyof T, T extends HasMethod<T, N, CallMeMaybe3<A1, A2, A3, R>>, A1, A2, A3, R>(thisObj: Maybe<T>, fnName: N, arg1: OptionalMaybe<A1>, arg2: OptionalMaybe<A2>, arg3: OptionalMaybe<A3>): Maybe<R>; | |
export function methodMaybe<N extends keyof T, T extends HasMethod<T, N, CallMeMaybe4<A1, A2, A3, A4, R>>, A1, A2, A3, A4, R>(thisObj: Maybe<T>, fnName: N, arg1: OptionalMaybe<A1>, arg2: OptionalMaybe<A2>, arg3: OptionalMaybe<A3>, arg4: OptionalMaybe<A4>): Maybe<R>; | |
export function methodMaybe< | |
N extends keyof T, | |
T extends HasMethod<T, N, CallMeMaybe<A1, A2, A3, A4, R>>, | |
A1, A2, A3, A4, R, | |
>( | |
thisObj: Maybe<T>, | |
fnName: N, | |
arg1?: OptionalMaybe<A1>, | |
arg2?: OptionalMaybe<A2>, | |
arg3?: OptionalMaybe<A3>, | |
arg4?: OptionalMaybe<A4> | |
): Maybe<R> { | |
if (isNone(thisObj)) return thisObj; | |
if (isMaybe(arg1) && isNone(arg1)) { return arg1; } | |
else if (isMaybe(arg2) && isNone(arg2)) { return arg2; } | |
else if (isMaybe(arg3) && isNone(arg3)) { return arg3; } | |
else if (isMaybe(arg4) && isNone(arg4)) { return arg4; } | |
let self: T = unwrap(thisObj); | |
let method: T[N] = self[fnName]; | |
let rv: OptionalMaybe<R> = method.call(self, | |
arg1 && unwrapIfMaybe(arg1), | |
arg2 && unwrapIfMaybe(arg2), | |
arg3 && unwrapIfMaybe(arg3), | |
arg4 && unwrapIfMaybe(arg4) | |
); | |
if (isMaybe(rv)) return rv; | |
return some(rv); | |
} | |
/** | |
* Runs the callback. | |
* | |
* If it returns a maybe, it is returned. | |
* If it raises an error, a None is returned and the caught error is thrown | |
* when the value is unwrapped. | |
* Otherwise the return value, is passed through `maybe()` | |
* returning a `Some` or `None` depending on the value. | |
*/ | |
export function attempt<R>(fn: () => OptionalMaybe<R>): Maybe<R> { | |
try { | |
let rv = fn(); | |
if (isMaybe(rv)) return rv; | |
return some(rv); | |
} catch (e) { | |
return none(e); | |
} | |
} | |
/** | |
* Type Guard. Test if the value is a Maybe (a Some or a None). | |
*/ | |
export function isMaybe(value: whatever): value is Maybe<something> { | |
if (isObject(value)) { | |
return value.hasOwnProperty(MAYBE); | |
} else { | |
return false; | |
} | |
} | |
/** | |
* Check if an arbitrary value is a Some of a particular type as determined by | |
* the provided type guard. Usually, you'll want to use `isSome` on a `Maybe` | |
* of a statically determined type. But this is useful if you need to accept | |
* a single value that is a `Maybe` of several types and you need to do some | |
* control flow before unwrapping. | |
*/ | |
export function isSomeOfType<T>(value: whatever, guard: TypeGuard<T>): value is Some<T> { | |
if (isMaybe(value)) { | |
if (isNone(value)) return false; | |
return guard(unwrap(value)); | |
} else { | |
return false; | |
} | |
} | |
/** | |
* Type Guard. Test if the value is a Some. | |
*/ | |
export function isSome<T extends something>(value: Maybe<T>): value is Some<T> { | |
return value[MAYBE] !== NO_VALUE; | |
} | |
/** | |
* Type Guard. Test if the value is a None. | |
*/ | |
export function isNone(value: Maybe<something>): value is None { | |
return value[MAYBE] === NO_VALUE; | |
} | |
/** | |
* An error class that is raised when the error provided is just a string | |
* message. | |
*/ | |
export class UndefinedValue extends Error { | |
constructor(message?: string) { | |
super(message || "A value was expected."); | |
} | |
} | |
/** | |
* If the Maybe is a None, raise an error. | |
* otherwise, return the value of the Maybe. | |
*/ | |
export function unwrap<T>(value: Maybe<T>): T { | |
if (isNone(value)) { | |
if (value.error) { | |
if (value.error instanceof Error) { | |
throw value.error; | |
} else { | |
throw new UndefinedValue(value.error); | |
} | |
} else { | |
throw new UndefinedValue(); | |
} | |
} else { | |
return value[MAYBE]; | |
} | |
} | |
/** | |
* If the value passed is a maybe, unwrap it. otherwise pass the value through. | |
*/ | |
export function unwrapIfMaybe<T>(value: OptionalMaybe<T>): T { | |
if (isMaybe(value)) { | |
return unwrap(value); | |
} else { | |
return value; | |
} | |
} |
This file contains 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
/** | |
* Types representing having no value for various reasons. | |
*/ | |
export type nothing = null | undefined | void; | |
export function isNothing(v: whatever): v is nothing { | |
return v === null || v === undefined; | |
} | |
/** | |
* Falsy in JS isn't always what you want. Somethings aren't nothings. | |
*/ | |
export type something = string | number | boolean | symbol | object; | |
export function isSomething(v: whatever): v is something { | |
return !isNothing(v); | |
} | |
/** | |
* Any value that isn't undefined or void. null is considered defined because | |
* something that is null represents the state of knowingly having no value. | |
*/ | |
export type defined = something | null; | |
export function isDefined(v: whatever): v is defined { | |
return v !== undefined; | |
} | |
/** | |
* TypeScript imbues `any` with dangerous special powers to access unknown | |
* properties and assume that values are defined by the type checker. | |
* Code that uses `any` removes type checking and makes our code less safe -- | |
* so we avoid `any` except in rare cases. | |
* | |
* If you need to represent a value that can be anything and might not even | |
* have a value, without making dangerous assumptions that come along with | |
* `any`, use whatever instead. | |
*/ | |
export type whatever = something | nothing; | |
/** | |
* This type guard is only useful for down casting an any to whatever. | |
* Note: You can just *cast* to `whatever` from `any` with zero runtime | |
* overhead, but this type guard is provided for completeness. | |
*/ | |
export function isWhatever(_v: any): _v is whatever { | |
return true; | |
} | |
/** | |
* undefined is not an object... but null is... but not in typescript. | |
*/ | |
export function isObject(v: whatever): v is object { | |
return (typeof v === "object" && v !== null); | |
} | |
export function isString(v: whatever): v is string { | |
return (typeof v === "string"); | |
} | |
export interface ObjectDictionary<T> { | |
[prop: string]: T; | |
} | |
export function isObjectDictionary<T>( | |
dict: whatever, | |
typeGuard: (v: whatever) => v is T, | |
) { | |
if (!isObject(dict)) return false; | |
for (let k of Object.keys(dict)) { | |
if (!typeGuard(dict[k])) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* This is like Object.values() but for an object where the values | |
* all have the same type so the value type can be inferred. | |
*/ | |
export function objectValues<T>(dict: ObjectDictionary<T>): Array<T> { | |
let keys = Object.keys(dict); | |
return keys.map(k => dict[k]); | |
} | |
export type StringDict = ObjectDictionary<string>; | |
export function isStringDict(dict: whatever): dict is StringDict { | |
return isObjectDictionary(dict, isString); | |
} | |
/** | |
* Set a value to the type of values in an array. | |
*/ | |
export type ItemType<T extends Array<any>> = T[0]; | |
/** | |
* represents a TypeScript type guard function. | |
*/ | |
export type TypeGuard<T extends whatever> = (v: whatever) => v is T; | |
/** A function that takes no arguments. */ | |
export type FunctionCall0<R> = () => R; | |
/** A function that takes a single argument. */ | |
export type FunctionCall1<A1, R> = (arg1: A1) => R; | |
/** A function that takes a two arguments. */ | |
export type FunctionCall2<A1, A2, R> = (arg1: A1, arg2: A2) => R; | |
/** A function that takes a three arguments. */ | |
export type FunctionCall3<A1, A2, A3, R> = (arg1: A1, arg2: A2, arg3: A3) => R; | |
/** A function that takes a four arguments. */ | |
export type FunctionCall4<A1, A2, A3, A4, R> = (arg1: A1, arg2: A2, arg3: A3, arg4: A4) => R; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment