Skip to content

Instantly share code, notes, and snippets.

@chriseppstein chriseppstein/Maybe.ts
Last active Feb 14, 2018

Embed
What would you like to do?
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;
}
}
/**
* 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
You can’t perform that action at this time.