Skip to content

Commit

Permalink
Merge pull request #85 from swan-io/expiriment-referential-equality
Browse files Browse the repository at this point in the history
Experiment: referential equality
  • Loading branch information
bloodyowl authored Oct 21, 2024
2 parents 26331a5 + bcfd092 commit a0112d4
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 61 deletions.
79 changes: 62 additions & 17 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
43 changes: 28 additions & 15 deletions src/AsyncData.ts
Original file line number Diff line number Diff line change
@@ -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<A> {
static P = {
Done: <const A>(value: A) => ({ tag: "Done", value }) as const,
Expand All @@ -14,10 +17,19 @@ class __AsyncData<A> {
* Create an AsyncData.Done value
*/
static Done = <A = never>(value: A): AsyncData<A> => {
const asyncData = Object.create(ASYNC_DATA_PROTO) as Done<A>;
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<A>;
// @ts-expect-error
asyncData.tag = "Done";
// @ts-expect-error
asyncData.value = value;
Object.freeze(asyncData);
AsyncDataStore.set(value, asyncData);
return asyncData;
} else {
return existing as Done<A>;
}
};

/**
Expand Down Expand Up @@ -295,34 +307,35 @@ class __AsyncData<A> {
// @ts-expect-error
__AsyncData.prototype.__boxed_type__ = "AsyncData";

const ASYNC_DATA_PROTO = Object.create(
null,
Object.getOwnPropertyDescriptors(__AsyncData.prototype),
);
const ASYNC_DATA_PROTO = __AsyncData.prototype;

const LOADING = (() => {
const asyncData = Object.create(ASYNC_DATA_PROTO) as Loading<unknown>;
// @ts-expect-error
asyncData.tag = "Loading";
Object.freeze(asyncData);
return asyncData;
})();

const NOT_ASKED = (() => {
const asyncData = Object.create(ASYNC_DATA_PROTO) as NotAsked<unknown>;
// @ts-expect-error
asyncData.tag = "NotAsked";
Object.freeze(asyncData);
return asyncData;
})();

interface Done<A> extends __AsyncData<A> {
tag: "Done";
value: A;
interface Done<A> extends Readonly<__AsyncData<A>> {
readonly tag: "Done";
readonly value: A;
}

interface Loading<A> extends __AsyncData<A> {
tag: "Loading";
interface Loading<A> extends Readonly<__AsyncData<A>> {
readonly tag: "Loading";
}

interface NotAsked<A> extends __AsyncData<A> {
tag: "NotAsked";
interface NotAsked<A> extends Readonly<__AsyncData<A>> {
readonly tag: "NotAsked";
}

export const AsyncData = __AsyncData;
Expand Down
83 changes: 56 additions & 27 deletions src/OptionResult.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
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 SomeStore = createStore();

class __Option<A> {
static P = {
Some: <const A>(value: A) => ({ tag: "Some", value }) as const,
None: { tag: "None" } as const,
};

static Some = <A = never>(value: A): Option<A> => {
const option = Object.create(OPTION_PROTO) as Some<A>;
option.tag = "Some";
option.value = value;
return option;
const existing = SomeStore.get(value);
if (existing === undefined) {
const option = Object.create(OPTION_PROTO) as Some<A>;
// @ts-expect-error
option.tag = "Some";
// @ts-expect-error
option.value = value;
Object.freeze(option);
SomeStore.set(value, option);
return option;
} else {
return existing as Some<A>;
}
};

static None = <A = never>(): Option<A> => NONE as None<A>;
Expand Down Expand Up @@ -261,47 +273,67 @@ class __Option<A> {
// @ts-expect-error
__Option.prototype.__boxed_type__ = "Option";

const OPTION_PROTO = Object.create(
null,
Object.getOwnPropertyDescriptors(__Option.prototype),
);
const OPTION_PROTO = __Option.prototype;

const NONE = (() => {
const option = Object.create(OPTION_PROTO) as None<unknown>;
// @ts-expect-error
option.tag = "None";
Object.freeze(option);
return option;
})();

interface Some<A> extends __Option<A> {
tag: "Some";
value: A;
readonly tag: "Some";
readonly value: A;
}

interface None<A> extends __Option<A> {
tag: "None";
readonly tag: "None";
}

export const Option = __Option;
export type Option<A> = Some<A> | None<A>;

const OkStore = createStore();
const ErrorStore = createStore();

class __Result<A, E> {
static P = {
Ok: <const A>(value: A) => ({ tag: "Ok", value }) as const,
Error: <const E>(error: E) => ({ tag: "Error", error }) as const,
};

static Ok = <A = never, E = never>(value: A): Result<A, E> => {
const result = Object.create(RESULT_PROTO) as Ok<A, E>;
result.tag = "Ok";
result.value = value;
return result;
const existing = OkStore.get(value);
if (existing === undefined) {
const result = Object.create(RESULT_PROTO) as Ok<A, E>;
// @ts-expect-error
result.tag = "Ok";
// @ts-expect-error
result.value = value;
Object.freeze(result);
OkStore.set(value, result);
return result;
} else {
return existing as Ok<A, E>;
}
};

static Error = <A = never, E = never>(error: E): Result<A, E> => {
const result = Object.create(RESULT_PROTO) as Error<A, E>;
result.tag = "Error";
result.error = error;
return result;
const existing = ErrorStore.get(error);
if (existing === undefined) {
const result = Object.create(RESULT_PROTO) as Error<A, E>;
// @ts-expect-error
result.tag = "Error";
// @ts-expect-error
result.error = error;
Object.freeze(result);
ErrorStore.set(error, result);
return result;
} else {
return existing as Error<A, E>;
}
};

static isResult = (value: unknown): value is Result<unknown, unknown> =>
Expand Down Expand Up @@ -583,19 +615,16 @@ class __Result<A, E> {
// @ts-expect-error
__Result.prototype.__boxed_type__ = "Result";

const RESULT_PROTO = Object.create(
null,
Object.getOwnPropertyDescriptors(__Result.prototype),
);
const RESULT_PROTO = __Result.prototype;

interface Ok<A, E> extends __Result<A, E> {
tag: "Ok";
value: A;
readonly tag: "Ok";
readonly value: A;
}

interface Error<A, E> extends __Result<A, E> {
tag: "Error";
error: E;
readonly tag: "Error";
readonly error: E;
}

export const Result = __Result;
Expand Down
63 changes: 63 additions & 0 deletions src/referenceStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const weakRefFallbackMap = new WeakMap();

interface WeakRefLike<T extends WeakKey> {
deref(): T | undefined;
}

// We can replicate the WeakRef for older browsers using a `WeakMap` using
// the weakref-like instace as key.
const WeakRefWithLegacyFallback =
typeof WeakRef === "function"
? WeakRef
: class WeakRefWithFallback {
constructor(value: unknown) {
weakRefFallbackMap.set(this, value);
}
deref() {
return weakRefFallbackMap.get(this);
}
};
// For each tag variant that can contain a value (`Some`, `Ok`, `Error`, `Done`), we create a store.
// The store enables us to keep a map of `value` to the container of this `value`.

// The container is stored through a `WeakRef`, ensuring that we don't prevent the containers
// from being garbage-collected.
export const createStore = () => {
const store = new Map<unknown, WeakRefLike<object>>();

// We subscribe to the garbage collection, to make sure we don't keep in memory
// some values that we don't need as keys, because if all instances of a container of
// a given value have been garbage collected, there isn't any comparison required.

// For instance, let's say we have an object `x`, and create a `Option.Some(x)`:
// 1. As long as `Option.Some(x)` isn't garbage collected,
// it means that the value needs to remain stable as it can be compared to
// another `Option.Some(x)`.
// 2. If all references to `Option.Some(x)` are garbage collected, it means that
// it's safe to dispose, as we don't have anymore requirement for comparison.
// 3. If we re-create an `Option.Some(x)` afterwards, the first one re-created is
// the new basis for comparison.
const registry =
typeof FinalizationRegistry === "function"
? new FinalizationRegistry((value) => {
store.delete(value);
})
: undefined;

return {
set: (key: unknown, value: object) => {
store.set(key, new WeakRefWithLegacyFallback(value));
// Older browsers that don't have `FinalizationRegistry` don't benefit
// from the memory cleanup, but that's fine as a best-effort thing
if (registry !== undefined) {
registry.register(value, key);
}
},
get: (key: unknown): unknown => {
const value = store.get(key);
if (value !== undefined) {
return value.deref();
}
},
};
};
Loading

0 comments on commit a0112d4

Please sign in to comment.