diff --git a/benchmark/README.md b/benchmark/README.md index d8eb581..4dcd47d 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -5,19 +5,19 @@ Tested on a M1 Pro MacBook Pro. ## Option ``` -fp-ts Option none x 134,775,969 ops/sec ±0.70% (99 runs sampled) -fp-ts Option some x 95,087,860 ops/sec ±0.26% (97 runs sampled) -fp-ts Option some chain x 121,914,020 ops/sec ±0.19% (98 runs sampled) -Boxed Option none x 157,293,978 ops/sec ±0.11% (98 runs sampled) -Boxed Option some x 204,144,235 ops/sec ±0.27% (94 runs sampled) -Boxed Option some flatMap x 204,023,130 ops/sec ±0.65% (97 runs sampled) +fp-ts Option none x 130,955,745 ops/sec ±1.11% (98 runs sampled) +fp-ts Option some x 97,400,638 ops/sec ±0.26% (100 runs sampled) +fp-ts Option some chain x 2,776,666 ops/sec ±0.16% (97 runs sampled) +Boxed Option none x 1,030,036,990 ops/sec ±0.13% (100 runs sampled) +Boxed Option some x 461,820,063 ops/sec ±0.20% (101 runs sampled) +Boxed Option some flatMap x 461,736,015 ops/sec ±0.32% (97 runs sampled) ``` ## Result ``` -fp-ts Result x 114,543,587 ops/sec ±0.15% (99 runs sampled) -Boxed Result x 202,457,204 ops/sec ±0.27% (100 runs sampled) +fp-ts Result x 117,922,535 ops/sec ±0.39% (98 runs sampled) +Boxed Result x 465,661,918 ops/sec ±0.21% (101 runs sampled) ``` ## Future @@ -25,6 +25,6 @@ Boxed Result x 202,457,204 ops/sec ±0.27% (100 runs sampled) Careful on the interpretation of the following, as Future doesn't use microtasks and calls its listeners synchronously. ``` -Future x 36,365,914 ops/sec ±0.81% (92 runs sampled) -Promise x 12,402,743 ops/sec ±0.94% (90 runs sampled) +Future x 38,364,199 ops/sec ±0.68% (92 runs sampled) +Promise x 13,482,385 ops/sec ±0.25% (90 runs sampled) ``` diff --git a/src/AsyncData.ts b/src/AsyncData.ts index 2a2d003..0791feb 100644 --- a/src/AsyncData.ts +++ b/src/AsyncData.ts @@ -1,168 +1,113 @@ import { keys, values } from "./Dict"; import { Option, Result } from "./OptionResult"; -import { LooseRecord, Remap } from "./types"; +import { LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; -interface IAsyncData { +class __AsyncData { + static P = { + Done: (value: A) => ({ tag: "Done", value }) as const, + NotAsked: { tag: "NotAsked" } as const, + Loading: { tag: "Loading" } as const, + }; /** - * Returns the AsyncData containing the value from the callback - * - * (AsyncData\, A => B) => AsyncData\ - */ - map(this: AsyncData, func: (value: A) => B): AsyncData; - - /** - * Returns the AsyncData containing the value from the callback - * - * (AsyncData\, A => AsyncData\) => AsyncData\ - */ - flatMap( - this: AsyncData, - func: (value: A) => AsyncData, - ): AsyncData; - - /** - * Takes a callback taking the Ok value and returning a new result and returns an AsyncData with this new result - * - * (AsyncData\>, A => \) => AsyncData\> - */ - mapOkToResult( - this: AsyncData>, - func: (value: A) => Result, - ): AsyncData>; - - /** - * Takes a callback taking the Ok value and returning a new result and returns an AsyncData with this new result - * - * (AsyncData\>, E => \) => AsyncData\> - */ - mapErrorToResult( - this: AsyncData>, - func: (value: E) => Result, - ): AsyncData>; - - /** - * Takes a callback taking the Ok value and returning a new ok value and returns an AsyncData resolving to this new result - * - * (AsyncData\>, A => B) => AsyncData\> - */ - mapOk( - this: AsyncData>, - func: (value: A) => B, - ): AsyncData>; - - /** - * Takes a callback taking the Error value and returning a new error value and returns an AsyncData to this new result - * - * (AsyncData\>, E => B) => AsyncData\> - */ - mapError( - this: AsyncData>, - func: (value: E) => B, - ): AsyncData>; - - /** - * Takes a callback taking the Ok value and returning an AsyncData - * - * (AsyncData\>, A => AsyncData\>) => AsyncData\> - */ - flatMapOk( - this: AsyncData>, - func: (value: A) => AsyncData>, - ): AsyncData>; - - /** - * Takes a callback taking the Error value and returning an AsyncData - * - * (AsyncData\>, E => AsyncData\>) => AsyncData\> - */ - flatMapError( - this: AsyncData>, - func: (value: E) => AsyncData>, - ): AsyncData>; - - /** - * Return the value if present, and the fallback otherwise - * - * (AsyncData\, A) => A + * Create an AsyncData.Done value */ - getWithDefault(this: AsyncData, defaultValue: A): A; + static Done = (value: A): AsyncData => { + const asyncData = Object.create(ASYNC_DATA_PROTO) as Done; + asyncData.tag = "Done"; + asyncData.value = value; + return asyncData; + }; /** - * Explodes the AsyncData given its case + * Create an AsyncData.Loading value */ - match( - this: AsyncData, - config: { - Done: (value: A) => B; - Loading: () => B; - NotAsked: () => B; - }, - ): B; + static Loading = (): AsyncData => LOADING as Loading; /** - * Runs the callback and returns `this` + * Create an AsyncData.NotAsked value */ - tap( - this: AsyncData, - func: (asyncData: AsyncData) => unknown, - ): AsyncData; + static NotAsked = (): AsyncData => NOT_ASKED as NotAsked; /** - * Return an option of the value - * - * (AsyncData\) => Option\ + * Turns an array of asyncData into an asyncData of array */ - toOption(this: AsyncData): Option; + static all = [] | []>( + asyncDatas: AsyncDatas, + ) => { + const length = asyncDatas.length; + let acc = AsyncData.Done>([]); + let index = 0; - /** - * Typeguard - */ - isDone(this: AsyncData): this is Done; + while (true) { + if (index >= length) { + return acc as AsyncData<{ + [K in keyof AsyncDatas]: AsyncDatas[K] extends AsyncData + ? T + : never; + }>; + } - /** - * Typeguard - */ - isLoading(this: AsyncData): this is Loading; + const item = asyncDatas[index]; - /** - * Typeguard - */ - isNotAsked(this: AsyncData): this is NotAsked; -} + if (item != null) { + acc = acc.flatMap((array) => { + return item.map((value) => { + array.push(value); + return array; + }); + }); + } -type Done = Remap> & { - tag: "Done"; - value: A; + index++; + } + }; /** - * Returns the value. Use within `if (asyncData.isDone()) { ... }` + * Turns an dict of asyncData into a asyncData of dict */ - get(this: Done): A; -}; + static allFromDict = >>( + dict: Dict, + ): AsyncData<{ + [K in keyof Dict]: Dict[K] extends AsyncData ? T : never; + }> => { + const dictKeys = keys(dict); -type Loading = Remap> & { - tag: "Loading"; -}; + return AsyncData.all(values(dict)).map((values) => + Object.fromEntries(zip(dictKeys, values)), + ); + }; -type NotAsked = Remap> & { - tag: "NotAsked"; -}; + static equals = ( + a: AsyncData, + b: AsyncData, + equals: (a: A, b: A) => boolean, + ) => { + return a.tag === "Done" && b.tag === "Done" + ? equals(a.value, b.value) + : a.tag === b.tag; + }; -export type AsyncData = Done | Loading | NotAsked; + static isAsyncData = (value: unknown): value is AsyncData => + // @ts-ignore + value != null && value.__boxed_type__ === "AsyncData"; -const asyncDataProto = ((): IAsyncData => ({ - map(this: AsyncData, func: (value: A) => B) { - return this.tag === "Done" - ? Done(func(this.value)) - : (this as unknown as AsyncData); - }, + map(this: AsyncData, func: (value: A) => B): AsyncData { + if (this === NOT_ASKED || this === LOADING) { + return this as unknown as AsyncData; + } + return AsyncData.Done(func((this as Done).value)); + } - flatMap(this: AsyncData, func: (value: A) => AsyncData) { - return this.tag === "Done" - ? func(this.value) - : (this as unknown as AsyncData); - }, + flatMap( + this: AsyncData, + func: (value: A) => AsyncData, + ): AsyncData { + if (this === NOT_ASKED || this === LOADING) { + return this as unknown as AsyncData; + } + return func((this as Done).value); + } /** * For AsyncData>: @@ -179,7 +124,7 @@ const asyncDataProto = ((): IAsyncData => ({ Error: () => value as unknown as Result, }); }); - }, + } /** * For AsyncData>: @@ -196,7 +141,7 @@ const asyncDataProto = ((): IAsyncData => ({ Ok: () => value as unknown as Result, }); }); - }, + } /** * For AsyncData>: @@ -213,7 +158,7 @@ const asyncDataProto = ((): IAsyncData => ({ Error: () => value as unknown as Result, }); }); - }, + } /** * For AsyncData>: @@ -231,7 +176,7 @@ const asyncDataProto = ((): IAsyncData => ({ Error: (error) => Result.Error(func(error)), }); }); - }, + } /** * For AsyncData>: @@ -245,10 +190,10 @@ const asyncDataProto = ((): IAsyncData => ({ return this.flatMap((value) => { return value.match({ Ok: (value) => func(value) as AsyncData>, - Error: () => Done(value as unknown as Result), + Error: () => AsyncData.Done(value as unknown as Result), }); }); - }, + } /** * For AsyncData>: @@ -261,15 +206,25 @@ const asyncDataProto = ((): IAsyncData => ({ ): AsyncData> { return this.flatMap((value) => { return value.match({ - Ok: () => Done(value as unknown as Result), + Ok: () => AsyncData.Done(value as unknown as Result), Error: (error) => func(error) as AsyncData>, }); }); - }, + } - getWithDefault(this: AsyncData, defaultValue: A) { - return this.tag === "Done" ? this.value : defaultValue; - }, + /** + * Returns the value. Use within `if (asyncData.isDone()) { ... }` + */ + get(this: Done) { + return this.value; + } + + getWithDefault(this: AsyncData, defaultValue: A): A { + if (this === NOT_ASKED || this === LOADING) { + return defaultValue; + } + return (this as Done).value; + } match( this: AsyncData, @@ -278,148 +233,73 @@ const asyncDataProto = ((): IAsyncData => ({ Loading: () => B; NotAsked: () => B; }, - ) { - return this.tag === "Done" - ? config.Done(this.value) - : this.tag === "Loading" - ? config.Loading() - : config.NotAsked(); - }, + ): B { + if (this === NOT_ASKED) { + return config.NotAsked(); + } + if (this === LOADING) { + return config.Loading(); + } + return config.Done((this as Done).value); + } tap(this: AsyncData, func: (asyncData: AsyncData) => unknown) { func(this); return this; - }, + } - toOption(this: AsyncData) { - return this.tag === "Done" ? Option.Some(this.value) : Option.None(); - }, + toOption(this: AsyncData): Option { + if (this === NOT_ASKED || this === LOADING) { + return Option.None(); + } + return Option.Some((this as Done).value); + } - isDone(this: AsyncData): boolean { - return this.tag === "Done"; - }, + isDone(this: AsyncData): this is Done { + return this !== NOT_ASKED && this !== LOADING; + } - isLoading(this: AsyncData): boolean { - return this.tag === "Loading"; - }, + isLoading(this: AsyncData): this is Loading { + return this === LOADING; + } - isNotAsked(this: AsyncData): boolean { - return this.tag === "NotAsked"; - }, -}))(); + isNotAsked(this: AsyncData): this is NotAsked { + return this === NOT_ASKED; + } +} // @ts-expect-error -asyncDataProto.__boxed_type__ = "AsyncData"; +__AsyncData.prototype.__boxed_type__ = "AsyncData"; -const doneProto = ((): Omit, "tag" | "value"> => ({ - ...(asyncDataProto as IAsyncData), - - get() { - return this.value; - }, -}))(); - -const Done = (value: A): AsyncData => { - const asyncData = Object.create(doneProto) as Done; - asyncData.tag = "Done"; - asyncData.value = value; - return asyncData; -}; +const ASYNC_DATA_PROTO = Object.create( + null, + Object.getOwnPropertyDescriptors(__AsyncData.prototype), +); const LOADING = (() => { - const asyncData = Object.create(asyncDataProto) as Loading; + const asyncData = Object.create(ASYNC_DATA_PROTO) as Loading; asyncData.tag = "Loading"; return asyncData; })(); const NOT_ASKED = (() => { - const asyncData = Object.create(asyncDataProto) as NotAsked; + const asyncData = Object.create(ASYNC_DATA_PROTO) as NotAsked; asyncData.tag = "NotAsked"; return asyncData; })(); -const Loading = (): AsyncData => LOADING as Loading; -const NotAsked = (): AsyncData => NOT_ASKED as NotAsked; - -const asyncDataPattern = { - Done: (value: A) => ({ tag: "Done", value } as const), - NotAsked: { tag: "NotAsked" } as const, - Loading: { tag: "Loading" } as const, -}; - -export const AsyncData = { - /** - * Create an AsyncData.Done value - */ - Done, - - /** - * Create an AsyncData.Loading value - */ - Loading, - - /** - * Create an AsyncData.NotAsked value - */ - NotAsked, - - /** - * Turns an array of asyncData into an asyncData of array - */ - all[] | []>(asyncDatas: AsyncDatas) { - const length = asyncDatas.length; - let acc = AsyncData.Done>([]); - let index = 0; - - while (true) { - if (index >= length) { - return acc as AsyncData<{ - [K in keyof AsyncDatas]: AsyncDatas[K] extends AsyncData - ? T - : never; - }>; - } - - const item = asyncDatas[index]; - - if (item != null) { - acc = acc.flatMap((array) => { - return item.map((value) => { - array.push(value); - return array; - }); - }); - } - - index++; - } - }, - - /** - * Turns an dict of asyncData into a asyncData of dict - */ - allFromDict>>( - dict: Dict, - ): AsyncData<{ - [K in keyof Dict]: Dict[K] extends AsyncData ? T : never; - }> { - const dictKeys = keys(dict); - - return AsyncData.all(values(dict)).map((values) => - Object.fromEntries(zip(dictKeys, values)), - ); - }, +interface Done extends __AsyncData { + tag: "Done"; + value: A; +} - equals(a: AsyncData, b: AsyncData, equals: (a: A, b: A) => boolean) { - return a.tag === "Done" && b.tag === "Done" - ? equals(a.value, b.value) - : a.tag === b.tag; - }, +interface Loading extends __AsyncData { + tag: "Loading"; +} - isAsyncData: (value: unknown): value is AsyncData => - value != null && - (Object.prototype.isPrototypeOf.call(doneProto, value) || - Object.prototype.isPrototypeOf.call(asyncDataProto, value)), +interface NotAsked extends __AsyncData { + tag: "NotAsked"; +} - P: asyncDataPattern, -}; +export const AsyncData = __AsyncData; +export type AsyncData = Done | Loading | NotAsked; diff --git a/src/Future.ts b/src/Future.ts index 95a9128..2733354 100644 --- a/src/Future.ts +++ b/src/Future.ts @@ -3,24 +3,34 @@ import { Result } from "./OptionResult"; import { LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; -export class Future { +export class __Future { /** * Creates a new future from its initializer function (like `new Promise(...)`) */ static make = ( init: (resolver: (value: A) => void) => (() => void) | void, ): Future => { - return new Future(init); + const future = Object.create(FUTURE_PROTO) as Future; + const resolver = (value: A) => { + if (future._state.tag === "Pending") { + const resolveCallbacks = future._state.resolveCallbacks; + future._state = { tag: "Resolved", value }; + resolveCallbacks?.forEach((func) => func(value)); + } + }; + future._state = { tag: "Pending" }; + future._state.cancel = init(resolver); + return future as Future; }; static isFuture = (value: unknown): value is Future => - value != null && Object.prototype.isPrototypeOf.call(futureProto, value); + value != null && Object.prototype.isPrototypeOf.call(FUTURE_PROTO, value); /** * Creates a future resolved to the passed value */ static value = (value: A): Future => { - const future = Object.create(futureProto); + const future = Object.create(FUTURE_PROTO); future._state = { tag: "Resolved", value }; return future as Future; }; @@ -98,19 +108,8 @@ export class Future { | { tag: "Cancelled" } | { tag: "Resolved"; value: A }; - protected constructor( - init: (resolver: (value: A) => void) => (() => void) | void, - ) { - const resolver = (value: A) => { - if (this._state.tag === "Pending") { - const resolveCallbacks = this._state.resolveCallbacks; - this._state = { tag: "Resolved", value }; - resolveCallbacks?.forEach((func) => func(value)); - } - }; - + protected constructor() { this._state = { tag: "Pending" }; - this._state.cancel = init(resolver); } /** @@ -145,7 +144,10 @@ export class Future { const { cancel, cancelCallbacks } = this._state; // We have to set the future as cancelled first to avoid an infinite loop this._state = { tag: "Cancelled" }; - cancel?.(); + if (cancel != undefined) { + // @ts-ignore Compiler doesn't like that `cancel` is potentially `void` + cancel(); + } cancelCallbacks?.forEach((func) => func()); } } @@ -390,7 +392,10 @@ export class Future { } } -const futureProto = Object.create( +const FUTURE_PROTO = Object.create( null, - Object.getOwnPropertyDescriptors(Future.prototype), + Object.getOwnPropertyDescriptors(__Future.prototype), ); + +export const Future = __Future; +export type Future = __Future; diff --git a/src/OptionResult.ts b/src/OptionResult.ts index 4460ff1..569f606 100644 --- a/src/OptionResult.ts +++ b/src/OptionResult.ts @@ -1,221 +1,53 @@ import { keys, values } from "./Dict"; -import { LooseRecord, Remap } from "./types"; +import { LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; -interface IOption { - /** - * Returns the Option containing the value from the callback - * - * (Option\, A => B) => Option\ - */ - map(this: Option, func: (value: A) => B): Option; - - /** - * Returns the Option containing the value from the callback - * - * (Option\, A => Option\) => Option\ - */ - flatMap(this: Option, func: (value: A) => Option): Option; - - /** - * Returns the Option if its value matches the predicate, otherwise false - * - * (Option\, A => boolean) => Option\ - */ - filter( - this: Option, - func: (value: A) => value is B, - ): Option; - filter(this: Option, func: (value: A) => boolean): Option; - - /** - * Return the value if present, and the fallback otherwise - * - * (Option\, A) => A - */ - getWithDefault(this: Option, defaultValue: A): A; - - /** - * Explodes the Option given its case - */ - match( - this: Option, - config: { Some: (value: A) => B; None: () => B }, - ): B; - - /** - * Runs the callback and returns `this` - */ - tap(this: Option, func: (option: Option) => unknown): Option; - - /** - * Converts the Option\ to a `A | undefined` - */ - toUndefined(this: Option): A | undefined; - - /** - * Converts the Option\ to a `A | null` - */ - toNull(this: Option): A | null; +class __Option { + static P = { + Some: (value: A) => ({ tag: "Some", value }) as const, + None: { tag: "None" } as const, + }; - /** - * Takes the option and turns it into Ok(value) is Some, or Error(valueWhenNone) - */ - toResult(this: Option, valueWhenNone: E): Result; + static Some = (value: A): Option => { + const option = Object.create(OPTION_PROTO) as Some; + option.tag = "Some"; + option.value = value; + return option; + }; - /** - * Typeguard - */ - isSome(this: Option): this is Some; - - /** - * Typeguard - */ - isNone(this: Option): this is None; -} + static None = (): Option => NONE as None; -type Some = Remap> & { - tag: "Some"; - value: A; - - /** - * Returns the value. Use within `if (option.isSome()) { ... }` - */ - get(this: Some): A; -}; - -type None = Remap> & { - tag: "None"; -}; - -export type Option = Some | None; - -const optionProto = ((): IOption => ({ - map(this: Option, func: (value: A) => B) { - return this.tag === "Some" - ? Some(func(this.value)) - : (this as unknown as Option); - }, - - flatMap(this: Option, func: (value: A) => Option) { - return this.tag === "Some" - ? func(this.value) - : (this as unknown as Option); - }, - - filter(this: Option, func: (value: A) => boolean) { - return this.tag === "Some" && func(this.value) ? this : Option.None(); - }, - - getWithDefault(this: Option, defaultValue: A) { - return this.tag === "Some" ? this.value : defaultValue; - }, - - match(this: Option, config: { Some: (value: A) => B; None: () => B }) { - return this.tag === "Some" ? config.Some(this.value) : config.None(); - }, - - tap(this: Option, func: (option: Option) => unknown) { - func(this); - return this; - }, - - toUndefined(this: Option) { - return this.tag === "Some" ? this.value : undefined; - }, - - toNull(this: Option) { - return this.tag === "Some" ? this.value : null; - }, - - toResult(this: Option, valueWhenNone: E): Result { - return this.match({ - Some: (ok) => Result.Ok(ok), - None: () => Result.Error(valueWhenNone), - }); - }, - - isSome(this: Option): boolean { - return this.tag === "Some"; - }, - - isNone(this: Option): boolean { - return this.tag === "None"; - }, -}))(); - -// @ts-expect-error -optionProto.__boxed_type__ = "Option"; - -const someProto = ((): Omit, "tag" | "value"> => ({ - ...(optionProto as IOption), - - get() { - return this.value; - }, -}))(); - -const Some = (value: A): Option => { - const option = Object.create(someProto) as Some; - option.tag = "Some"; - option.value = value; - return option; -}; - -const NONE = (() => { - const option = Object.create(optionProto) as None; - option.tag = "None"; - return option; -})(); - -const None = (): Option => NONE as None; - -const optionPattern = { - Some: (value: A) => ({ tag: "Some", value } as const), - None: { tag: "None" } as const, -}; - -export const Option = { - /** - * Create an Option.Some value - */ - Some, - - /** - * Create an Option.None value - */ - None, - - isOption: (value: unknown): value is Option => - value != null && - (Object.prototype.isPrototypeOf.call(optionProto, value) || - Object.prototype.isPrototypeOf.call(someProto, value)), + static isOption = (value: unknown): value is Option => + // @ts-ignore + value != null && value.__boxed_type__ === "Option"; /** * Create an Option from a nullable value */ - fromNullable(nullable: A | null | undefined): Option { - return nullable == null ? None() : Some(nullable); - }, + static fromNullable = (nullable: A | null | undefined): Option => { + return nullable == null ? (NONE as None) : Option.Some(nullable); + }; /** * Create an Option from a value | null */ - fromNull(nullable: A | null): Option { - return nullable === null ? None() : Some(nullable); - }, + static fromNull = (nullable: A | null): Option => { + return nullable === null ? (NONE as None) : Option.Some(nullable); + }; /** * Create an Option from a undefined | value */ - fromUndefined(nullable: A | undefined): Option { - return nullable === undefined ? None() : Some(nullable); - }, + static fromUndefined = (nullable: A | undefined): Option => { + return nullable === undefined + ? (NONE as None) + : Option.Some(nullable); + }; /** * Turns an array of options into an option of array */ - all[] | []>(options: Options) { + static all = [] | []>(options: Options) => { const length = options.length; let acc = Option.Some>([]); let index = 0; @@ -240,302 +72,247 @@ export const Option = { index++; } - }, + }; /** * Turns an dict of options into a options of dict */ - allFromDict>>( + static allFromDict = >>( dict: Dict, ): Option<{ [K in keyof Dict]: Dict[K] extends Option ? T : never; - }> { + }> => { const dictKeys = keys(dict); - return this.all(values(dict)).map((values) => + return Option.all(values(dict)).map((values) => Object.fromEntries(zip(dictKeys, values)), ); - }, + }; - equals( + static equals = ( a: Option, b: Option, equals: (a: A, b: A) => boolean, - ): boolean { - return a.tag === "Some" && b.tag === "Some" - ? equals(a.value, b.value) + ): boolean => { + return a.isSome() && b.isSome() + ? equals(a.get(), b.get()) : a.tag === b.tag; - }, - - P: optionPattern, -}; + }; -interface IResult { /** - * Returns the Result containing the value from the callback + * Returns the Option containing the value from the callback * - * (Result\, A => B) => Result\ + * (Option\, A => B) => Option\ */ - map(this: Result, func: (value: A) => B): Result; + map(this: Option, func: (value: A) => B): Option { + if (this === NONE) { + return this as unknown as Option; + } + return Option.Some(func((this as Some).value)); + } /** - * Returns the Result containing the error returned from the callback + * Returns the Option containing the value from the callback * - * (Result\, E => F) => Result\ + * (Option\, A => Option\) => Option\ */ - mapError(this: Result, func: (value: E) => F): Result; + flatMap(this: Option, func: (value: A) => Option): Option { + if (this === NONE) { + return this as unknown as Option; + } + return func((this as Some).value); + } /** - * Returns the Result containing the value from the callback + * Returns the Option if its value matches the predicate, otherwise false * - * (Result\, A => Result\) => Result\ + * (Option\, A => boolean) => Option\ */ - flatMap( - this: Result, - func: (value: A) => Result, - ): Result; + filter( + this: Option, + func: (value: A) => value is B, + ): Option; + filter(this: Option, func: (value: A) => boolean): Option; + filter(this: Option, func: (value: A) => boolean): Option { + if (this === NONE) { + return this as unknown as Option; + } + return func((this as Some).value) ? this : (NONE as None); + } /** - * Returns the Result containing the value from the callback - * - * (Result\, E => Result\) => Result\ + * Returns the value. Use within `if (Option.isSome()) { ... }` */ - flatMapError( - this: Result, - func: (value: E) => Result, - ): Result; - + get(this: Some) { + return this.value; + } /** * Return the value if present, and the fallback otherwise * - * (Result\, A) => A + * (Option\, A) => A */ - getWithDefault(this: Result, defaultValue: A): A; + getWithDefault(this: Option, defaultValue: A): A { + if (this === NONE) { + return defaultValue; + } + return (this as Some).value; + } /** - * Explodes the Result given its case + * Explodes the Option given its case */ match( - this: Result, - config: { Ok: (value: A) => B; Error: (error: E) => B }, - ): B; + this: Option, + config: { Some: (value: A) => B; None: () => B }, + ): B { + if (this === NONE) { + return config.None(); + } + return config.Some((this as Some).value); + } /** * Runs the callback and returns `this` */ - tap( - this: Result, - func: (result: Result) => unknown, - ): Result; + tap(this: Option, func: (option: Option) => unknown): Option { + func(this); + return this; + } /** - * Runs the callback if ok and returns `this` + * Converts the Option\ to a `A | undefined` */ - tapOk(this: Result, func: (value: A) => unknown): Result; + toUndefined(this: Option): A | undefined { + if (this === NONE) { + return undefined; + } + return (this as Some).value; + } /** - * Runs the callback if error and returns `this` + * Converts the Option\ to a `A | null` */ - tapError(this: Result, func: (error: E) => unknown): Result; + toNull(this: Option): A | null { + if (this === NONE) { + return null; + } + return (this as Some).value; + } /** - * Return an option of the value - * - * (Result\) => Option\ + * Takes the option and turns it into Ok(value) is Some, or Error(valueWhenNone) */ - toOption(this: Result): Option; + toResult(this: Option, valueWhenNone: E): Result { + return this.match({ + Some: (ok) => Result.Ok(ok), + None: () => Result.Error(valueWhenNone), + }); + } /** * Typeguard */ - isOk(this: Result): this is Ok; + isSome(this: Option): this is Some { + return this !== NONE; + } /** * Typeguard */ - isError(this: Result): this is Error; + isNone(this: Option): this is None { + return this === NONE; + } } -type Ok = Remap> & { - tag: "Ok"; - value: A; - - /** - * Returns the ok value. Use within `if (result.isOk()) { ... }` - */ - get(this: Ok): A; -}; - -type Error = Remap> & { - tag: "Error"; - value: E; - - /** - * Returns the error value. Use within `if (result.isError()) { ... }` - */ - getError(this: Error): E; -}; - -export type Result = Ok | Error; - -const resultProto = ((): IResult => ({ - map(this: Result, func: (value: A) => B) { - return this.tag === "Ok" - ? Ok(func(this.value)) - : (this as unknown as Result); - }, - - mapError(this: Result, func: (value: E) => F) { - return this.tag === "Ok" - ? (this as unknown as Result) - : Error(func(this.value)); - }, - - flatMap(this: Result, func: (value: A) => Result) { - return this.tag === "Ok" - ? func(this.value) - : (this as unknown as Result); - }, - - flatMapError(this: Result, func: (value: E) => Result) { - return this.tag === "Ok" - ? (this as unknown as Result) - : func(this.value); - }, - - getWithDefault(this: Result, defaultValue: A) { - return this.tag === "Ok" ? this.value : defaultValue; - }, - - match( - this: Result, - config: { Ok: (value: A) => B; Error: (error: E) => B }, - ) { - return this.tag === "Ok" ? config.Ok(this.value) : config.Error(this.value); - }, - - tap(this: Result, func: (result: Result) => unknown) { - func(this); - return this; - }, - - tapOk(this: Result, func: (value: A) => unknown) { - if (this.tag === "Ok") { - func(this.value); - } - return this; - }, - - tapError(this: Result, func: (error: E) => unknown) { - if (this.tag === "Error") { - func(this.value); - } - return this; - }, - - toOption(this: Result) { - return this.tag === "Ok" ? Some(this.value) : None(); - }, - - isOk(this: Result): boolean { - return this.tag === "Ok"; - }, - - isError(this: Result): boolean { - return this.tag === "Error"; - }, -}))(); - // @ts-expect-error -resultProto.__boxed_type__ = "Result"; +__Option.prototype.__boxed_type__ = "Option"; -const okProto = ((): Omit, "tag" | "value"> => ({ - ...(resultProto as IResult), +const OPTION_PROTO = Object.create( + null, + Object.getOwnPropertyDescriptors(__Option.prototype), +); - get() { - return this.value; - }, -}))(); - -const errorProto = ((): Omit, "tag" | "value"> => ({ - ...(resultProto as IResult), +const NONE = (() => { + const option = Object.create(OPTION_PROTO) as None; + option.tag = "None"; + return option; +})(); - getError() { - return this.value; - }, -}))(); +interface Some extends __Option { + tag: "Some"; + value: A; +} -const Ok = (value: A): Result => { - const result = Object.create(okProto) as Ok; - result.tag = "Ok"; - result.value = value; - return result; -}; +interface None extends __Option { + tag: "None"; +} -const Error = (value: E): Result => { - const result = Object.create(errorProto) as Error; - result.tag = "Error"; - result.value = value; - return result; -}; +export const Option = __Option; +export type Option = Some | None; -const resultPattern = { - Ok: (value: A) => ({ tag: "Ok", value } as const), - Error: (value: E) => ({ tag: "Error", value } as const), -}; +class __Result { + static P = { + Ok: (value: A) => ({ tag: "Ok", value }) as const, + Error: (error: E) => ({ tag: "Error", error }) as const, + }; -export const Result = { - /** - * Create an Result.Ok value - */ - Ok, + static Ok = (value: A): Result => { + const result = Object.create(RESULT_PROTO) as Ok; + result.tag = "Ok"; + result.value = value; + return result; + }; - /** - * Create an Result.Error value - */ - Error, + static Error = (error: E): Result => { + const result = Object.create(RESULT_PROTO) as Error; + result.tag = "Error"; + result.error = error; + return result; + }; - isResult: (value: unknown): value is Result => - value != null && - (Object.prototype.isPrototypeOf.call(okProto, value) || - Object.prototype.isPrototypeOf.call(errorProto, value)), + static isResult = (value: unknown): value is Result => + // @ts-ignore + value != null && value.__boxed_type__ === "Result"; /** * Runs the function and resolves a result of its return value, or to an error if thrown */ - fromExecution(func: () => A): Result { + static fromExecution = (func: () => A): Result => { try { return Result.Ok(func()); } catch (error) { return Result.Error(error) as Result; } - }, + }; /** * Takes the promise and resolves a result of its value, or to an error if rejected */ - async fromPromise( + static fromPromise = async ( promise: Promise, - ): Promise> { + ): Promise> => { try { const value = await promise; return Result.Ok(value); } catch (error) { return Result.Error(error as E); } - }, + }; /** * Takes the option and turns it into Ok(value) is Some, or Error(valueWhenNone) */ - fromOption(option: Option, valueWhenNone: E): Result { + static fromOption = ( + option: Option, + valueWhenNone: E, + ): Result => { return option.toResult(valueWhenNone); - }, + }; /** * Turns an array of results into an result of array */ - all[] | []>(results: Results) { + static all = [] | []>(results: Results) => { const length = results.length; let acc = Result.Ok, unknown>([]); let index = 0; @@ -569,12 +346,12 @@ export const Result = { index++; } - }, + }; /** * Turns an dict of results into a results of dict */ - allFromDict>>( + static allFromDict = >>( dict: Dict, ): Result< { @@ -583,27 +360,187 @@ export const Result = { { [K in keyof Dict]: Dict[K] extends Result ? T : never; }[keyof Dict] - > { + > => { const dictKeys = keys(dict); return Result.all(values(dict)).map((values) => Object.fromEntries(zip(dictKeys, values)), ); - }, + }; - equals( + static equals = ( a: Result, b: Result, equals: (a: A, b: A) => boolean, - ) { + ) => { if (a.tag !== b.tag) { return false; } - if (a.tag === "Error" && b.tag === "Error") { + if (a.isError() && b.isError()) { return true; } - return equals(a.value as unknown as A, b.value as unknown as A); - }, - P: resultPattern, -}; + if (a.isOk() && b.isOk()) { + return equals(a.get(), b.get()); + } + + return false; + }; + + /** + * Returns the Result containing the value from the callback + * + * (Result\, A => B) => Result\ + */ + map(this: Result, func: (value: A) => B): Result { + return this.tag === "Ok" + ? Result.Ok(func(this.value)) + : (this as unknown as Result); + } + + /** + * Returns the Result containing the error returned from the callback + * + * (Result\, E => F) => Result\ + */ + mapError(this: Result, func: (value: E) => F): Result { + return this.tag === "Ok" + ? (this as unknown as Result) + : Result.Error(func(this.error)); + } + + /** + * Returns the Result containing the value from the callback + * + * (Result\, A => Result\) => Result\ + */ + flatMap( + this: Result, + func: (value: A) => Result, + ): Result { + return this.tag === "Ok" + ? func(this.value) + : (this as unknown as Result); + } + + /** + * Returns the Result containing the value from the callback + * + * (Result\, E => Result\) => Result\ + */ + flatMapError( + this: Result, + func: (value: E) => Result, + ): Result { + return this.tag === "Ok" + ? (this as unknown as Result) + : func(this.error); + } + /** + * Returns the value. Use within `if (result.isOk()) { ... }` + */ + get(this: Ok) { + return this.value; + } + + /** + * Returns the error. Use within `if (result.isError()) { ... }` + */ + getError(this: Error) { + return this.error; + } + + /** + * Return the value if present, and the fallback otherwise + * + * (Result\, A) => A + */ + getWithDefault(this: Result, defaultValue: A): A { + return this.tag === "Ok" ? this.value : defaultValue; + } + + /** + * Explodes the Result given its case + */ + match( + this: Result, + config: { Ok: (value: A) => B; Error: (error: E) => B }, + ): B { + return this.tag === "Ok" ? config.Ok(this.value) : config.Error(this.error); + } + + /** + * Runs the callback and returns `this` + */ + tap( + this: Result, + func: (result: Result) => unknown, + ): Result { + func(this); + return this; + } + + /** + * Runs the callback if ok and returns `this` + */ + tapOk(this: Result, func: (value: A) => unknown): Result { + if (this.tag === "Ok") { + func(this.value); + } + return this; + } + + /** + * Runs the callback if error and returns `this` + */ + tapError(this: Result, func: (error: E) => unknown): Result { + if (this.tag === "Error") { + func(this.error); + } + return this; + } + + /** + * Return an option of the value + * + * (Result\) => Option\ + */ + toOption(this: Result): Option { + return this.tag === "Ok" ? Option.Some(this.value) : (NONE as None); + } + + /** + * Typeguard + */ + isOk(this: Result): this is Ok { + return this.tag === "Ok"; + } + + /** + * Typeguard + */ + isError(this: Result): this is Error { + return this.tag === "Error"; + } +} + +// @ts-expect-error +__Result.prototype.__boxed_type__ = "Result"; + +const RESULT_PROTO = Object.create( + null, + Object.getOwnPropertyDescriptors(__Result.prototype), +); + +interface Ok extends __Result { + tag: "Ok"; + value: A; +} + +interface Error extends __Result { + tag: "Error"; + error: E; +} + +export const Result = __Result; +export type Result = Ok | Error; diff --git a/src/Serializer.ts b/src/Serializer.ts index 3650ed5..742e63d 100644 --- a/src/Serializer.ts +++ b/src/Serializer.ts @@ -20,6 +20,7 @@ export const encode = (value: any, indent?: number | undefined) => { __boxed_type__: "Result", tag: value.tag, value: value.value, + error: value.error, }; } if (value.__boxed_type__ === "AsyncData") { @@ -46,7 +47,7 @@ export const decode = (value: string) => { if (value.__boxed_type__ === "Result") { return value.tag === "Ok" ? Result.Ok(value.value) - : Result.Error(value.value); + : Result.Error(value.error); } if (value.__boxed_type__ === "AsyncData") { return value.tag === "NotAsked" diff --git a/src/types.ts b/src/types.ts index b025b2f..163c8ab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1 @@ export type LooseRecord = Record; - -export type Remap = { - [K in keyof T]: T[K]; -}; diff --git a/test/Result.test.ts b/test/Result.test.ts index 80937c6..6f575f0 100644 --- a/test/Result.test.ts +++ b/test/Result.test.ts @@ -139,7 +139,7 @@ test("Result.equals", () => { test("Result serialize", () => { expect(JSON.parse(JSON.stringify(Result.Error(1)))).toEqual({ tag: "Error", - value: 1, + error: 1, }); expect(JSON.parse(JSON.stringify(Result.Ok(1)))).toEqual({ tag: "Ok",