diff --git a/benchmark/README.md b/benchmark/README.md index 006a318..79f6ebf 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -4,39 +4,84 @@ Tested on a M1 Pro MacBook Pro. ## Option +### v8 + +``` +fp-ts Option none x 134,019,171 ops/sec ±1.58% (94 runs sampled) +fp-ts Option some x 90,043,117 ops/sec ±3.50% (97 runs sampled) +fp-ts Option some chain x 2,797,176 ops/sec ±1.35% (97 runs sampled) +effect Option none x 28,233,747 ops/sec ±0.52% (97 runs sampled) +effect Option some x 24,837,619 ops/sec ±1.55% (90 runs sampled) +effect Option some chain x 23,184,057 ops/sec ±3.08% (94 runs sampled) +Boxed Option none x 610,271,309 ops/sec ±1.22% (98 runs sampled) +Boxed Option some x 27,055,907 ops/sec ±0.09% (96 runs sampled) +Boxed Option some flatMap x 26,768,922 ops/sec ±2.66% (88 runs sampled) +``` + +### jscore + ``` -fp-ts Option none x 136,289,948 ops/sec ±0.35% (99 runs sampled) -fp-ts Option some x 92,748,146 ops/sec ±0.36% (95 runs sampled) -fp-ts Option some chain x 2,831,502 ops/sec ±0.21% (98 runs sampled) -effect Option none x 28,316,950 ops/sec ±0.68% (90 runs sampled) -effect Option some x 24,920,467 ops/sec ±0.78% (92 runs sampled) -effect Option some chain x 24,060,988 ops/sec ±0.31% (99 runs sampled) -Boxed Option none x 613,986,228 ops/sec ±0.20% (99 runs sampled) -Boxed Option some x 445,172,964 ops/sec ±0.50% (100 runs sampled) -Boxed Option some flatMap x 447,362,963 ops/sec ±0.53% (98 runs sampled) +fp-ts Option none x 26,370,592 ops/sec ±3.01% (80 runs sampled) +fp-ts Option some x 22,306,443 ops/sec ±3.35% (80 runs sampled) +fp-ts Option some chain x 6,395,953 ops/sec ±0.72% (94 runs sampled) +effect Option none x 34,944,770 ops/sec ±3.60% (80 runs sampled) +effect Option some x 24,062,381 ops/sec ±3.20% (78 runs sampled) +effect Option some chain x 21,272,877 ops/sec ±3.03% (77 runs sampled) +Boxed Option none x 360,292,187 ops/sec ±52.07% (26 runs sampled) +Boxed Option some x 42,614,750 ops/sec ±2.65% (81 runs sampled) +Boxed Option some flatMap x 39,815,444 ops/sec ±2.15% (85 runs sampled) ``` ## Result +### v8 + +``` +fp-ts Result x 116,486,882 ops/sec ±1.58% (96 runs sampled) +effect Result x 26,664,690 ops/sec ±1.45% (101 runs sampled) +Boxed Result x 28,373,628 ops/sec ±2.47% (99 runs sampled) +``` + +### jscore + ``` -fp-ts Result x 116,379,320 ops/sec ±0.48% (95 runs sampled) -effect Result x 26,538,884 ops/sec ±0.43% (96 runs sampled) -Boxed Result x 422,758,104 ops/sec ±0.98% (94 runs sampled) +fp-ts Result x 24,241,558 ops/sec ±4.98% (75 runs sampled) +effect Result x 26,450,388 ops/sec ±2.68% (79 runs sampled) +Boxed Result x 39,799,836 ops/sec ±2.69% (81 runs sampled) ``` ## Future Careful on the interpretation of the following, as Future doesn't use microtasks and calls its listeners synchronously. +### v8 + +``` +Promise x 13,345,062 ops/sec ±1.63% (86 runs sampled) +Future x 163,971 ops/sec ±50.30% (30 runs sampled) +``` + +### jscore + ``` -Promise x 13,399,997 ops/sec ±0.57% (81 runs sampled) -Future x 210,785 ops/sec ±7.41% (32 runs sampled) +Promise x 6,744,022 ops/sec ±3.20% (83 runs sampled) +Future x 162,704 ops/sec ±28.63% (24 runs sampled) ``` ## Future with Result +### v8 + +``` +fp-ts TaskEither x 524,318 ops/sec ±1.67% (90 runs sampled) +effect Effect x 193,219 ops/sec ±0.69% (90 runs sampled) +Future x 315,497 ops/sec ±10.37% (71 runs sampled) +``` + +### jscore + ``` -fp-ts TaskEither x 538,403 ops/sec ±0.29% (86 runs sampled) -effect Effect x 189,220 ops/sec ±0.94% (87 runs sampled) -Future x 575,626 ops/sec ±0.30% (90 runs sampled) +fp-ts TaskEither x 724,358 ops/sec ±7.41% (83 runs sampled) +effect Effect x 470,137 ops/sec ±0.86% (83 runs sampled) +Future x 707,572 ops/sec ±2.97% (72 runs sampled) ``` diff --git a/src/AsyncData.ts b/src/AsyncData.ts index 354e4b0..65eb7cc 100644 --- a/src/AsyncData.ts +++ b/src/AsyncData.ts @@ -1,9 +1,12 @@ import { keys, values } from "./Dict"; import { Option, Result } from "./OptionResult"; +import { createStore } from "./referenceStore"; import { BOXED_TYPE } from "./symbols"; import { JsonAsyncData, LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; +const AsyncDataStore = createStore(); + class __AsyncData { static P = { Done: (value: A) => ({ tag: "Done", value }) as const, @@ -14,10 +17,16 @@ class __AsyncData { * Create an AsyncData.Done value */ static Done = (value: A): AsyncData => { - const asyncData = Object.create(ASYNC_DATA_PROTO) as Done; - asyncData.tag = "Done"; - asyncData.value = value; - return asyncData; + const existing = AsyncDataStore.get(value); + if (existing === undefined) { + const asyncData = Object.create(ASYNC_DATA_PROTO) as Done; + asyncData.tag = "Done"; + asyncData.value = value; + AsyncDataStore.set(value, asyncData); + return asyncData; + } else { + return existing as Done; + } }; /** diff --git a/src/OptionResult.ts b/src/OptionResult.ts index 8ed321b..6ad7179 100644 --- a/src/OptionResult.ts +++ b/src/OptionResult.ts @@ -1,8 +1,11 @@ import { keys, values } from "./Dict"; +import { createStore } from "./referenceStore"; import { BOXED_TYPE } from "./symbols"; import { JsonOption, JsonResult, LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; +const OptionStore = createStore(); + class __Option { static P = { Some: (value: A) => ({ tag: "Some", value }) as const, @@ -10,10 +13,16 @@ class __Option { }; static Some = (value: A): Option => { - const option = Object.create(OPTION_PROTO) as Some; - option.tag = "Some"; - option.value = value; - return option; + const existing = OptionStore.get(value); + if (existing === undefined) { + const option = Object.create(OPTION_PROTO) as Some; + option.tag = "Some"; + option.value = value; + OptionStore.set(value, option); + return option; + } else { + return existing as Some; + } }; static None = (): Option => NONE as None; @@ -284,6 +293,9 @@ interface None extends __Option { export const Option = __Option; export type Option = Some | None; +const OkStore = createStore(); +const ErrorStore = createStore(); + class __Result { static P = { Ok: (value: A) => ({ tag: "Ok", value }) as const, @@ -291,17 +303,29 @@ class __Result { }; static Ok = (value: A): Result => { - const result = Object.create(RESULT_PROTO) as Ok; - result.tag = "Ok"; - result.value = value; - return result; + const existing = OkStore.get(value); + if (existing === undefined) { + const result = Object.create(RESULT_PROTO) as Ok; + result.tag = "Ok"; + result.value = value; + OkStore.set(value, result); + return result; + } else { + return existing as Ok; + } }; static Error = (error: E): Result => { - const result = Object.create(RESULT_PROTO) as Error; - result.tag = "Error"; - result.error = error; - return result; + const existing = ErrorStore.get(error); + if (existing === undefined) { + const result = Object.create(RESULT_PROTO) as Error; + result.tag = "Error"; + result.error = error; + ErrorStore.set(error, result); + return result; + } else { + return existing as Error; + } }; static isResult = (value: unknown): value is Result => diff --git a/src/referenceStore.ts b/src/referenceStore.ts new file mode 100644 index 0000000..a6f896f --- /dev/null +++ b/src/referenceStore.ts @@ -0,0 +1,20 @@ +export const createStore = () => { + const store = new Map>(); + + const registry = new FinalizationRegistry((value) => { + store.delete(value); + }); + + return { + set: (key: unknown, value: object) => { + store.set(key, new WeakRef(value)); + registry.register(value, key); + }, + get: (key: unknown): unknown => { + const value = store.get(key); + if (value !== undefined) { + return value.deref(); + } + }, + }; +}; diff --git a/test/AsyncData.test.ts b/test/AsyncData.test.ts index 0426746..25d1261 100644 --- a/test/AsyncData.test.ts +++ b/test/AsyncData.test.ts @@ -389,3 +389,10 @@ test("AsyncData JSON serialization", () => { }), ).toEqual(data); }); + +test("AsyncData equality", () => { + expect(AsyncData.Done(1)).toBe(AsyncData.Done(1)); + expect(AsyncData.Done({})).not.toBe(AsyncData.Done({})); + const x = {}; + expect(AsyncData.Done(x)).toBe(AsyncData.Done(x)); +}); diff --git a/test/Option.test.ts b/test/Option.test.ts index 14488cf..6b06ba0 100644 --- a/test/Option.test.ts +++ b/test/Option.test.ts @@ -211,3 +211,10 @@ test("Option JSON serialization", () => { }), ).toEqual(option); }); + +test("Option equality", () => { + expect(Option.Some(1)).toBe(Option.Some(1)); + expect(Option.Some({})).not.toBe(Option.Some({})); + const x = {}; + expect(Option.Some(x)).toBe(Option.Some(x)); +}); diff --git a/test/Result.test.ts b/test/Result.test.ts index 90b4f80..bcf6bd3 100644 --- a/test/Result.test.ts +++ b/test/Result.test.ts @@ -269,3 +269,13 @@ test("Result JSON serialization", async () => { }), ).toEqual(resultError); }); + +test("Result equality", () => { + expect(Result.Ok(1)).toBe(Result.Ok(1)); + expect(Result.Error(1)).toBe(Result.Error(1)); + expect(Result.Ok({})).not.toBe(Result.Ok({})); + expect(Result.Error({})).not.toBe(Result.Error({})); + const x = {}; + expect(Result.Ok(x)).toBe(Result.Ok(x)); + expect(Result.Error(x)).toBe(Result.Error(x)); +}); diff --git a/tsconfig.json b/tsconfig.json index ef94261..24c4327 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "target": "ES2015", - "lib": ["DOM", "DOM.Iterable", "ES2017"], + "lib": ["DOM", "DOM.Iterable", "ES2021"], "moduleResolution": "Node", "outDir": "dist/",