initial commit
This commit is contained in:
86
benchmarks/README.md
Normal file
86
benchmarks/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# native-monad benchmarks
|
||||
|
||||
Comprehensive benchmark suite comparing `native-monad` against popular TypeScript monad libraries.
|
||||
|
||||
## Competitor Libraries
|
||||
|
||||
| Library | Result type | Option type | Async |
|
||||
| -------------- | ---------------- | ------------ | --------------- |
|
||||
| **neverthrow** | `Result<T,E>` | — | `ResultAsync` |
|
||||
| **ts-results-es** | `Result<T,E>` | `Option<T>` | — |
|
||||
| **oxide.ts** | `Result<T,E>` | `Option<T>` | — |
|
||||
| **fp-ts** | `Either<E,A>` | `Option<A>` | `TaskEither` |
|
||||
| **true-myth** | `Result<T,E>` | `Maybe<T>` | — |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The parent project must be built before running benchmarks:
|
||||
|
||||
```bash
|
||||
cd .. && pnpm build
|
||||
```
|
||||
|
||||
This happens automatically via the `prebench` script when running `pnpm bench`.
|
||||
|
||||
## Running Benchmarks
|
||||
|
||||
```bash
|
||||
# Install dependencies (first time only)
|
||||
pnpm install
|
||||
|
||||
# Run all benchmarks
|
||||
pnpm bench
|
||||
|
||||
# Run by type
|
||||
pnpm bench:result
|
||||
pnpm bench:option
|
||||
pnpm bench:query
|
||||
pnpm bench:scenarios
|
||||
|
||||
# Run a single file
|
||||
npx vitest bench result/construction.bench.ts --run
|
||||
```
|
||||
|
||||
## Reading Output
|
||||
|
||||
Vitest bench outputs a table per `describe` group:
|
||||
|
||||
| Column | Meaning |
|
||||
| ------ | ------------------------------------------ |
|
||||
| `hz` | Operations per second |
|
||||
| `min` | Fastest single iteration (ms) |
|
||||
| `max` | Slowest single iteration (ms) |
|
||||
| `mean` | Average iteration time (ms) |
|
||||
| `p75` | 75th percentile iteration time (ms) |
|
||||
| `p99` | 99th percentile iteration time (ms) |
|
||||
| `rme` | Relative margin of error (%) |
|
||||
|
||||
After all groups, a **Summary** section shows the fastest library per group and relative slowdown factors.
|
||||
|
||||
## Benchmark Structure
|
||||
|
||||
```
|
||||
result/ — Result/Either type benchmarks
|
||||
option/ — Option/Maybe type benchmarks
|
||||
query/ — Query (three-state) benchmarks + nested equivalents
|
||||
scenarios/ — Real-world patterns (parsing, validation, error propagation)
|
||||
helpers/ — Shared config, data generators, imports
|
||||
```
|
||||
|
||||
Each benchmark file covers a specific operation category:
|
||||
- `construction.bench.ts` — Creating Ok/Err/Some/None instances
|
||||
- `instance-methods.bench.ts` — map, flatMap, mapErr, match, filter
|
||||
- `type-guards.bench.ts` — isOk/isErr/isSome/isNone
|
||||
- `conversion.bench.ts` — toNullable, toUndefined, toString
|
||||
- `collection.bench.ts` — all, partition, compact, some, every
|
||||
- `chaining.bench.ts` — Multi-step pipelines
|
||||
- `from-wrapping.bench.ts` — Try-catch wrapping
|
||||
- `async.bench.ts` — Promise-based operations
|
||||
|
||||
## Caveats
|
||||
|
||||
- **Microbenchmarks** measure isolated operations. Real-world performance depends on usage patterns, GC pressure, and JIT behavior in context.
|
||||
- **Async benchmarks** have higher variance due to event loop scheduling. Results use extended warmup/time to compensate.
|
||||
- **fp-ts** uses a fundamentally different paradigm (`pipe` + standalone functions vs class methods). The benchmarks use each library's idiomatic API for fair comparison.
|
||||
- **`bench.skip`** appears when a library lacks an equivalent operation — no fabricated workarounds.
|
||||
- Collection benchmarks test at sizes 100 and 10K to show scaling behavior.
|
||||
25
benchmarks/helpers/bench-options.ts
Normal file
25
benchmarks/helpers/bench-options.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { BenchOptions } from 'vitest';
|
||||
|
||||
/** Standard options for most benchmarks — 500ms run time, 100 min iterations */
|
||||
export const STANDARD: BenchOptions = {
|
||||
time: 500,
|
||||
iterations: 100,
|
||||
warmupTime: 100,
|
||||
warmupIterations: 5,
|
||||
};
|
||||
|
||||
/** Extended options for async/expensive benchmarks — 1s run time */
|
||||
export const EXTENDED: BenchOptions = {
|
||||
time: 1000,
|
||||
iterations: 25,
|
||||
warmupTime: 250,
|
||||
warmupIterations: 3,
|
||||
};
|
||||
|
||||
/** Quick options for allocation/memory pattern benchmarks */
|
||||
export const QUICK: BenchOptions = {
|
||||
time: 250,
|
||||
iterations: 500,
|
||||
warmupTime: 50,
|
||||
warmupIterations: 10,
|
||||
};
|
||||
22
benchmarks/helpers/data-generators.ts
Normal file
22
benchmarks/helpers/data-generators.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const SIZES = [100, 10_000] as const;
|
||||
|
||||
export function generateNumbers(size: number): number[] {
|
||||
return Array.from({ length: size }, (_, i) => i);
|
||||
}
|
||||
|
||||
export function generateStrings(size: number): string[] {
|
||||
return Array.from({ length: size }, (_, i) => `item-${i}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates indices that should be "error" slots, deterministically distributed.
|
||||
* With errRatio=0.2 and size=10, returns indices [4, 9] (every 5th element).
|
||||
*/
|
||||
export function errorIndices(size: number, errRatio = 0.2): Set<number> {
|
||||
const step = Math.max(1, Math.floor(1 / errRatio));
|
||||
const indices = new Set<number>();
|
||||
for (let i = step - 1; i < size; i += step) {
|
||||
indices.add(i);
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
53
benchmarks/helpers/imports.ts
Normal file
53
benchmarks/helpers/imports.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// native-monad
|
||||
export {
|
||||
ok,
|
||||
err,
|
||||
some,
|
||||
none,
|
||||
okSome,
|
||||
okNone,
|
||||
errNone,
|
||||
Result,
|
||||
Option,
|
||||
Query,
|
||||
ResultPromise,
|
||||
OptionPromise,
|
||||
QueryPromise,
|
||||
} from 'native-monad';
|
||||
|
||||
// neverthrow
|
||||
export {
|
||||
ok as ntOk,
|
||||
err as ntErr,
|
||||
Result as NtResult,
|
||||
ResultAsync as NtResultAsync,
|
||||
fromThrowable as ntFromThrowable,
|
||||
} from 'neverthrow';
|
||||
|
||||
// ts-results-es
|
||||
export {
|
||||
Ok as TsOk,
|
||||
Err as TsErr,
|
||||
Some as TsSome,
|
||||
None as TsNone,
|
||||
Result as TsResult,
|
||||
Option as TsOption,
|
||||
} from 'ts-results-es';
|
||||
|
||||
// oxide.ts
|
||||
export {
|
||||
Ok as OxOk,
|
||||
Err as OxErr,
|
||||
Some as OxSome,
|
||||
None as OxNone,
|
||||
match as oxMatch,
|
||||
} from 'oxide.ts';
|
||||
|
||||
// fp-ts
|
||||
export * as E from 'fp-ts/Either';
|
||||
export * as O from 'fp-ts/Option';
|
||||
export { pipe } from 'fp-ts/function';
|
||||
|
||||
// true-myth (static functions are named exports, not on default)
|
||||
export * as TmResult from 'true-myth/result';
|
||||
export * as TmMaybe from 'true-myth/maybe';
|
||||
42
benchmarks/option/async.bench.ts
Normal file
42
benchmarks/option/async.bench.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { EXTENDED } from '../helpers/bench-options.js';
|
||||
import {
|
||||
some,
|
||||
none,
|
||||
OptionPromise,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OptionPromise.from — returns value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('OptionPromise.from — returns value', () => {
|
||||
bench('native-monad', async () => {
|
||||
await OptionPromise.from(() => Promise.resolve(42));
|
||||
}, EXTENDED);
|
||||
// No other library has an async Option wrapper
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OptionPromise.from — returns null
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('OptionPromise.from — returns null', () => {
|
||||
bench('native-monad', async () => {
|
||||
await OptionPromise.from(() => Promise.resolve(null));
|
||||
}, EXTENDED);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OptionPromise.all — all Some
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of [10, 100] as const) {
|
||||
describe(`OptionPromise.all — ${size} items, all Some`, () => {
|
||||
const promises = () => Array.from({ length: size }, (_, i) => Promise.resolve(some(i)));
|
||||
|
||||
bench('native-monad', async () => {
|
||||
await OptionPromise.all(promises());
|
||||
}, EXTENDED);
|
||||
});
|
||||
}
|
||||
132
benchmarks/option/chaining.bench.ts
Normal file
132
benchmarks/option/chaining.bench.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
some,
|
||||
none,
|
||||
TsSome,
|
||||
TsNone,
|
||||
OxSome,
|
||||
OxNone,
|
||||
O,
|
||||
pipe,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5-step pipeline — Some path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option chaining — 5-step pipeline (all Some)', () => {
|
||||
bench('native-monad', () => {
|
||||
some(42)
|
||||
.map(x => x * 2)
|
||||
.flatMap(x => (x > 0 ? some(x) : none()))
|
||||
.map(x => ({ value: x }))
|
||||
.flatMap(x => some({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
TsSome(42)
|
||||
.map(x => x * 2)
|
||||
.andThen(x => (x > 0 ? TsSome(x) : TsNone))
|
||||
.map(x => ({ value: x }))
|
||||
.andThen(x => TsSome({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
OxSome(42)
|
||||
.map(x => x * 2)
|
||||
.andThen(x => (x > 0 ? OxSome(x) : OxNone))
|
||||
.map(x => ({ value: x }))
|
||||
.andThen(x => OxSome({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(
|
||||
O.some(42),
|
||||
O.map(x => x * 2),
|
||||
O.flatMap(x => (x > 0 ? O.some(x) : O.none)),
|
||||
O.map(x => ({ value: x })),
|
||||
O.flatMap(x => O.some({ ...x, label: 'result' })),
|
||||
O.map(x => JSON.stringify(x)),
|
||||
);
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
TmMaybe.just(42)
|
||||
.map(x => x * 2)
|
||||
.andThen(x => (x > 0 ? TmMaybe.just(x) : TmMaybe.nothing()))
|
||||
.map(x => ({ value: x }))
|
||||
.andThen(x => TmMaybe.just({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// None short-circuit — 9 no-op maps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option chaining — None at step 1, 9 no-op maps', () => {
|
||||
const nmNone = none();
|
||||
const tsNone = TsNone;
|
||||
const oxNone = OxNone;
|
||||
const fpNone = O.none;
|
||||
const tmNone = TmMaybe.nothing<number>();
|
||||
const fn = (x: number) => x + 1;
|
||||
|
||||
bench('native-monad', () => {
|
||||
nmNone.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
tsNone.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
oxNone.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(fpNone, O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn));
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
tmNone.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 20-step deep map chain — Some path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option chaining — 20 maps on Some', () => {
|
||||
const fn = (x: number) => x + 1;
|
||||
|
||||
bench('native-monad', () => {
|
||||
let r = some(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
let r = TsSome(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
let r = OxSome(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
let r = O.some(0) as O.Option<number>;
|
||||
for (let i = 0; i < 20; i++) r = pipe(r, O.map(fn));
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
let r = TmMaybe.just(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||
}, STANDARD);
|
||||
});
|
||||
144
benchmarks/option/collection.bench.ts
Normal file
144
benchmarks/option/collection.bench.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD, EXTENDED } from '../helpers/bench-options.js';
|
||||
import { SIZES, errorIndices } from '../helpers/data-generators.js';
|
||||
import {
|
||||
some,
|
||||
none,
|
||||
Option,
|
||||
TsSome,
|
||||
TsNone,
|
||||
TsOption,
|
||||
OxSome,
|
||||
OxNone,
|
||||
O,
|
||||
pipe,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
import * as RA from 'fp-ts/ReadonlyArray';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.all — all Some
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
|
||||
const nmAll = Array.from({ length: size }, (_, i) => some(i));
|
||||
const tsAll = Array.from({ length: size }, (_, i) => TsSome(i));
|
||||
const fpAll = Array.from({ length: size }, (_, i) => O.some(i));
|
||||
const tmAll = Array.from({ length: size }, (_, i) => TmMaybe.just(i));
|
||||
|
||||
describe(`Option.all — ${size} items, all Some`, () => {
|
||||
bench('native-monad', () => { Option.all(nmAll); }, opts);
|
||||
bench('ts-results-es', () => { TsOption.all(...tsAll); }, opts);
|
||||
bench('fp-ts', () => { pipe(fpAll, RA.sequence(O.Applicative)); }, opts);
|
||||
bench('true-myth (manual)', () => {
|
||||
const values: number[] = [];
|
||||
for (const m of tmAll) {
|
||||
if (m.isNothing) return TmMaybe.nothing();
|
||||
values.push((m as { value: number }).value);
|
||||
}
|
||||
return TmMaybe.just(values);
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.all — 20% None (short-circuit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const noneIdx = errorIndices(size, 0.2);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? none() : some(i),
|
||||
);
|
||||
const tsMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? TsNone : TsSome(i),
|
||||
);
|
||||
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? O.none : O.some(i),
|
||||
);
|
||||
|
||||
describe(`Option.all — ${size} items, 20% None (short-circuit)`, () => {
|
||||
bench('native-monad', () => { Option.all(nmMixed); }, opts);
|
||||
bench('ts-results-es', () => { TsOption.all(...tsMixed); }, opts);
|
||||
bench('fp-ts', () => { pipe(fpMixed, RA.sequence(O.Applicative)); }, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.partition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const noneIdx = errorIndices(size, 0.2);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? none() : some(i),
|
||||
);
|
||||
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? O.none : O.some(i),
|
||||
);
|
||||
|
||||
describe(`Option.partition — ${size} items`, () => {
|
||||
bench('native-monad', () => { Option.partition(nmMixed); }, opts);
|
||||
bench('fp-ts (manual)', () => {
|
||||
const values: number[] = [];
|
||||
let noneCount = 0;
|
||||
for (const o of fpMixed) {
|
||||
if (O.isSome(o)) values.push(o.value);
|
||||
else noneCount++;
|
||||
}
|
||||
return { values, noneCount };
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.compact
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const noneIdx = errorIndices(size, 0.2);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? none() : some(i),
|
||||
);
|
||||
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? O.none : O.some(i),
|
||||
);
|
||||
|
||||
describe(`Option.compact — ${size} items`, () => {
|
||||
bench('native-monad', () => { Option.compact(nmMixed); }, opts);
|
||||
bench('fp-ts', () => { pipe(fpMixed, RA.compact); }, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.some / Option.every
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of [100, 1_000] as const) {
|
||||
const noneIdx = errorIndices(size, 0.2);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? none() : some(i),
|
||||
);
|
||||
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||
noneIdx.has(i) ? O.none : O.some(i),
|
||||
);
|
||||
|
||||
describe(`Option.some — ${size} items`, () => {
|
||||
bench('native-monad', () => { Option.some(nmMixed); }, STANDARD);
|
||||
bench('fp-ts (manual)', () => { fpMixed.some(O.isSome); }, STANDARD);
|
||||
});
|
||||
|
||||
describe(`Option.every — ${size} items`, () => {
|
||||
bench('native-monad', () => { Option.every(nmMixed); }, STANDARD);
|
||||
bench('fp-ts (manual)', () => { fpMixed.every(O.isSome); }, STANDARD);
|
||||
});
|
||||
}
|
||||
47
benchmarks/option/construction.bench.ts
Normal file
47
benchmarks/option/construction.bench.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
some,
|
||||
none,
|
||||
TsSome,
|
||||
TsNone,
|
||||
OxSome,
|
||||
OxNone,
|
||||
O,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Some construction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option construction — Some (primitive)', () => {
|
||||
bench('native-monad', () => { some(42); }, STANDARD);
|
||||
// neverthrow: no Option type
|
||||
bench('ts-results-es', () => { TsSome(42); }, STANDARD);
|
||||
bench('oxide.ts', () => { OxSome(42); }, STANDARD);
|
||||
bench('fp-ts', () => { O.some(42); }, STANDARD);
|
||||
bench('true-myth', () => { TmMaybe.just(42); }, STANDARD);
|
||||
});
|
||||
|
||||
describe('Option construction — Some (object payload)', () => {
|
||||
const value = { id: 1, name: 'Alice', roles: ['admin', 'user'] };
|
||||
|
||||
bench('native-monad', () => { some(value); }, STANDARD);
|
||||
bench('ts-results-es', () => { TsSome(value); }, STANDARD);
|
||||
bench('oxide.ts', () => { OxSome(value); }, STANDARD);
|
||||
bench('fp-ts', () => { O.some(value); }, STANDARD);
|
||||
bench('true-myth', () => { TmMaybe.just(value); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// None construction / access
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option construction — None', () => {
|
||||
bench('native-monad', () => { none(); }, STANDARD);
|
||||
bench('ts-results-es', () => { TsNone; }, STANDARD);
|
||||
bench('oxide.ts', () => { OxNone; }, STANDARD);
|
||||
bench('fp-ts', () => { O.none; }, STANDARD);
|
||||
bench('true-myth', () => { TmMaybe.nothing(); }, STANDARD);
|
||||
});
|
||||
55
benchmarks/option/conversion.bench.ts
Normal file
55
benchmarks/option/conversion.bench.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
some,
|
||||
none,
|
||||
OxSome,
|
||||
OxNone,
|
||||
O,
|
||||
pipe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toNullable — Some path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.toNullable — Some', () => {
|
||||
const nm = some(42);
|
||||
const ox = OxSome(42);
|
||||
const fp = O.some(42);
|
||||
|
||||
bench('native-monad', () => { nm.toNullable(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.into(); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.toNullable); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toNullable — None path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.toNullable — None', () => {
|
||||
const nm = none();
|
||||
const ox = OxNone;
|
||||
const fp = O.none;
|
||||
|
||||
bench('native-monad', () => { nm.toNullable(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.into(); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.toNullable); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toString
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.toString — Some', () => {
|
||||
const nm = some(42);
|
||||
|
||||
bench('native-monad', () => { nm.toString(); }, STANDARD);
|
||||
// Most other libraries don't have toString
|
||||
});
|
||||
|
||||
describe('Option.toString — None', () => {
|
||||
const nm = none();
|
||||
|
||||
bench('native-monad', () => { nm.toString(); }, STANDARD);
|
||||
});
|
||||
59
benchmarks/option/from-wrapping.bench.ts
Normal file
59
benchmarks/option/from-wrapping.bench.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
Option,
|
||||
O,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.from — non-nullish value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.from — non-nullish value', () => {
|
||||
bench('native-monad', () => { Option.from(42); }, STANDARD);
|
||||
bench('fp-ts', () => { O.fromNullable(42); }, STANDARD);
|
||||
bench('true-myth', () => { TmMaybe.of(42); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.from — null value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.from — null', () => {
|
||||
bench('native-monad', () => { Option.from(null); }, STANDARD);
|
||||
bench('fp-ts', () => { O.fromNullable(null); }, STANDARD);
|
||||
bench('true-myth', () => { TmMaybe.of(null); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.from — undefined value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.from — undefined', () => {
|
||||
bench('native-monad', () => { Option.from(undefined); }, STANDARD);
|
||||
bench('fp-ts', () => { O.fromNullable(undefined); }, STANDARD);
|
||||
bench('true-myth', () => { TmMaybe.of(undefined); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.from — function that returns value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.from — function returning value', () => {
|
||||
const fn = () => 42;
|
||||
|
||||
bench('native-monad', () => { Option.from(fn); }, STANDARD);
|
||||
// fp-ts: no function overload for fromNullable
|
||||
// true-myth: no function overload for of
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Option.from — function that returns null
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.from — function returning null', () => {
|
||||
const fn = () => null;
|
||||
|
||||
bench('native-monad', () => { Option.from(fn); }, STANDARD);
|
||||
});
|
||||
151
benchmarks/option/instance-methods.bench.ts
Normal file
151
benchmarks/option/instance-methods.bench.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
some,
|
||||
none,
|
||||
TsSome,
|
||||
TsNone,
|
||||
OxSome,
|
||||
OxNone,
|
||||
oxMatch,
|
||||
O,
|
||||
pipe,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
const double = (x: number) => x * 2;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// map — Some path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.map — Some path', () => {
|
||||
const nm = some(42);
|
||||
const ts = TsSome(42);
|
||||
const ox = OxSome(42);
|
||||
const fp = O.some(42);
|
||||
const tm = TmMaybe.just(42);
|
||||
|
||||
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.map(double); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.map(double); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.map(double)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.map(double); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// map — None path (no-op)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.map — None path (no-op)', () => {
|
||||
const nm = none();
|
||||
const ts = TsNone;
|
||||
const ox = OxNone;
|
||||
const fp = O.none;
|
||||
const tm = TmMaybe.nothing<number>();
|
||||
|
||||
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.map(double); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.map(double); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.map(double)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.map(double); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// flatMap — Some path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.flatMap — Some path', () => {
|
||||
const nm = some(42);
|
||||
const ts = TsSome(42);
|
||||
const ox = OxSome(42);
|
||||
const fp = O.some(42);
|
||||
const tm = TmMaybe.just(42);
|
||||
|
||||
bench('native-monad', () => { nm.flatMap(x => some(x * 2)); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.andThen(x => TsSome(x * 2)); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.andThen(x => OxSome(x * 2)); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.flatMap(x => O.some(x * 2))); }, STANDARD);
|
||||
bench('true-myth', () => { tm.andThen(x => TmMaybe.just(x * 2)); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// flatMap — None path (no-op)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.flatMap — None path (no-op)', () => {
|
||||
const nm = none();
|
||||
const ts = TsNone;
|
||||
const ox = OxNone;
|
||||
const fp = O.none;
|
||||
const tm = TmMaybe.nothing<number>();
|
||||
|
||||
bench('native-monad', () => { nm.flatMap(x => some(x * 2)); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.andThen(x => TsSome(x * 2)); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.andThen(x => OxSome(x * 2)); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.flatMap(x => O.some(x * 2))); }, STANDARD);
|
||||
bench('true-myth', () => { tm.andThen(x => TmMaybe.just(x * 2)); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// match — Some path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.match — Some path', () => {
|
||||
const nm = some(42);
|
||||
const ox = OxSome(42);
|
||||
const fp = O.some(42);
|
||||
const tm = TmMaybe.just(42);
|
||||
|
||||
bench('native-monad', () => { nm.match(x => x * 2, () => 0); }, STANDARD);
|
||||
// ts-results-es: no match
|
||||
bench('oxide.ts', () => { oxMatch(ox, { Some: (x: number) => x * 2, None: () => 0 }); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.match(() => 0, x => x * 2)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.match({ Just: x => x * 2, Nothing: () => 0 }); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// match — None path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.match — None path', () => {
|
||||
const nm = none();
|
||||
const ox = OxNone;
|
||||
const fp = O.none;
|
||||
const tm = TmMaybe.nothing<number>();
|
||||
|
||||
bench('native-monad', () => { nm.match(x => x * 2, () => 0); }, STANDARD);
|
||||
bench('oxide.ts', () => { oxMatch(ox, { Some: (x: number) => x * 2, None: () => 0 }); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.match(() => 0, x => x * 2)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.match({ Just: x => x * 2, Nothing: () => 0 }); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// filter — Some path (predicate passes)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.filter — predicate passes', () => {
|
||||
const nm = some(42);
|
||||
const ox = OxSome(42);
|
||||
const fp = O.some(42);
|
||||
|
||||
bench('native-monad', () => { nm.filter(x => x > 0); }, STANDARD);
|
||||
// ts-results-es: no filter
|
||||
bench('oxide.ts', () => { ox.filter(x => x > 0); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.filter(x => x > 0)); }, STANDARD);
|
||||
// true-myth: no filter on Maybe
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// filter — predicate fails
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.filter — predicate fails', () => {
|
||||
const nm = some(-1);
|
||||
const ox = OxSome(-1);
|
||||
const fp = O.some(-1);
|
||||
|
||||
bench('native-monad', () => { nm.filter(x => x > 0); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.filter(x => x > 0); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, O.filter(x => x > 0)); }, STANDARD);
|
||||
});
|
||||
66
benchmarks/option/type-guards.bench.ts
Normal file
66
benchmarks/option/type-guards.bench.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
some,
|
||||
none,
|
||||
TsSome,
|
||||
TsNone,
|
||||
OxSome,
|
||||
OxNone,
|
||||
O,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isSome — on Some
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.isSome — on Some', () => {
|
||||
const nm = some(42);
|
||||
const ts = TsSome(42);
|
||||
const ox = OxSome(42);
|
||||
const fp = O.some(42);
|
||||
const tm = TmMaybe.just(42);
|
||||
|
||||
bench('native-monad', () => { nm.isSome(); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.isSome(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.isSome(); }, STANDARD);
|
||||
bench('fp-ts', () => { O.isSome(fp); }, STANDARD);
|
||||
bench('true-myth', () => { tm.isJust; }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isSome — on None
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.isSome — on None', () => {
|
||||
const nm = none();
|
||||
const ts = TsNone;
|
||||
const ox = OxNone;
|
||||
const fp = O.none;
|
||||
const tm = TmMaybe.nothing<number>();
|
||||
|
||||
bench('native-monad', () => { nm.isSome(); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.isSome(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.isSome(); }, STANDARD);
|
||||
bench('fp-ts', () => { O.isSome(fp); }, STANDARD);
|
||||
bench('true-myth', () => { tm.isJust; }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isNone — on None
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Option.isNone — on None', () => {
|
||||
const nm = none();
|
||||
const ts = TsNone;
|
||||
const ox = OxNone;
|
||||
const fp = O.none;
|
||||
const tm = TmMaybe.nothing<number>();
|
||||
|
||||
bench('native-monad', () => { nm.isNone(); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.isNone(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.isNone(); }, STANDARD);
|
||||
bench('fp-ts', () => { O.isNone(fp); }, STANDARD);
|
||||
bench('true-myth', () => { tm.isNothing; }, STANDARD);
|
||||
});
|
||||
25
benchmarks/package.json
Normal file
25
benchmarks/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "native-monad-benchmarks",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prebench": "cd .. && pnpm build",
|
||||
"bench": "vitest bench",
|
||||
"bench:parallel": "vitest bench result/ > results/result.txt 2>&1 & vitest bench option/ > results/option.txt 2>&1 & vitest bench query/ > results/query.txt 2>&1 & vitest bench scenarios/ > results/scenarios.txt 2>&1 & wait && cat results/result.txt results/option.txt results/query.txt results/scenarios.txt",
|
||||
"bench:result": "vitest bench result/",
|
||||
"bench:option": "vitest bench option/",
|
||||
"bench:query": "vitest bench query/",
|
||||
"bench:scenarios": "vitest bench scenarios/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"native-monad": "file:..",
|
||||
"neverthrow": "^8.0.0",
|
||||
"ts-results-es": "^4.0.0",
|
||||
"oxide.ts": "^1.1.0",
|
||||
"fp-ts": "^2.16.0",
|
||||
"true-myth": "^7.0.0",
|
||||
"vitest": "^3.2.4",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
1046
benchmarks/pnpm-lock.yaml
generated
Normal file
1046
benchmarks/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
benchmarks/query/async.bench.ts
Normal file
42
benchmarks/query/async.bench.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { EXTENDED } from '../helpers/bench-options.js';
|
||||
import {
|
||||
okSome,
|
||||
okNone,
|
||||
QueryPromise,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// QueryPromise.from — returns value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('QueryPromise.from — returns value', () => {
|
||||
bench('native-monad', async () => {
|
||||
await QueryPromise.from(() => Promise.resolve(42));
|
||||
}, EXTENDED);
|
||||
// No other library has a three-state async wrapper
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// QueryPromise.from — returns null
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('QueryPromise.from — returns null', () => {
|
||||
bench('native-monad', async () => {
|
||||
await QueryPromise.from(() => Promise.resolve(null));
|
||||
}, EXTENDED);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// QueryPromise.all — all OkSome
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of [10, 100] as const) {
|
||||
describe(`QueryPromise.all — ${size} items, all OkSome`, () => {
|
||||
const promises = () => Array.from({ length: size }, (_, i) => Promise.resolve(okSome(i)));
|
||||
|
||||
bench('native-monad', async () => {
|
||||
await QueryPromise.all(promises());
|
||||
}, EXTENDED);
|
||||
});
|
||||
}
|
||||
128
benchmarks/query/chaining.bench.ts
Normal file
128
benchmarks/query/chaining.bench.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
okSome,
|
||||
okNone,
|
||||
errNone,
|
||||
E,
|
||||
O,
|
||||
pipe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5-step pipeline — OkSome path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query chaining — 5-step pipeline (all OkSome)', () => {
|
||||
bench('native-monad', () => {
|
||||
okSome(42)
|
||||
.map(x => x * 2)
|
||||
.flatMap(x => (x > 0 ? okSome(x) : okNone()))
|
||||
.map(x => ({ value: x }))
|
||||
.flatMap(x => okSome({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(
|
||||
E.right(O.some(42)) as E.Either<string, O.Option<number>>,
|
||||
E.map(O.map(x => x * 2)),
|
||||
E.flatMap(inner =>
|
||||
pipe(
|
||||
inner,
|
||||
O.match(
|
||||
() => E.right(O.none) as E.Either<string, O.Option<number>>,
|
||||
v => (v > 0 ? E.right(O.some(v)) : E.right(O.none)) as E.Either<string, O.Option<number>>,
|
||||
),
|
||||
),
|
||||
),
|
||||
E.map(O.map(x => ({ value: x }))),
|
||||
E.flatMap(inner =>
|
||||
pipe(
|
||||
inner,
|
||||
O.match(
|
||||
() => E.right(O.none) as E.Either<string, O.Option<{ value: number; label: string }>>,
|
||||
v => E.right(O.some({ ...v, label: 'result' })),
|
||||
),
|
||||
),
|
||||
),
|
||||
E.map(O.map(x => JSON.stringify(x))),
|
||||
);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ErrNone short-circuit — 9 no-op maps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query chaining — ErrNone at step 1, 9 no-op maps', () => {
|
||||
const nm = errNone('fail');
|
||||
const fp = E.left('fail') as E.Either<string, O.Option<number>>;
|
||||
const fn = (x: number) => x + 1;
|
||||
|
||||
bench('native-monad', () => {
|
||||
nm.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(
|
||||
fp,
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OkNone short-circuit — 9 no-op maps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query chaining — OkNone, 9 no-op maps', () => {
|
||||
const nm = okNone();
|
||||
const fp = E.right(O.none) as E.Either<string, O.Option<number>>;
|
||||
const fn = (x: number) => x + 1;
|
||||
|
||||
bench('native-monad', () => {
|
||||
nm.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(
|
||||
fp,
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
E.map(O.map(fn)),
|
||||
);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 20-step deep map chain — OkSome path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query chaining — 20 maps on OkSome', () => {
|
||||
const fn = (x: number) => x + 1;
|
||||
|
||||
bench('native-monad', () => {
|
||||
let r = okSome(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts (nested)', () => {
|
||||
let r = E.right(O.some(0)) as E.Either<string, O.Option<number>>;
|
||||
for (let i = 0; i < 20; i++) r = pipe(r, E.map(O.map(fn)));
|
||||
}, STANDARD);
|
||||
});
|
||||
144
benchmarks/query/collection.bench.ts
Normal file
144
benchmarks/query/collection.bench.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD, EXTENDED } from '../helpers/bench-options.js';
|
||||
import { SIZES, errorIndices } from '../helpers/data-generators.js';
|
||||
import {
|
||||
okSome,
|
||||
okNone,
|
||||
errNone,
|
||||
Query,
|
||||
E,
|
||||
O,
|
||||
pipe,
|
||||
} from '../helpers/imports.js';
|
||||
import * as RA from 'fp-ts/ReadonlyArray';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query.all — all OkSome
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
|
||||
const nmAll = Array.from({ length: size }, (_, i) => okSome(i));
|
||||
const fpAll = Array.from({ length: size }, (_, i) =>
|
||||
E.right(O.some(i)) as E.Either<string, O.Option<number>>,
|
||||
);
|
||||
|
||||
describe(`Query.all — ${size} items, all OkSome`, () => {
|
||||
bench('native-monad', () => { Query.all(nmAll); }, opts);
|
||||
bench('fp-ts (nested, manual)', () => {
|
||||
const values: number[] = [];
|
||||
for (const item of fpAll) {
|
||||
if (E.isLeft(item)) return item;
|
||||
if (O.isNone(item.right)) continue;
|
||||
values.push(item.right.value);
|
||||
}
|
||||
return E.right(O.some(values));
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query.all — mixed (OkSome + OkNone + ErrNone)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const errIdx = errorIndices(size, 0.1);
|
||||
const noneIdx = errorIndices(size, 0.15);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) => {
|
||||
if (errIdx.has(i)) return errNone('fail');
|
||||
if (noneIdx.has(i)) return okNone();
|
||||
return okSome(i);
|
||||
});
|
||||
|
||||
const fpMixed = Array.from({ length: size }, (_, i) => {
|
||||
if (errIdx.has(i)) return E.left('fail') as E.Either<string, O.Option<number>>;
|
||||
if (noneIdx.has(i)) return E.right(O.none) as E.Either<string, O.Option<number>>;
|
||||
return E.right(O.some(i)) as E.Either<string, O.Option<number>>;
|
||||
});
|
||||
|
||||
describe(`Query.all — ${size} items, mixed (short-circuit)`, () => {
|
||||
bench('native-monad', () => { Query.all(nmMixed); }, opts);
|
||||
bench('fp-ts (nested, manual)', () => {
|
||||
const values: number[] = [];
|
||||
for (const item of fpMixed) {
|
||||
if (E.isLeft(item)) return item;
|
||||
if (O.isNone(item.right)) continue;
|
||||
values.push(item.right.value);
|
||||
}
|
||||
return E.right(O.some(values));
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query.partition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const errIdx = errorIndices(size, 0.1);
|
||||
const noneIdx = errorIndices(size, 0.15);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) => {
|
||||
if (errIdx.has(i)) return errNone('fail');
|
||||
if (noneIdx.has(i)) return okNone();
|
||||
return okSome(i);
|
||||
});
|
||||
|
||||
const fpMixed = Array.from({ length: size }, (_, i) => {
|
||||
if (errIdx.has(i)) return E.left('fail') as E.Either<string, O.Option<number>>;
|
||||
if (noneIdx.has(i)) return E.right(O.none) as E.Either<string, O.Option<number>>;
|
||||
return E.right(O.some(i)) as E.Either<string, O.Option<number>>;
|
||||
});
|
||||
|
||||
describe(`Query.partition — ${size} items`, () => {
|
||||
bench('native-monad', () => { Query.partition(nmMixed); }, opts);
|
||||
bench('fp-ts (nested, manual)', () => {
|
||||
const values: number[] = [];
|
||||
const errors: string[] = [];
|
||||
let noneCount = 0;
|
||||
for (const item of fpMixed) {
|
||||
if (E.isLeft(item)) { errors.push(item.left); continue; }
|
||||
if (O.isNone(item.right)) { noneCount++; continue; }
|
||||
values.push(item.right.value);
|
||||
}
|
||||
return { values, noneCount, errors };
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query.compact
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const errIdx = errorIndices(size, 0.1);
|
||||
const noneIdx = errorIndices(size, 0.15);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) => {
|
||||
if (errIdx.has(i)) return errNone('fail');
|
||||
if (noneIdx.has(i)) return okNone();
|
||||
return okSome(i);
|
||||
});
|
||||
|
||||
const fpMixed = Array.from({ length: size }, (_, i) => {
|
||||
if (errIdx.has(i)) return E.left('fail') as E.Either<string, O.Option<number>>;
|
||||
if (noneIdx.has(i)) return E.right(O.none) as E.Either<string, O.Option<number>>;
|
||||
return E.right(O.some(i)) as E.Either<string, O.Option<number>>;
|
||||
});
|
||||
|
||||
describe(`Query.compact — ${size} items`, () => {
|
||||
bench('native-monad', () => { Query.compact(nmMixed); }, opts);
|
||||
bench('fp-ts (nested, manual)', () => {
|
||||
const values: number[] = [];
|
||||
for (const item of fpMixed) {
|
||||
if (E.isRight(item) && O.isSome(item.right)) values.push(item.right.value);
|
||||
}
|
||||
return values;
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
65
benchmarks/query/construction.bench.ts
Normal file
65
benchmarks/query/construction.bench.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
okSome,
|
||||
okNone,
|
||||
errNone,
|
||||
TsOk,
|
||||
TsErr,
|
||||
TsSome,
|
||||
TsNone,
|
||||
OxOk,
|
||||
OxErr,
|
||||
OxSome,
|
||||
OxNone,
|
||||
E,
|
||||
O,
|
||||
TmResult,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OkSome — vs nested Result<Option<T>, E>
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query construction — OkSome (primitive)', () => {
|
||||
bench('native-monad', () => { okSome(42); }, STANDARD);
|
||||
bench('ts-results-es (nested)', () => { TsOk(TsSome(42)); }, STANDARD);
|
||||
bench('oxide.ts (nested)', () => { OxOk(OxSome(42)); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => { E.right(O.some(42)); }, STANDARD);
|
||||
bench('true-myth (nested)', () => { TmResult.ok(TmMaybe.just(42)); }, STANDARD);
|
||||
});
|
||||
|
||||
describe('Query construction — OkSome (object payload)', () => {
|
||||
const value = { id: 1, name: 'Alice' };
|
||||
|
||||
bench('native-monad', () => { okSome(value); }, STANDARD);
|
||||
bench('ts-results-es (nested)', () => { TsOk(TsSome(value)); }, STANDARD);
|
||||
bench('oxide.ts (nested)', () => { OxOk(OxSome(value)); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => { E.right(O.some(value)); }, STANDARD);
|
||||
bench('true-myth (nested)', () => { TmResult.ok(TmMaybe.just(value)); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OkNone — vs nested Result<Option<T>, E>
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query construction — OkNone (singleton)', () => {
|
||||
bench('native-monad', () => { okNone(); }, STANDARD);
|
||||
bench('ts-results-es (nested)', () => { TsOk(TsNone); }, STANDARD);
|
||||
bench('oxide.ts (nested)', () => { OxOk(OxNone); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => { E.right(O.none); }, STANDARD);
|
||||
bench('true-myth (nested)', () => { TmResult.ok(TmMaybe.nothing()); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ErrNone — vs nested Err
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query construction — ErrNone', () => {
|
||||
bench('native-monad', () => { errNone('fail'); }, STANDARD);
|
||||
bench('ts-results-es (nested)', () => { TsErr('fail'); }, STANDARD);
|
||||
bench('oxide.ts (nested)', () => { OxErr('fail'); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => { E.left('fail'); }, STANDARD);
|
||||
bench('true-myth (nested)', () => { TmResult.err('fail'); }, STANDARD);
|
||||
});
|
||||
172
benchmarks/query/instance-methods.bench.ts
Normal file
172
benchmarks/query/instance-methods.bench.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
okSome,
|
||||
okNone,
|
||||
errNone,
|
||||
E,
|
||||
O,
|
||||
pipe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
const double = (x: number) => x * 2;
|
||||
const toUpper = (e: string) => e.toUpperCase();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// map — OkSome path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query.map — OkSome path', () => {
|
||||
const nm = okSome(42);
|
||||
const fp = E.right(O.some(42));
|
||||
|
||||
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(fp, E.map(O.map(double)));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// map — OkNone path (no-op)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query.map — OkNone path (no-op)', () => {
|
||||
const nm = okNone();
|
||||
const fp = E.right(O.none);
|
||||
|
||||
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(fp, E.map(O.map(double)));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// map — ErrNone path (no-op)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query.map — ErrNone path (no-op)', () => {
|
||||
const nm = errNone('fail');
|
||||
const fp = E.left('fail');
|
||||
|
||||
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(fp, E.map(O.map(double)));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// flatMap — OkSome path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query.flatMap — OkSome path', () => {
|
||||
const nm = okSome(42);
|
||||
const fp = E.right(O.some(42));
|
||||
|
||||
bench('native-monad', () => { nm.flatMap(x => okSome(x * 2)); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(
|
||||
fp,
|
||||
E.flatMap(inner =>
|
||||
pipe(
|
||||
inner,
|
||||
O.match(
|
||||
() => E.right(O.none) as E.Either<string, O.Option<number>>,
|
||||
v => E.right(O.some(v * 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mapErr — ErrNone path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query.mapErr — ErrNone path', () => {
|
||||
const nm = errNone('fail');
|
||||
const fp = E.left('fail');
|
||||
|
||||
bench('native-monad', () => { nm.mapErr(toUpper); }, STANDARD);
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(fp, E.mapLeft(toUpper));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// match — three-way
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query.match — OkSome (three-way dispatch)', () => {
|
||||
const nm = okSome(42);
|
||||
const fp = E.right(O.some(42));
|
||||
|
||||
bench('native-monad', () => {
|
||||
nm.match(v => v * 2, () => 0, () => -1);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts (nested, two match calls)', () => {
|
||||
pipe(
|
||||
fp,
|
||||
E.match(
|
||||
() => -1,
|
||||
O.match(() => 0, v => v * 2),
|
||||
),
|
||||
);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
describe('Query.match — OkNone (three-way dispatch)', () => {
|
||||
const nm = okNone();
|
||||
const fp = E.right(O.none);
|
||||
|
||||
bench('native-monad', () => {
|
||||
nm.match(() => 1, () => 0, () => -1);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(
|
||||
fp,
|
||||
E.match(
|
||||
() => -1,
|
||||
O.match(() => 0, () => 1),
|
||||
),
|
||||
);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
describe('Query.match — ErrNone (three-way dispatch)', () => {
|
||||
const nm = errNone('fail');
|
||||
const fp = E.left('fail');
|
||||
|
||||
bench('native-monad', () => {
|
||||
nm.match(() => 1, () => 0, e => e.length);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts (nested)', () => {
|
||||
pipe(
|
||||
fp,
|
||||
E.match(
|
||||
e => e.length,
|
||||
O.match(() => 0, () => 1),
|
||||
),
|
||||
);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// filter — OkSome path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Query.filter — predicate passes', () => {
|
||||
const nm = okSome(42);
|
||||
|
||||
bench('native-monad', () => { nm.filter(x => x > 0); }, STANDARD);
|
||||
// No other library has a three-state filter
|
||||
});
|
||||
|
||||
describe('Query.filter — predicate fails', () => {
|
||||
const nm = okSome(-1);
|
||||
|
||||
bench('native-monad', () => { nm.filter(x => x > 0); }, STANDARD);
|
||||
});
|
||||
70
benchmarks/result/async.bench.ts
Normal file
70
benchmarks/result/async.bench.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { EXTENDED } from '../helpers/bench-options.js';
|
||||
import { SIZES } from '../helpers/data-generators.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
Result,
|
||||
ResultPromise,
|
||||
ntOk,
|
||||
ntErr,
|
||||
NtResultAsync,
|
||||
E,
|
||||
pipe,
|
||||
} from '../helpers/imports.js';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ResultPromise.from — success
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('ResultPromise.from — success', () => {
|
||||
bench('native-monad', async () => {
|
||||
await ResultPromise.from(() => Promise.resolve(42));
|
||||
}, EXTENDED);
|
||||
|
||||
bench('neverthrow', async () => {
|
||||
await NtResultAsync.fromPromise(Promise.resolve(42), () => 'err');
|
||||
}, EXTENDED);
|
||||
|
||||
bench('fp-ts', async () => {
|
||||
await TE.tryCatch(() => Promise.resolve(42), () => 'err')();
|
||||
}, EXTENDED);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ResultPromise.from — failure
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('ResultPromise.from — failure', () => {
|
||||
bench('native-monad', async () => {
|
||||
await ResultPromise.from(() => Promise.reject(new Error('fail')), () => 'err');
|
||||
}, EXTENDED);
|
||||
|
||||
bench('neverthrow', async () => {
|
||||
await NtResultAsync.fromPromise(Promise.reject(new Error('fail')), () => 'err');
|
||||
}, EXTENDED);
|
||||
|
||||
bench('fp-ts', async () => {
|
||||
await TE.tryCatch(() => Promise.reject(new Error('fail')), () => 'err')();
|
||||
}, EXTENDED);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ResultPromise.all — all Ok
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of [10, 100] as const) {
|
||||
describe(`ResultPromise.all — ${size} items, all Ok`, () => {
|
||||
const nmPromises = () => Array.from({ length: size }, (_, i) => Promise.resolve(ok(i)));
|
||||
const ntPromises = () => Array.from({ length: size }, (_, i) => NtResultAsync.fromSafePromise(Promise.resolve(i)));
|
||||
|
||||
bench('native-monad', async () => {
|
||||
await ResultPromise.all(nmPromises());
|
||||
}, EXTENDED);
|
||||
|
||||
bench('neverthrow', async () => {
|
||||
await NtResultAsync.combine(ntPromises());
|
||||
}, EXTENDED);
|
||||
});
|
||||
}
|
||||
153
benchmarks/result/chaining.bench.ts
Normal file
153
benchmarks/result/chaining.bench.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
ntOk,
|
||||
ntErr,
|
||||
TsOk,
|
||||
TsErr,
|
||||
OxOk,
|
||||
OxErr,
|
||||
E,
|
||||
pipe,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5-step pipeline — success path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result chaining — 5-step pipeline (all succeed)', () => {
|
||||
bench('native-monad', () => {
|
||||
ok(42)
|
||||
.map(x => x * 2)
|
||||
.flatMap(x => (x > 0 ? ok(x) : err('negative')))
|
||||
.map(x => ({ value: x }))
|
||||
.flatMap(x => ok({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
ntOk(42)
|
||||
.map(x => x * 2)
|
||||
.andThen(x => (x > 0 ? ntOk(x) : ntErr('negative')))
|
||||
.map(x => ({ value: x }))
|
||||
.andThen(x => ntOk({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
TsOk<number, string>(42)
|
||||
.map(x => x * 2)
|
||||
.andThen(x => (x > 0 ? TsOk(x) : TsErr('negative')))
|
||||
.map(x => ({ value: x }))
|
||||
.andThen(x => TsOk<{ value: number; label: string }, string>({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
OxOk(42)
|
||||
.map(x => x * 2)
|
||||
.andThen(x => (x > 0 ? OxOk(x) : OxErr('negative')))
|
||||
.map(x => ({ value: x }))
|
||||
.andThen(x => OxOk({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(
|
||||
E.right(42),
|
||||
E.map(x => x * 2),
|
||||
E.flatMap(x => (x > 0 ? E.right(x) : E.left('negative'))),
|
||||
E.map(x => ({ value: x })),
|
||||
E.flatMap(x => E.right({ ...x, label: 'result' })),
|
||||
E.map(x => JSON.stringify(x)),
|
||||
);
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
TmResult.ok<number, string>(42)
|
||||
.map(x => x * 2)
|
||||
.andThen(x => (x > 0 ? TmResult.ok(x) : TmResult.err('negative')))
|
||||
.map(x => ({ value: x }))
|
||||
.andThen(x => TmResult.ok({ ...x, label: 'result' }))
|
||||
.map(x => JSON.stringify(x));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 10-step pipeline — error at step 1 (9 no-ops)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result chaining — error at step 1, 9 no-op maps', () => {
|
||||
const nmErr = err('fail');
|
||||
const ntErrVal = ntErr('fail');
|
||||
const tsErr = TsErr<number, string>('fail');
|
||||
const oxErr = OxErr('fail');
|
||||
const fpErr = E.left('fail');
|
||||
const tmErr = TmResult.err<number, string>('fail');
|
||||
const fn = (x: number) => x + 1;
|
||||
|
||||
bench('native-monad', () => {
|
||||
nmErr.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
ntErrVal.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
tsErr.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
oxErr.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(fpErr, E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn));
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
tmErr.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 20-step deep map chain — success path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result chaining — 20 maps on Ok', () => {
|
||||
const fn = (x: number) => x + 1;
|
||||
|
||||
bench('native-monad', () => {
|
||||
let r = ok(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
let r = ntOk(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
let r = TsOk<number, string>(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
let r = OxOk(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
let r = E.right(0) as E.Either<string, number>;
|
||||
for (let i = 0; i < 20; i++) r = pipe(r, E.map(fn));
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
let r = TmResult.ok<number, string>(0);
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||
}, STANDARD);
|
||||
});
|
||||
177
benchmarks/result/collection.bench.ts
Normal file
177
benchmarks/result/collection.bench.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD, EXTENDED } from '../helpers/bench-options.js';
|
||||
import { SIZES, errorIndices } from '../helpers/data-generators.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
Result,
|
||||
ntOk,
|
||||
ntErr,
|
||||
NtResult,
|
||||
TsOk,
|
||||
TsErr,
|
||||
TsResult,
|
||||
OxOk,
|
||||
OxErr,
|
||||
E,
|
||||
pipe,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
import * as RA from 'fp-ts/ReadonlyArray';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result.all — all Ok
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
|
||||
const nmAll = Array.from({ length: size }, (_, i) => ok(i));
|
||||
const ntAll = Array.from({ length: size }, (_, i) => ntOk(i));
|
||||
const tsAll = Array.from({ length: size }, (_, i) => TsOk<number, string>(i));
|
||||
const oxAll = Array.from({ length: size }, (_, i) => OxOk(i));
|
||||
const fpAll = Array.from({ length: size }, (_, i) => E.right(i));
|
||||
const tmAll = Array.from({ length: size }, (_, i) => TmResult.ok<number, string>(i));
|
||||
|
||||
describe(`Result.all — ${size} items, all Ok`, () => {
|
||||
bench('native-monad', () => { Result.all(nmAll); }, opts);
|
||||
bench('neverthrow', () => { NtResult.combine(ntAll); }, opts);
|
||||
bench('ts-results-es', () => { TsResult.all(...tsAll); }, opts);
|
||||
// oxide.ts: no built-in all/combine
|
||||
bench('fp-ts', () => { pipe(fpAll, RA.sequence(E.Applicative)); }, opts);
|
||||
// true-myth: no built-in all
|
||||
bench('true-myth (manual)', () => {
|
||||
const values: number[] = [];
|
||||
for (const r of tmAll) {
|
||||
if (TmResult.isErr(r)) return r;
|
||||
values.push((r as { value: number }).value);
|
||||
}
|
||||
return TmResult.ok(values);
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result.all — 20% Err (short-circuit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const errIdx = errorIndices(size, 0.2);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? err('fail') : ok(i),
|
||||
);
|
||||
const ntMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? ntErr('fail') : ntOk(i),
|
||||
);
|
||||
const tsMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? TsErr<number, string>('fail') : TsOk<number, string>(i),
|
||||
);
|
||||
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? E.left('fail' as const) : E.right(i),
|
||||
);
|
||||
const tmMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? TmResult.err<number, string>('fail') : TmResult.ok<number, string>(i),
|
||||
);
|
||||
|
||||
describe(`Result.all — ${size} items, 20% Err (short-circuit)`, () => {
|
||||
bench('native-monad', () => { Result.all(nmMixed); }, opts);
|
||||
bench('neverthrow', () => { NtResult.combine(ntMixed as any); }, opts);
|
||||
bench('ts-results-es', () => { TsResult.all(...tsMixed); }, opts);
|
||||
bench('fp-ts', () => { pipe(fpMixed, RA.sequence(E.Applicative)); }, opts);
|
||||
bench('true-myth (manual)', () => {
|
||||
const values: number[] = [];
|
||||
for (const r of tmMixed) {
|
||||
if (TmResult.isErr(r)) return r;
|
||||
values.push((r as { value: number }).value);
|
||||
}
|
||||
return TmResult.ok(values);
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result.partition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const errIdx = errorIndices(size, 0.2);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? err('fail') : ok(i),
|
||||
);
|
||||
const ntMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? ntErr('fail') : ntOk(i),
|
||||
);
|
||||
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? E.left('fail' as const) : E.right(i),
|
||||
);
|
||||
|
||||
describe(`Result.partition — ${size} items`, () => {
|
||||
bench('native-monad', () => { Result.partition(nmMixed); }, opts);
|
||||
bench('neverthrow (manual)', () => {
|
||||
const values: number[] = [];
|
||||
const errors: string[] = [];
|
||||
for (const r of ntMixed) {
|
||||
if (r.isOk()) values.push(r.value);
|
||||
else errors.push(r.error);
|
||||
}
|
||||
return { values, errors };
|
||||
}, opts);
|
||||
bench('fp-ts', () => { RA.separate(fpMixed); }, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result.compact
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of SIZES) {
|
||||
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||
const errIdx = errorIndices(size, 0.2);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? err('fail') : ok(i),
|
||||
);
|
||||
const ntMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? ntErr('fail') : ntOk(i),
|
||||
);
|
||||
|
||||
describe(`Result.compact — ${size} items`, () => {
|
||||
bench('native-monad', () => { Result.compact(nmMixed); }, opts);
|
||||
bench('neverthrow (manual)', () => {
|
||||
const values: number[] = [];
|
||||
for (const r of ntMixed) {
|
||||
if (r.isOk()) values.push(r.value);
|
||||
}
|
||||
return values;
|
||||
}, opts);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result.some / Result.every
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const size of [100, 1_000]) {
|
||||
const errIdx = errorIndices(size, 0.2);
|
||||
|
||||
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? err('fail') : ok(i),
|
||||
);
|
||||
const ntMixed = Array.from({ length: size }, (_, i) =>
|
||||
errIdx.has(i) ? ntErr('fail') : ntOk(i),
|
||||
);
|
||||
|
||||
describe(`Result.some — ${size} items`, () => {
|
||||
bench('native-monad', () => { Result.some(nmMixed); }, STANDARD);
|
||||
bench('neverthrow (manual)', () => { ntMixed.some(r => r.isOk()); }, STANDARD);
|
||||
});
|
||||
|
||||
describe(`Result.every — ${size} items`, () => {
|
||||
bench('native-monad', () => { Result.every(nmMixed); }, STANDARD);
|
||||
bench('neverthrow (manual)', () => { ntMixed.every(r => r.isOk()); }, STANDARD);
|
||||
});
|
||||
}
|
||||
59
benchmarks/result/construction.bench.ts
Normal file
59
benchmarks/result/construction.bench.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
ntOk,
|
||||
ntErr,
|
||||
TsOk,
|
||||
TsErr,
|
||||
OxOk,
|
||||
OxErr,
|
||||
E,
|
||||
pipe,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ok / Right / success construction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result construction — Ok (primitive)', () => {
|
||||
bench('native-monad', () => { ok(42); }, STANDARD);
|
||||
bench('neverthrow', () => { ntOk(42); }, STANDARD);
|
||||
bench('ts-results-es', () => { TsOk(42); }, STANDARD);
|
||||
bench('oxide.ts', () => { OxOk(42); }, STANDARD);
|
||||
bench('fp-ts', () => { E.right(42); }, STANDARD);
|
||||
bench('true-myth', () => { TmResult.ok(42); }, STANDARD);
|
||||
});
|
||||
|
||||
describe('Result construction — Err (primitive)', () => {
|
||||
bench('native-monad', () => { err('fail'); }, STANDARD);
|
||||
bench('neverthrow', () => { ntErr('fail'); }, STANDARD);
|
||||
bench('ts-results-es', () => { TsErr('fail'); }, STANDARD);
|
||||
bench('oxide.ts', () => { OxErr('fail'); }, STANDARD);
|
||||
bench('fp-ts', () => { E.left('fail'); }, STANDARD);
|
||||
bench('true-myth', () => { TmResult.err('fail'); }, STANDARD);
|
||||
});
|
||||
|
||||
describe('Result construction — Ok (object payload)', () => {
|
||||
const value = { id: 1, name: 'Alice', roles: ['admin', 'user'] };
|
||||
|
||||
bench('native-monad', () => { ok(value); }, STANDARD);
|
||||
bench('neverthrow', () => { ntOk(value); }, STANDARD);
|
||||
bench('ts-results-es', () => { TsOk(value); }, STANDARD);
|
||||
bench('oxide.ts', () => { OxOk(value); }, STANDARD);
|
||||
bench('fp-ts', () => { E.right(value); }, STANDARD);
|
||||
bench('true-myth', () => { TmResult.ok(value); }, STANDARD);
|
||||
});
|
||||
|
||||
describe('Result construction — Err (Error object)', () => {
|
||||
const error = new Error('something went wrong');
|
||||
|
||||
bench('native-monad', () => { err(error); }, STANDARD);
|
||||
bench('neverthrow', () => { ntErr(error); }, STANDARD);
|
||||
bench('ts-results-es', () => { TsErr(error); }, STANDARD);
|
||||
bench('oxide.ts', () => { OxErr(error); }, STANDARD);
|
||||
bench('fp-ts', () => { E.left(error); }, STANDARD);
|
||||
bench('true-myth', () => { TmResult.err(error); }, STANDARD);
|
||||
});
|
||||
68
benchmarks/result/conversion.bench.ts
Normal file
68
benchmarks/result/conversion.bench.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
ntOk,
|
||||
ntErr,
|
||||
TsOk,
|
||||
TsErr,
|
||||
OxOk,
|
||||
OxErr,
|
||||
E,
|
||||
pipe,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toNullable — success path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.toNullable — Ok', () => {
|
||||
const nm = ok(42);
|
||||
const ox = OxOk(42);
|
||||
|
||||
bench('native-monad', () => { nm.toNullable(); }, STANDARD);
|
||||
// neverthrow: no toNullable — use match or _unsafeUnwrap
|
||||
// ts-results-es: no toNullable
|
||||
bench('oxide.ts', () => { ox.into(); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(E.right(42), E.toUnion); }, STANDARD);
|
||||
bench('true-myth', () => { TmResult.ok<number, string>(42).isOk ? 42 : null; }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toNullable — failure path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.toNullable — Err', () => {
|
||||
const nm = err('fail');
|
||||
const ox = OxErr('fail');
|
||||
|
||||
bench('native-monad', () => { nm.toNullable(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.into(); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(E.left('fail'), E.toUnion); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toString
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.toString — Ok', () => {
|
||||
const nm = ok(42);
|
||||
const ox = OxOk(42);
|
||||
|
||||
bench('native-monad', () => { nm.toString(); }, STANDARD);
|
||||
// neverthrow: no toString
|
||||
// ts-results-es: no toString
|
||||
bench('oxide.ts', () => { ox.toString(); }, STANDARD);
|
||||
// fp-ts: no toString
|
||||
// true-myth: no toString
|
||||
});
|
||||
|
||||
describe('Result.toString — Err', () => {
|
||||
const nm = err('fail');
|
||||
const ox = OxErr('fail');
|
||||
|
||||
bench('native-monad', () => { nm.toString(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.toString(); }, STANDARD);
|
||||
});
|
||||
39
benchmarks/result/from-wrapping.bench.ts
Normal file
39
benchmarks/result/from-wrapping.bench.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
Result,
|
||||
ntFromThrowable,
|
||||
E,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result.from — success (no throw)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.from — success (no throw)', () => {
|
||||
const fn = () => 42;
|
||||
|
||||
bench('native-monad', () => { Result.from(fn); }, STANDARD);
|
||||
bench('neverthrow', () => { ntFromThrowable(fn)(); }, STANDARD);
|
||||
bench('fp-ts', () => { E.tryCatch(fn, () => 'error'); }, STANDARD);
|
||||
bench('true-myth', () => { TmResult.tryOr('error', fn); }, STANDARD);
|
||||
// ts-results-es: no from/tryCatch
|
||||
// oxide.ts: no from/tryCatch
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result.from — failure (throws)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.from — failure (throws)', () => {
|
||||
const thrower = () => {
|
||||
throw new Error('fail');
|
||||
};
|
||||
const mapErr = () => 'error' as const;
|
||||
|
||||
bench('native-monad', () => { Result.from(thrower, mapErr); }, STANDARD);
|
||||
bench('neverthrow', () => { ntFromThrowable(thrower, mapErr)(); }, STANDARD);
|
||||
bench('fp-ts', () => { E.tryCatch(thrower, mapErr); }, STANDARD);
|
||||
bench('true-myth', () => { TmResult.tryOr('error', thrower); }, STANDARD);
|
||||
});
|
||||
208
benchmarks/result/instance-methods.bench.ts
Normal file
208
benchmarks/result/instance-methods.bench.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
ntOk,
|
||||
ntErr,
|
||||
TsOk,
|
||||
TsErr,
|
||||
OxOk,
|
||||
OxErr,
|
||||
oxMatch,
|
||||
E,
|
||||
pipe,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
const double = (x: number) => x * 2;
|
||||
const toUpper = (e: string) => e.toUpperCase();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// map — success path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.map — success path', () => {
|
||||
const nm = ok(42);
|
||||
const nt = ntOk(42);
|
||||
const ts = TsOk(42);
|
||||
const ox = OxOk(42);
|
||||
const fp = E.right(42);
|
||||
const tm = TmResult.ok<number, string>(42);
|
||||
|
||||
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.map(double); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.map(double); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.map(double); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, E.map(double)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.map(double); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// map — failure path (no-op)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.map — failure path (no-op)', () => {
|
||||
const nm = err('fail');
|
||||
const nt = ntErr('fail');
|
||||
const ts = TsErr('fail');
|
||||
const ox = OxErr('fail');
|
||||
const fp = E.left('fail');
|
||||
const tm = TmResult.err<number, string>('fail');
|
||||
|
||||
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.map(double); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.map(double); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.map(double); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, E.map(double)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.map(double); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// flatMap — success path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.flatMap — success path', () => {
|
||||
const nm = ok(42);
|
||||
const nt = ntOk(42);
|
||||
const ts = TsOk<number, string>(42);
|
||||
const ox = OxOk(42);
|
||||
const fp = E.right(42);
|
||||
const tm = TmResult.ok<number, string>(42);
|
||||
|
||||
bench('native-monad', () => { nm.flatMap(x => ok(x * 2)); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.andThen(x => ntOk(x * 2)); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.andThen(x => TsOk(x * 2)); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.andThen(x => OxOk(x * 2)); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, E.flatMap(x => E.right(x * 2))); }, STANDARD);
|
||||
bench('true-myth', () => { tm.andThen(x => TmResult.ok(x * 2)); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// flatMap — failure path (no-op)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.flatMap — failure path (no-op)', () => {
|
||||
const nm = err('fail');
|
||||
const nt = ntErr('fail');
|
||||
const ts = TsErr<number, string>('fail');
|
||||
const ox = OxErr('fail');
|
||||
const fp = E.left('fail');
|
||||
const tm = TmResult.err<number, string>('fail');
|
||||
|
||||
bench('native-monad', () => { nm.flatMap(x => ok(x * 2)); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.andThen(x => ntOk(x * 2)); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.andThen(x => TsOk(x * 2)); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.andThen(x => OxOk(x * 2)); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, E.flatMap(x => E.right(x * 2))); }, STANDARD);
|
||||
bench('true-myth', () => { tm.andThen(x => TmResult.ok(x * 2)); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mapErr — error path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.mapErr — error path', () => {
|
||||
const nm = err('fail');
|
||||
const nt = ntErr('fail');
|
||||
const ts = TsErr<number, string>('fail');
|
||||
const ox = OxErr('fail');
|
||||
const fp = E.left('fail');
|
||||
const tm = TmResult.err<number, string>('fail');
|
||||
|
||||
bench('native-monad', () => { nm.mapErr(toUpper); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.mapErr(toUpper); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.mapErr(toUpper); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.mapErr(toUpper); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, E.mapLeft(toUpper)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.mapErr(toUpper); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mapErr — success path (no-op)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.mapErr — success path (no-op)', () => {
|
||||
const nm = ok(42);
|
||||
const nt = ntOk(42);
|
||||
const ts = TsOk<number, string>(42);
|
||||
const ox = OxOk(42);
|
||||
const fp = E.right(42);
|
||||
const tm = TmResult.ok<number, string>(42);
|
||||
|
||||
bench('native-monad', () => { nm.mapErr(toUpper); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.mapErr(toUpper); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.mapErr(toUpper); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.mapErr(toUpper); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, E.mapLeft(toUpper)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.mapErr(toUpper); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// match — success path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.match — success path', () => {
|
||||
const nm = ok(42);
|
||||
const nt = ntOk(42);
|
||||
const ox = OxOk(42);
|
||||
const fp = E.right(42);
|
||||
const tm = TmResult.ok<number, string>(42);
|
||||
|
||||
bench('native-monad', () => { nm.match(x => x * 2, () => 0); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.match(x => x * 2, () => 0); }, STANDARD);
|
||||
// ts-results-es has no match — skip
|
||||
bench('oxide.ts', () => { oxMatch(ox, { Ok: (x: number) => x * 2, Err: () => 0 }); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, E.match(() => 0, x => x * 2)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.match({ Ok: x => x * 2, Err: () => 0 }); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// match — failure path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.match — failure path', () => {
|
||||
const nm = err('fail');
|
||||
const nt = ntErr('fail');
|
||||
const ox = OxErr('fail');
|
||||
const fp = E.left('fail');
|
||||
const tm = TmResult.err<number, string>('fail');
|
||||
|
||||
bench('native-monad', () => { nm.match(() => 0, e => e.length); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.match(() => 0, e => e.length); }, STANDARD);
|
||||
bench('oxide.ts', () => { oxMatch(ox, { Ok: () => 0, Err: (e: string) => e.length }); }, STANDARD);
|
||||
bench('fp-ts', () => { pipe(fp, E.match(e => e.length, () => 0)); }, STANDARD);
|
||||
bench('true-myth', () => { tm.match({ Ok: () => 0, Err: e => e.length }); }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// filter — success path (predicate passes)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.filter — predicate passes', () => {
|
||||
const nm = ok(42);
|
||||
const fp = E.right(42);
|
||||
|
||||
bench('native-monad', () => { nm.filter(x => x > 0, () => 'negative'); }, STANDARD);
|
||||
// neverthrow: no filter — skip
|
||||
// ts-results-es: no filter — skip
|
||||
// oxide.ts: filter returns Option, not Result — different semantics, skip
|
||||
bench('fp-ts', () => {
|
||||
pipe(fp, E.filterOrElse(x => x > 0, () => 'negative' as const));
|
||||
}, STANDARD);
|
||||
// true-myth: no filter — skip
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// filter — predicate fails
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.filter — predicate fails', () => {
|
||||
const nm = ok(-1);
|
||||
const fp = E.right(-1);
|
||||
|
||||
bench('native-monad', () => { nm.filter(x => x > 0, () => 'negative'); }, STANDARD);
|
||||
bench('fp-ts', () => {
|
||||
pipe(fp, E.filterOrElse(x => x > 0, () => 'negative' as const));
|
||||
}, STANDARD);
|
||||
});
|
||||
74
benchmarks/result/type-guards.bench.ts
Normal file
74
benchmarks/result/type-guards.bench.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
ntOk,
|
||||
ntErr,
|
||||
TsOk,
|
||||
TsErr,
|
||||
OxOk,
|
||||
OxErr,
|
||||
E,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isOk — on Ok variant
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.isOk — on Ok', () => {
|
||||
const nm = ok(42);
|
||||
const nt = ntOk(42);
|
||||
const ts = TsOk(42);
|
||||
const ox = OxOk(42);
|
||||
const fp = E.right(42);
|
||||
const tm = TmResult.ok<number, string>(42);
|
||||
|
||||
bench('native-monad', () => { nm.isOk(); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.isOk(); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.isOk(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.isOk(); }, STANDARD);
|
||||
bench('fp-ts', () => { E.isRight(fp); }, STANDARD);
|
||||
bench('true-myth', () => { tm.isOk; }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isOk — on Err variant
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.isOk — on Err', () => {
|
||||
const nm = err('fail');
|
||||
const nt = ntErr('fail');
|
||||
const ts = TsErr('fail');
|
||||
const ox = OxErr('fail');
|
||||
const fp = E.left('fail');
|
||||
const tm = TmResult.err<number, string>('fail');
|
||||
|
||||
bench('native-monad', () => { nm.isOk(); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.isOk(); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.isOk(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.isOk(); }, STANDARD);
|
||||
bench('fp-ts', () => { E.isRight(fp); }, STANDARD);
|
||||
bench('true-myth', () => { tm.isOk; }, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isErr — on Err variant
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Result.isErr — on Err', () => {
|
||||
const nm = err('fail');
|
||||
const nt = ntErr('fail');
|
||||
const ts = TsErr('fail');
|
||||
const ox = OxErr('fail');
|
||||
const fp = E.left('fail');
|
||||
const tm = TmResult.err<number, string>('fail');
|
||||
|
||||
bench('native-monad', () => { nm.isErr(); }, STANDARD);
|
||||
bench('neverthrow', () => { nt.isErr(); }, STANDARD);
|
||||
bench('ts-results-es', () => { ts.isErr(); }, STANDARD);
|
||||
bench('oxide.ts', () => { ox.isErr(); }, STANDARD);
|
||||
bench('fp-ts', () => { E.isLeft(fp); }, STANDARD);
|
||||
bench('true-myth', () => { tm.isErr; }, STANDARD);
|
||||
});
|
||||
145
benchmarks/scenarios/error-propagation.bench.ts
Normal file
145
benchmarks/scenarios/error-propagation.bench.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
none,
|
||||
ntOk,
|
||||
ntErr,
|
||||
TsOk,
|
||||
TsErr,
|
||||
TsNone,
|
||||
OxOk,
|
||||
OxErr,
|
||||
OxNone,
|
||||
E,
|
||||
O,
|
||||
pipe,
|
||||
TmResult,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
const fn = (x: number) => x + 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error propagation through 20 map calls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Scenario: Err through 20 map calls', () => {
|
||||
const nm = err('fail');
|
||||
const nt = ntErr('fail');
|
||||
const ts = TsErr<number, string>('fail');
|
||||
const ox = OxErr('fail');
|
||||
const fp = E.left('fail') as E.Either<string, number>;
|
||||
const tm = TmResult.err<number, string>('fail');
|
||||
|
||||
bench('native-monad', () => {
|
||||
let r = nm as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
let r = nt as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
let r = ts as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
let r = ox as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
let r = fp;
|
||||
for (let i = 0; i < 20; i++) r = pipe(r, E.map(fn));
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
let r = tm as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error propagation through 20 flatMap calls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Scenario: Err through 20 flatMap calls', () => {
|
||||
const nm = err('fail');
|
||||
const nt = ntErr('fail');
|
||||
const ts = TsErr<number, string>('fail');
|
||||
const ox = OxErr('fail');
|
||||
const fp = E.left('fail') as E.Either<string, number>;
|
||||
const tm = TmResult.err<number, string>('fail');
|
||||
|
||||
bench('native-monad', () => {
|
||||
let r = nm as any;
|
||||
for (let i = 0; i < 20; i++) r = r.flatMap((x: number) => ok(x + 1));
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
let r = nt as any;
|
||||
for (let i = 0; i < 20; i++) r = r.andThen((x: number) => ntOk(x + 1));
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
let r = ts as any;
|
||||
for (let i = 0; i < 20; i++) r = r.andThen((x: number) => TsOk(x + 1));
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
let r = ox as any;
|
||||
for (let i = 0; i < 20; i++) r = r.andThen((x: number) => OxOk(x + 1));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
let r = fp;
|
||||
for (let i = 0; i < 20; i++) r = pipe(r, E.flatMap(x => E.right(x + 1)));
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
let r = tm as any;
|
||||
for (let i = 0; i < 20; i++) r = r.andThen((x: number) => TmResult.ok(x + 1));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// None propagation through 20 map calls (Option)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Scenario: None through 20 map calls', () => {
|
||||
const nm = none();
|
||||
const ts = TsNone;
|
||||
const ox = OxNone;
|
||||
const fp = O.none as O.Option<number>;
|
||||
const tm = TmMaybe.nothing<number>();
|
||||
|
||||
bench('native-monad', () => {
|
||||
let r = nm as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
let r = ts as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
let r = ox as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
let r = fp;
|
||||
for (let i = 0; i < 20; i++) r = pipe(r, O.map(fn));
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
let r = tm as any;
|
||||
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||
}, STANDARD);
|
||||
});
|
||||
171
benchmarks/scenarios/memory.bench.ts
Normal file
171
benchmarks/scenarios/memory.bench.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { QUICK } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
some,
|
||||
none,
|
||||
okSome,
|
||||
okNone,
|
||||
errNone,
|
||||
ntOk,
|
||||
ntErr,
|
||||
TsOk,
|
||||
TsErr,
|
||||
TsSome,
|
||||
TsNone,
|
||||
OxOk,
|
||||
OxErr,
|
||||
OxSome,
|
||||
OxNone,
|
||||
E,
|
||||
O,
|
||||
TmResult,
|
||||
TmMaybe,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Singleton reuse: None / OkNone
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Memory: 1000x None creation (singleton vs constructor)', () => {
|
||||
bench('native-monad none() — frozen singleton', () => {
|
||||
for (let i = 0; i < 1000; i++) none();
|
||||
}, QUICK);
|
||||
|
||||
bench('ts-results-es None — constant', () => {
|
||||
for (let i = 0; i < 1000; i++) { const _ = TsNone; }
|
||||
}, QUICK);
|
||||
|
||||
bench('oxide.ts None — constant', () => {
|
||||
for (let i = 0; i < 1000; i++) { const _ = OxNone; }
|
||||
}, QUICK);
|
||||
|
||||
bench('fp-ts O.none — constant', () => {
|
||||
for (let i = 0; i < 1000; i++) { const _ = O.none; }
|
||||
}, QUICK);
|
||||
|
||||
bench('true-myth Maybe.nothing() — constructor', () => {
|
||||
for (let i = 0; i < 1000; i++) TmMaybe.nothing();
|
||||
}, QUICK);
|
||||
});
|
||||
|
||||
describe('Memory: 1000x OkNone creation', () => {
|
||||
bench('native-monad okNone() — frozen singleton', () => {
|
||||
for (let i = 0; i < 1000; i++) okNone();
|
||||
}, QUICK);
|
||||
|
||||
bench('fp-ts E.right(O.none) — two allocations', () => {
|
||||
for (let i = 0; i < 1000; i++) E.right(O.none);
|
||||
}, QUICK);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor allocation: Ok / Some / OkSome
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Memory: 1000x Ok construction', () => {
|
||||
bench('native-monad ok(i) — class instance', () => {
|
||||
for (let i = 0; i < 1000; i++) ok(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('neverthrow ok(i) — class instance', () => {
|
||||
for (let i = 0; i < 1000; i++) ntOk(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('ts-results-es Ok(i) — class instance', () => {
|
||||
for (let i = 0; i < 1000; i++) TsOk(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('oxide.ts Ok(i) — tagged object', () => {
|
||||
for (let i = 0; i < 1000; i++) OxOk(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('fp-ts E.right(i) — tagged object', () => {
|
||||
for (let i = 0; i < 1000; i++) E.right(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('true-myth Result.ok(i) — class instance', () => {
|
||||
for (let i = 0; i < 1000; i++) TmResult.ok(i);
|
||||
}, QUICK);
|
||||
});
|
||||
|
||||
describe('Memory: 1000x Some construction', () => {
|
||||
bench('native-monad some(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) some(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('ts-results-es Some(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) TsSome(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('oxide.ts Some(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) OxSome(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('fp-ts O.some(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) O.some(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('true-myth Maybe.just(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) TmMaybe.just(i);
|
||||
}, QUICK);
|
||||
});
|
||||
|
||||
describe('Memory: 1000x Err construction', () => {
|
||||
bench('native-monad err(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) err(`err-${i}`);
|
||||
}, QUICK);
|
||||
|
||||
bench('neverthrow err(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) ntErr(`err-${i}`);
|
||||
}, QUICK);
|
||||
|
||||
bench('ts-results-es Err(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) TsErr(`err-${i}`);
|
||||
}, QUICK);
|
||||
|
||||
bench('oxide.ts Err(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) OxErr(`err-${i}`);
|
||||
}, QUICK);
|
||||
|
||||
bench('fp-ts E.left(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) E.left(`err-${i}`);
|
||||
}, QUICK);
|
||||
|
||||
bench('true-myth Result.err(i)', () => {
|
||||
for (let i = 0; i < 1000; i++) TmResult.err(`err-${i}`);
|
||||
}, QUICK);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Three-state: OkSome vs nested allocation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Memory: 1000x OkSome (single vs nested allocation)', () => {
|
||||
bench('native-monad okSome(i) — single class', () => {
|
||||
for (let i = 0; i < 1000; i++) okSome(i);
|
||||
}, QUICK);
|
||||
|
||||
bench('fp-ts E.right(O.some(i)) — two objects', () => {
|
||||
for (let i = 0; i < 1000; i++) E.right(O.some(i));
|
||||
}, QUICK);
|
||||
|
||||
bench('ts-results-es Ok(Some(i)) — two classes', () => {
|
||||
for (let i = 0; i < 1000; i++) TsOk(TsSome(i));
|
||||
}, QUICK);
|
||||
});
|
||||
|
||||
describe('Memory: 1000x ErrNone (single vs nested allocation)', () => {
|
||||
bench('native-monad errNone(i) — single class', () => {
|
||||
for (let i = 0; i < 1000; i++) errNone(`err-${i}`);
|
||||
}, QUICK);
|
||||
|
||||
bench('fp-ts E.left(i) — single object', () => {
|
||||
for (let i = 0; i < 1000; i++) E.left(`err-${i}`);
|
||||
}, QUICK);
|
||||
|
||||
bench('ts-results-es Err(i) — single class', () => {
|
||||
for (let i = 0; i < 1000; i++) TsErr(`err-${i}`);
|
||||
}, QUICK);
|
||||
});
|
||||
127
benchmarks/scenarios/parsing.bench.ts
Normal file
127
benchmarks/scenarios/parsing.bench.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
Result,
|
||||
ntOk,
|
||||
ntErr,
|
||||
ntFromThrowable,
|
||||
TsOk,
|
||||
TsErr,
|
||||
OxOk,
|
||||
OxErr,
|
||||
E,
|
||||
pipe,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
const validJson = '{"name":"Alice","age":30,"email":"alice@example.com"}';
|
||||
const invalidJson = 'not json {{{';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON parsing — valid input
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Scenario: JSON parse + extract field — valid input', () => {
|
||||
bench('native-monad', () => {
|
||||
Result.from(() => JSON.parse(validJson), () => 'parse error')
|
||||
.map(obj => obj.name as string)
|
||||
.flatMap(name => (name.length > 0 ? ok(name) : err('empty name')));
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
ntFromThrowable(() => JSON.parse(validJson), () => 'parse error')()
|
||||
.map(obj => obj.name as string)
|
||||
.andThen(name => (name.length > 0 ? ntOk(name) : ntErr('empty name')));
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
let r;
|
||||
try { r = TsOk(JSON.parse(validJson)); }
|
||||
catch { r = TsErr('parse error'); }
|
||||
r.map((obj: any) => obj.name as string)
|
||||
.andThen((name: string) => (name.length > 0 ? TsOk(name) : TsErr('empty name')));
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
let r;
|
||||
try { r = OxOk(JSON.parse(validJson)); }
|
||||
catch { r = OxErr('parse error'); }
|
||||
r.map((obj: any) => obj.name as string)
|
||||
.andThen((name: string) => (name.length > 0 ? OxOk(name) : OxErr('parse error')));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(
|
||||
E.tryCatch(() => JSON.parse(validJson), () => 'parse error'),
|
||||
E.map(obj => obj.name as string),
|
||||
E.flatMap(name => (name.length > 0 ? E.right(name) : E.left('empty name'))),
|
||||
);
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
TmResult.tryOr('parse error', () => JSON.parse(validJson))
|
||||
.map(obj => obj.name as string)
|
||||
.andThen(name => (name.length > 0 ? TmResult.ok(name) : TmResult.err('empty name')));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON parsing — invalid input
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Scenario: JSON parse + extract field — invalid input', () => {
|
||||
bench('native-monad', () => {
|
||||
Result.from(() => JSON.parse(invalidJson), () => 'parse error')
|
||||
.map(obj => obj.name as string)
|
||||
.flatMap(name => (name.length > 0 ? ok(name) : err('empty name')));
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
ntFromThrowable(() => JSON.parse(invalidJson), () => 'parse error')()
|
||||
.map(obj => obj.name as string)
|
||||
.andThen(name => (name.length > 0 ? ntOk(name) : ntErr('empty name')));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(
|
||||
E.tryCatch(() => JSON.parse(invalidJson), () => 'parse error'),
|
||||
E.map(obj => obj.name as string),
|
||||
E.flatMap(name => (name.length > 0 ? E.right(name) : E.left('empty name'))),
|
||||
);
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
TmResult.tryOr('parse error', () => JSON.parse(invalidJson))
|
||||
.map(obj => obj.name as string)
|
||||
.andThen(name => (name.length > 0 ? TmResult.ok(name) : TmResult.err('empty name')));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Number parsing — valid
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Scenario: number parsing — valid', () => {
|
||||
const input = '42.5';
|
||||
|
||||
bench('native-monad', () => {
|
||||
Result.from(() => {
|
||||
const n = Number(input);
|
||||
if (Number.isNaN(n)) throw new Error('NaN');
|
||||
return n;
|
||||
}).flatMap(n => (n > 0 ? ok(n) : err('not positive')));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(
|
||||
E.tryCatch(() => {
|
||||
const n = Number(input);
|
||||
if (Number.isNaN(n)) throw new Error('NaN');
|
||||
return n;
|
||||
}, () => 'parse error'),
|
||||
E.flatMap(n => (n > 0 ? E.right(n) : E.left('not positive'))),
|
||||
);
|
||||
}, STANDARD);
|
||||
});
|
||||
148
benchmarks/scenarios/validation-pipeline.bench.ts
Normal file
148
benchmarks/scenarios/validation-pipeline.bench.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { STANDARD } from '../helpers/bench-options.js';
|
||||
import {
|
||||
ok,
|
||||
err,
|
||||
ntOk,
|
||||
ntErr,
|
||||
TsOk,
|
||||
TsErr,
|
||||
OxOk,
|
||||
OxErr,
|
||||
E,
|
||||
pipe,
|
||||
TmResult,
|
||||
} from '../helpers/imports.js';
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
email: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
const validUser: User = { name: 'Alice', email: 'alice@example.com', age: 30 };
|
||||
const invalidUser: User = { name: '', email: 'alice@example.com', age: 30 };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5-check validation — all pass
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Scenario: 5-check validation pipeline — all pass', () => {
|
||||
bench('native-monad', () => {
|
||||
ok<User, string>(validUser)
|
||||
.flatMap(u => (u.name ? ok(u) : err('name required')))
|
||||
.flatMap(u => (u.email.includes('@') ? ok(u) : err('invalid email')))
|
||||
.flatMap(u => (u.age >= 18 ? ok(u) : err('must be 18+')))
|
||||
.flatMap(u => (u.age <= 120 ? ok(u) : err('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
ntOk<User, string>(validUser)
|
||||
.andThen(u => (u.name ? ntOk(u) : ntErr('name required')))
|
||||
.andThen(u => (u.email.includes('@') ? ntOk(u) : ntErr('invalid email')))
|
||||
.andThen(u => (u.age >= 18 ? ntOk(u) : ntErr('must be 18+')))
|
||||
.andThen(u => (u.age <= 120 ? ntOk(u) : ntErr('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
TsOk<User, string>(validUser)
|
||||
.andThen(u => (u.name ? TsOk(u) : TsErr('name required')))
|
||||
.andThen(u => (u.email.includes('@') ? TsOk(u) : TsErr('invalid email')))
|
||||
.andThen(u => (u.age >= 18 ? TsOk(u) : TsErr('must be 18+')))
|
||||
.andThen(u => (u.age <= 120 ? TsOk(u) : TsErr('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
OxOk<User, string>(validUser)
|
||||
.andThen(u => (u.name ? OxOk(u) : OxErr('name required')))
|
||||
.andThen(u => (u.email.includes('@') ? OxOk(u) : OxErr('invalid email')))
|
||||
.andThen(u => (u.age >= 18 ? OxOk(u) : OxErr('must be 18+')))
|
||||
.andThen(u => (u.age <= 120 ? OxOk(u) : OxErr('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(
|
||||
E.right<string, User>(validUser),
|
||||
E.flatMap(u => (u.name ? E.right(u) : E.left('name required'))),
|
||||
E.flatMap(u => (u.email.includes('@') ? E.right(u) : E.left('invalid email'))),
|
||||
E.flatMap(u => (u.age >= 18 ? E.right(u) : E.left('must be 18+'))),
|
||||
E.flatMap(u => (u.age <= 120 ? E.right(u) : E.left('unrealistic age'))),
|
||||
E.map(u => ({ ...u, validated: true })),
|
||||
);
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
TmResult.ok<User, string>(validUser)
|
||||
.andThen(u => (u.name ? TmResult.ok(u) : TmResult.err('name required')))
|
||||
.andThen(u => (u.email.includes('@') ? TmResult.ok(u) : TmResult.err('invalid email')))
|
||||
.andThen(u => (u.age >= 18 ? TmResult.ok(u) : TmResult.err('must be 18+')))
|
||||
.andThen(u => (u.age <= 120 ? TmResult.ok(u) : TmResult.err('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5-check validation — fails at step 1 (early exit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Scenario: 5-check validation pipeline — fails at step 1', () => {
|
||||
bench('native-monad', () => {
|
||||
ok<User, string>(invalidUser)
|
||||
.flatMap(u => (u.name ? ok(u) : err('name required')))
|
||||
.flatMap(u => (u.email.includes('@') ? ok(u) : err('invalid email')))
|
||||
.flatMap(u => (u.age >= 18 ? ok(u) : err('must be 18+')))
|
||||
.flatMap(u => (u.age <= 120 ? ok(u) : err('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
|
||||
bench('neverthrow', () => {
|
||||
ntOk<User, string>(invalidUser)
|
||||
.andThen(u => (u.name ? ntOk(u) : ntErr('name required')))
|
||||
.andThen(u => (u.email.includes('@') ? ntOk(u) : ntErr('invalid email')))
|
||||
.andThen(u => (u.age >= 18 ? ntOk(u) : ntErr('must be 18+')))
|
||||
.andThen(u => (u.age <= 120 ? ntOk(u) : ntErr('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
|
||||
bench('ts-results-es', () => {
|
||||
TsOk<User, string>(invalidUser)
|
||||
.andThen(u => (u.name ? TsOk(u) : TsErr('name required')))
|
||||
.andThen(u => (u.email.includes('@') ? TsOk(u) : TsErr('invalid email')))
|
||||
.andThen(u => (u.age >= 18 ? TsOk(u) : TsErr('must be 18+')))
|
||||
.andThen(u => (u.age <= 120 ? TsOk(u) : TsErr('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
|
||||
bench('oxide.ts', () => {
|
||||
OxOk<User, string>(invalidUser)
|
||||
.andThen(u => (u.name ? OxOk(u) : OxErr('name required')))
|
||||
.andThen(u => (u.email.includes('@') ? OxOk(u) : OxErr('invalid email')))
|
||||
.andThen(u => (u.age >= 18 ? OxOk(u) : OxErr('must be 18+')))
|
||||
.andThen(u => (u.age <= 120 ? OxOk(u) : OxErr('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
|
||||
bench('fp-ts', () => {
|
||||
pipe(
|
||||
E.right<string, User>(invalidUser),
|
||||
E.flatMap(u => (u.name ? E.right(u) : E.left('name required'))),
|
||||
E.flatMap(u => (u.email.includes('@') ? E.right(u) : E.left('invalid email'))),
|
||||
E.flatMap(u => (u.age >= 18 ? E.right(u) : E.left('must be 18+'))),
|
||||
E.flatMap(u => (u.age <= 120 ? E.right(u) : E.left('unrealistic age'))),
|
||||
E.map(u => ({ ...u, validated: true })),
|
||||
);
|
||||
}, STANDARD);
|
||||
|
||||
bench('true-myth', () => {
|
||||
TmResult.ok<User, string>(invalidUser)
|
||||
.andThen(u => (u.name ? TmResult.ok(u) : TmResult.err('name required')))
|
||||
.andThen(u => (u.email.includes('@') ? TmResult.ok(u) : TmResult.err('invalid email')))
|
||||
.andThen(u => (u.age >= 18 ? TmResult.ok(u) : TmResult.err('must be 18+')))
|
||||
.andThen(u => (u.age <= 120 ? TmResult.ok(u) : TmResult.err('unrealistic age')))
|
||||
.map(u => ({ ...u, validated: true }));
|
||||
}, STANDARD);
|
||||
});
|
||||
14
benchmarks/tsconfig.json
Normal file
14
benchmarks/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["es2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["**/*.bench.ts", "helpers/**/*.ts"]
|
||||
}
|
||||
9
benchmarks/vitest.config.ts
Normal file
9
benchmarks/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
benchmark: {
|
||||
include: ['**/*.bench.ts'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user