UNPKG

true-myth

Version:

A library for safe functional programming in JavaScript, with first-class support for TypeScript

572 lines (511 loc) 18.9 kB
/** A value of type `T` which may, or may not, be present. If the value is present, it is {@linkcode Just Just(value)}. If it's absent, it is {@linkcode Nothing} instead. For a deep dive on the type, see [the guide](/guide/understanding/maybe.md). @module */ import { curry1, isVoid, safeToString } from './-private/utils.js'; /** Discriminant for the {@linkcode Just} and {@linkcode Nothing} type instances. You can use the discriminant via the `variant` property of {@linkcode Maybe} instances if you need to match explicitly on it. */ export const Variant = { Just: 'Just', Nothing: 'Nothing', }; /** A single instance of the `Nothing` object, to minimize memory usage. No matter how many `Maybe`s are floating around, there will always be exactly and only one `Nothing`. @private */ let NOTHING; // Defines the *implementation*, but not the *types*. See the exports below. class MaybeImpl { // SAFETY: this is definitely assigned in the constructor for every *actual* // instance, but TS cannot see that: it is only set for `Nothing` instances // when `NOTHING` does not already exist. repr; constructor(value) { if (isVoid(value)) { // SAFETY: there is only a single `Nothing` in the system, because the // only difference between `Nothing<string>` and `Nothing<number>` is at // the type-checking level. if (!NOTHING) { this.repr = [Variant.Nothing]; NOTHING = this; } return NOTHING; } else { this.repr = [Variant.Just, value]; } } // Then the implementation signature is simply the same as the last two, // overloads because we do not *and cannot* prevent the undesired function // types from appearing here at runtime: doing so would require having a value // on which to (maybe) apply the function! static of(value) { return new Maybe(value); } // The runtime signature *does* allow null and undefined values so that the // body can correctly throw at runtime in the case where a caller passes data // whose type lies about the contained value. static just(value) { if (isVoid(value)) { throw new Error(`attempted to call "just" with ${value}`); } return new Maybe(value); } /** Create an instance of `Maybe.Nothing`. If you want to create an instance with a specific type, e.g. for use in a function which expects a `Maybe<T>` where the `<T>` is known but you have no value to give it, you can use a type parameter: ```ts const notString = Maybe.nothing<string>(); ``` @template T The type of the item contained in the `Maybe`. @returns An instance of `Maybe.Nothing<T>`. */ static nothing(_) { return new MaybeImpl(); } /** Distinguish between the `Just` and `Nothing` {@link Variant variants}. */ get variant() { return this.repr[0]; } /** The wrapped value. @warning throws if you access this from a {@linkcode Just} */ get value() { if (this.repr[0] === Variant.Nothing) { throw new Error('Cannot get the value of `Nothing`'); } return this.repr[1]; } /** Is the {@linkcode Maybe} a {@linkcode Just}? */ get isJust() { return this.repr[0] === Variant.Just; } /** Is the {@linkcode Maybe} a {@linkcode Nothing}? */ get isNothing() { return this.repr[0] === Variant.Nothing; } /** Method variant for {@linkcode map} */ map(mapFn) { return this.repr[0] === 'Just' ? Maybe.just(mapFn(this.repr[1])) : nothing(); } /** Method variant for {@link mapOr|`mapOr`} */ mapOr(orU, mapFn) { return this.repr[0] === 'Just' ? mapFn(this.repr[1]) : orU; } /** Method variant for {@linkcode mapOrElse} */ mapOrElse(orElseFn, mapFn) { return this.repr[0] === 'Just' ? mapFn(this.repr[1]) : orElseFn(); } /** Method variant for {@linkcode match} */ match(matcher) { return this.repr[0] === 'Just' ? matcher.Just(this.repr[1]) : matcher.Nothing(); } /** Method variant for {@linkcode or} */ or(mOr) { return this.repr[0] === 'Just' ? this : mOr; } orElse(orElseFn) { return (this.repr[0] === 'Just' ? this : orElseFn()); } /** Method variant for {@linkcode and} */ and(mAnd) { return (this.repr[0] === 'Just' ? mAnd : this); } andThen(andThenFn) { return (this.repr[0] === 'Just' ? andThenFn(this.repr[1]) : this); } /** Method variant for {@linkcode unwrapOr} */ unwrapOr(defaultValue) { return this.repr[0] === 'Just' ? this.repr[1] : defaultValue; } /** Method variant for {@linkcode unwrapOrElse} */ unwrapOrElse(elseFn) { return this.repr[0] === 'Just' ? this.repr[1] : elseFn(); } /** Method variant for {@linkcode toString} */ toString() { return this.repr[0] === 'Just' ? `Just(${safeToString(this.repr[1])})` : 'Nothing'; } /** Method variant for {@linkcode toJSON} */ toJSON() { const variant = this.repr[0]; // Handle nested Maybes if (variant === 'Just') { const value = isInstance(this.repr[1]) ? this.repr[1].toJSON() : this.repr[1]; return { variant, value }; } else { return { variant }; } } /** Method variant for {@linkcode equals} */ equals(comparison) { return (this.repr[0] === comparison.repr[0] && this.repr[1] === comparison.repr[1]); } /** Method variant for {@linkcode ap} */ ap(val) { return val.andThen((val) => this.map((fn) => fn(val))); } /** Method variant for {@linkcode get} If you have a `Maybe` of an object type, you can do `thatMaybe.get('a key')` to look up the next layer down in the object. ```ts type DeepOptionalType = { something?: { with?: { deeperKeys?: string; } } }; const fullySet: DeepType = { something: { with: { deeperKeys: 'like this' } } }; const deepJust = Maybe.of(fullySet) .get('something') .get('with') .get('deeperKeys'); console.log(deepJust); // Just('like this'); const partiallyUnset: DeepType = { something: { } }; const deepEmpty = Maybe.of(partiallyUnset) .get('something') .get('with') .get('deeperKeys'); console.log(deepEmpty); // Nothing ``` */ get(key) { return this.andThen(property(key)); } } /** Create a {@linkcode Maybe} instance which is a {@linkcode Just}. `null` and `undefined` are allowed by the type signature so that the function may `throw` on those rather than constructing a type like `Maybe<undefined>`. @template T The type of the item contained in the `Maybe`. @param value The value to wrap in a `Maybe.Just`. @returns An instance of `Maybe.Just<T>`. @throws If you pass `null` or `undefined`. */ export const just = MaybeImpl.just; /** Is the {@linkcode Maybe} a {@linkcode Just}? @template T The type of the item contained in the `Maybe`. @param maybe The `Maybe` to check. @returns A type guarded `Just`. */ export function isJust(maybe) { return maybe.isJust; } /** Is the {@linkcode Maybe} a {@linkcode Nothing}? @template T The type of the item contained in the `Maybe`. @param maybe The `Maybe` to check. @returns A type guarded `Nothing`. */ export function isNothing(maybe) { return maybe.isNothing; } /** Create a {@linkcode Maybe} instance which is a {@linkcode Nothing}. If you want to create an instance with a specific type, e.g. for use in a function which expects a `Maybe<T>` where the `<T>` is known but you have no value to give it, you can use a type parameter: ```ts const notString = Maybe.nothing<string>(); ``` @template T The type of the item contained in the `Maybe`. @returns An instance of `Maybe.Nothing<T>`. */ export const nothing = MaybeImpl.nothing; /** Create a {@linkcode Maybe} from any value. To specify that the result should be interpreted as a specific type, you may invoke `Maybe.of` with an explicit type parameter: ```ts import * as maybe from 'true-myth/maybe'; const foo = maybe.of<string>(null); ``` This is usually only important in two cases: 1. If you are intentionally constructing a `Nothing` from a known `null` or undefined value *which is untyped*. 2. If you are specifying that the type is more general than the value passed (since TypeScript can define types as literals). @template T The type of the item contained in the `Maybe`. @param value The value to wrap in a `Maybe`. If it is `undefined` or `null`, the result will be `Nothing`; otherwise it will be the type of the value passed. */ export const of = MaybeImpl.of; export function map(mapFn, maybe) { const op = (m) => m.map(mapFn); return curry1(op, maybe); } export function mapOr(orU, mapFn, maybe) { function fullOp(fn, m) { return m.mapOr(orU, fn); } function partialOp(fn, curriedMaybe) { return curriedMaybe !== undefined ? fullOp(fn, curriedMaybe) : (extraCurriedMaybe) => fullOp(fn, extraCurriedMaybe); } return mapFn === undefined ? partialOp : maybe === undefined ? partialOp(mapFn) : partialOp(mapFn, maybe); } export function mapOrElse(orElseFn, mapFn, maybe) { function fullOp(fn, m) { return m.mapOrElse(orElseFn, fn); } function partialOp(fn, curriedMaybe) { return curriedMaybe !== undefined ? fullOp(fn, curriedMaybe) : (extraCurriedMaybe) => fullOp(fn, extraCurriedMaybe); } if (mapFn === undefined) { return partialOp; } else if (maybe === undefined) { return partialOp(mapFn); } else { return partialOp(mapFn, maybe); } } export function and(andMaybe, maybe) { const op = (m) => m.and(andMaybe); return curry1(op, maybe); } export function andThen(thenFn, maybe) { const op = (m) => m.andThen(thenFn); return maybe !== undefined ? op(maybe) : op; } export function or(defaultMaybe, maybe) { const op = (m) => m.or(defaultMaybe); return maybe !== undefined ? op(maybe) : op; } export function orElse(elseFn, maybe) { const op = (m) => m.orElse(elseFn); return curry1(op, maybe); } export function unwrapOr(defaultValue, maybe) { const op = (m) => m.unwrapOr(defaultValue); return curry1(op, maybe); } export function unwrapOrElse(orElseFn, maybe) { const op = (m) => m.unwrapOrElse(orElseFn); return curry1(op, maybe); } /** Create a `String` representation of a {@linkcode Maybe} instance. A {@linkcode Just} instance will be `Just(<representation of the value>)`, where the representation of the value is simply the value's own `toString` representation. For example: | call | output | |----------------------------------------|-------------------------| | `toString(Maybe.of(42))` | `Just(42)` | | `toString(Maybe.of([1, 2, 3]))` | `Just(1,2,3)` | | `toString(Maybe.of({ an: 'object' }))` | `Just([object Object])` | | `toString(Maybe.nothing())` | `Nothing` | @template T The type of the wrapped value; its own `.toString` will be used to print the interior contents of the `Just` variant. @param maybe The value to convert to a string. @returns The string representation of the `Maybe`. */ export function toString(maybe) { return maybe.toString(); } /** * Create an `Object` representation of a {@linkcode Maybe} instance. * * Useful for serialization. `JSON.stringify()` uses it. * * @param maybe The value to convert to JSON * @returns The JSON representation of the `Maybe` */ export function toJSON(maybe) { return maybe.toJSON(); } /** * Given a {@linkcode MaybeJSON} instance, convert it into a {@linkcode Maybe}. * * @param json The value to convert to JSON * @returns The JSON representation of the `Maybe` */ export function fromJSON(json) { return json.variant === Variant.Just ? just(json.value) : nothing(); } export function match(matcher, maybe) { const op = (curriedMaybe) => curriedMaybe.match(matcher); return curry1(op, maybe); } export function equals(mb, ma) { const op = (maybeA) => maybeA.equals(mb); return curry1(op, ma); } export function ap(maybeFn, maybe) { const op = (m) => maybeFn.ap(m); return curry1(op, maybe); } export function isInstance(item) { return item instanceof Maybe; } export function find(predicate, array) { const op = (a) => Maybe.of(a.find(predicate)); return curry1(op, array); } export function first(array) { return array.length !== 0 ? Maybe.just(Maybe.of(array[0])) : Maybe.nothing(); } export function last(array) { return array.length !== 0 ? Maybe.just(Maybe.of(array[array.length - 1])) : Maybe.nothing(); } /** Given an array or tuple of {@linkcode Maybe}s, return a `Maybe` of the array or tuple values. - Given an array of type `Array<Maybe<A> | Maybe<B>>`, the resulting type is `Maybe<Array<A | B>>`. - Given a tuple of type `[Maybe<A>, Maybe<B>]`, the resulting type is `Maybe<[A, B]>`. If any of the items in the array or tuple are {@linkcode Nothing}, the whole result is `Nothing`. If all items in the array or tuple are {@linkcode Just}, the whole result is `Just`. ## Examples Given an array with a mix of `Maybe` types in it, both `allJust` and `mixed` here will have the type `Maybe<Array<string | number>>`, but will be `Just` and `Nothing` respectively. ```ts import Maybe, { transposeArray } from 'true-myth/maybe'; let valid = [Maybe.just(2), Maybe.just('three')]; let allJust = transposeArray(valid); // => Just([2, 'three']); let invalid = [Maybe.just(2), Maybe.nothing<string>()]; let mixed = transposeArray(invalid); // => Nothing ``` When working with a tuple type, the structure of the tuple is preserved. Here, for example, `result` has the type `Maybe<[string, number]>` and will be `Nothing`: ```ts import Maybe, { transposeArray } from 'true-myth/maybe'; type Tuple = [Maybe<string>, Maybe<number>]; let invalid: Tuple = [Maybe.just('wat'), Maybe.nothing()]; let result = transposeArray(invalid); // => Nothing ``` If all of the items in the tuple are `Just`, the result is `Just` wrapping the tuple of the values of the items. Here, for example, `result` again has the type `Maybe<[string, number]>` and will be `Just(['hey', 12]`: ```ts import Maybe, { transposeArray } from 'true-myth/maybe'; type Tuple = [Maybe<string>, Maybe<number>]; let valid: Tuple = [Maybe.just('hey'), Maybe.just(12)]; let result = transposeArray(valid); // => Just(['hey', 12]) ``` @param maybes The `Maybe`s to resolve to a single `Maybe`. */ export function transposeArray(maybes) { // The slightly odd-seeming use of `[...ms, m]` here instead of `concat` is // necessary to preserve the structure of the value passed in. The goal is for // `[Maybe<string>, [Maybe<number>, Maybe<boolean>]]` not to be flattened into // `Maybe<[string, number, boolean]>` (as `concat` would do) but instead to // produce `Maybe<[string, [number, boolean]]>`. return maybes.reduce((acc, m) => acc.andThen((ms) => m.map((m) => [...ms, m])), just([])); } export function property(key, obj) { const op = (t) => Maybe.of(t[key]); return curry1(op, obj); } export function get(key, maybeObj) { return curry1(andThen(property(key)), maybeObj); } /** Transform a function from a normal JS function which may return `null` or `undefined` to a function which returns a {@linkcode Maybe} instead. For example, dealing with the `Document#querySelector` DOM API involves a *lot* of things which can be `null`: ```ts const foo = document.querySelector('#foo'); let width: number; if (foo !== null) { width = foo.getBoundingClientRect().width; } else { width = 0; } const getStyle = (el: HTMLElement, rule: string) => el.style[rule]; const bar = document.querySelector('.bar'); let color: string; if (bar != null) { let possibleColor = getStyle(bar, 'color'); if (possibleColor !== null) { color = possibleColor; } else { color = 'black'; } } ``` (Imagine in this example that there were more than two options: the simplifying workarounds you commonly use to make this terser in JS, like the ternary operator or the short-circuiting `||` or `??` operators, eventually become very confusing with more complicated flows.) We can work around this with `Maybe`, always wrapping each layer in {@linkcode Maybe.of} invocations, and this is *somewhat* better: ```ts import Maybe from 'true-myth/maybe'; const aWidth = Maybe.of(document.querySelector('#foo')) .map(el => el.getBoundingClientRect().width) .unwrapOr(0); const aColor = Maybe.of(document.querySelector('.bar')) .andThen(el => Maybe.of(getStyle(el, 'color')) .unwrapOr('black'); ``` With `safe`, though, you can create a transformed version of a function *once* and then be able to use it freely throughout your codebase, *always* getting back a `Maybe`: ```ts import { safe } from 'true-myth/maybe'; const querySelector = safe(document.querySelector.bind(document)); const safelyGetStyle = safe(getStyle); const aWidth = querySelector('#foo') .map(el => el.getBoundingClientRect().width) .unwrapOr(0); const aColor = querySelector('.bar') .andThen(el => safelyGetStyle(el, 'color')) .unwrapOr('black'); ``` @param fn The function to transform; the resulting function will have the exact same signature except for its return type. */ export function safe(fn) { return (...params) => Maybe.of(fn(...params)); } // Duplicate documentation because it will show up more nicely when rendered in // TypeDoc than if it applies to only one or the other; using `@inheritdoc` will // also work but works less well in terms of how editors render it (they do not // process that “directive” in general). /** * `Maybe` represents a value which may ({@linkcode Just `Just<T>`}) or may not * ({@linkcode Nothing}) be present. * * @class */ export const Maybe = MaybeImpl; export default Maybe; //# sourceMappingURL=maybe.js.map