Skip to content

Commit

Permalink
feat(memoize): add async versions of all memoize functions (#493)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: remove obsolete arity overrides (i.e. 2/3/4 suffixed versions)

- add memoizeAsync()
- add memoizeAsync1()
- add memoizeAsyncJ()
- add memoizeAsyncO()
- refactor memoize fns to be variadic
- remove obsolete fixed arity versions (e.g. `memoize2O`, 3O, 4O etc.)
- update tests
  • Loading branch information
postspectacular committed Oct 31, 2024
1 parent 603c76c commit e31543c
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 95 deletions.
40 changes: 25 additions & 15 deletions packages/memoize/src/memoize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Fn, Fn2, Fn3, Fn4, FnAny } from "@thi.ng/api";
import type { FnAny } from "@thi.ng/api";
import type { MapLike } from "./api.js";

/**
Expand All @@ -18,24 +18,34 @@ import type { MapLike } from "./api.js";
* @param fn -
* @param cache -
*/
export function memoize<A, B>(fn: Fn<A, B>, cache: MapLike<A, B>): Fn<A, B>;
export function memoize<A, B, C>(
fn: Fn2<A, B, C>,
cache: MapLike<[A, B], C>
): Fn2<A, B, C>;
export function memoize<A, B, C, D>(
fn: Fn3<A, B, C, D>,
cache: MapLike<[A, B, C], D>
): Fn3<A, B, C, D>;
export function memoize<A, B, C, D, E>(
fn: Fn4<A, B, C, D, E>,
cache: MapLike<[A, B, C, D], E>
): Fn4<A, B, C, D, E>;
export function memoize(fn: FnAny<any>, cache: MapLike<any, any>): FnAny<any> {
export function memoize<T extends FnAny<any>>(
fn: T,
cache: MapLike<any, any>
): T {
// @ts-ignore
return (...args: any[]) => {
let res;
return cache.has(args)
? cache.get(args)
: (cache.set(args, (res = fn.apply(null, args))), res);
};
}

/**
* Async version of {@link memoize}.
*
* @param fn
* @param cache
*/
export function memoizeAsync<T extends FnAny<any>>(
fn: T,
cache: MapLike<any, any>
): (...xs: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
// @ts-ignore
return async (...args: any[]) => {
let res;
return cache.has(args)
? cache.get(args)
: (cache.set(args, (res = await fn.apply(null, args))), res);
};
}
17 changes: 16 additions & 1 deletion packages/memoize/src/memoize1.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Fn } from "@thi.ng/api";
import type { Fn, MaybePromise } from "@thi.ng/api";
import type { MapLike } from "./api.js";

/**
Expand All @@ -24,3 +24,18 @@ export const memoize1 =
? cache.get(x)!
: (cache.set(x, (res = fn(x))), res);
};

/**
* Async version of {@link memoize1}.
*
* @param fn
* @param cache
*/
export const memoizeAsync1 =
<A, B>(fn: Fn<A, MaybePromise<B>>, cache: MapLike<A, B> = new Map()) =>
async (x: A): Promise<B> => {
let res;
return cache.has(x)
? cache.get(x)!
: (cache.set(x, (res = await fn(x))), res);
};
46 changes: 28 additions & 18 deletions packages/memoize/src/memoizej.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Fn, Fn2, Fn3, Fn4, FnAny, IObjectOf } from "@thi.ng/api";
import type { FnAny, IObjectOf } from "@thi.ng/api";

/**
* Function memoization for arbitrary argument counts. Returns augmented
Expand All @@ -15,23 +15,11 @@ import type { Fn, Fn2, Fn3, Fn4, FnAny, IObjectOf } from "@thi.ng/api";
* @param fn -
* @param cache -
*/
export function memoizeJ<A, B>(fn: Fn<A, B>, cache?: IObjectOf<B>): Fn<A, B>;
export function memoizeJ<A, B, C>(
fn: Fn2<A, B, C>,
cache?: IObjectOf<C>
): Fn2<A, B, C>;
export function memoizeJ<A, B, C, D>(
fn: Fn3<A, B, C, D>,
cache?: IObjectOf<D>
): Fn3<A, B, C, D>;
export function memoizeJ<A, B, C, D, E>(
fn: Fn4<A, B, C, D, E>,
cache?: IObjectOf<E>
): Fn4<A, B, C, D, E>;
export function memoizeJ(
fn: FnAny<any>,
cache: Record<string, any> = Object.create(null)
): FnAny<any> {
export function memoizeJ<T extends FnAny<any>>(
fn: T,
cache: IObjectOf<any> = Object.create(null)
): T {
// @ts-ignore
return (...args: any[]) => {
const key = JSON.stringify(args);
if (key !== undefined) {
Expand All @@ -42,3 +30,25 @@ export function memoizeJ(
return fn.apply(null, args);
};
}

/**
* Async version of {@link memoizeJ}.
*
* @param fn
* @param cache
*/
export function memoizeAsyncJ<T extends FnAny<any>>(
fn: T,
cache: IObjectOf<any> = Object.create(null)
): (...xs: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
// @ts-ignore
return async (...args: any[]) => {
const key = JSON.stringify(args);
if (key !== undefined) {
return key in cache
? cache[key]
: (cache[key] = await fn.apply(null, args));
}
return await fn.apply(null, args);
};
}
144 changes: 87 additions & 57 deletions packages/memoize/src/memoizeo.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import type { Fn, Fn2, Fn3, Fn4, NumOrString } from "@thi.ng/api";
import type {
Fn,
Fn2,
Fn3,
Fn4,
IObjectOf,
MaybePromise,
NumOrString,
} from "@thi.ng/api";

/**
* The most minimalistic & fastest memoization function of this package. Similar
* to {@link memoize1}, but only supports numbers or strings as keys and uses a
* vanilla JS object as cache.
* The most minimalistic memoization function of this package, but only supports
* numbers or strings as arguments (max. 4) and uses a vanilla JS object as
* cache.
*
* @remarks
* Also see {@link memoize1}, {@link memoizeJ}, {@link memoize}.
* If `fn` throws an error, no result value will be cached and no memoization
* happens for this invocation using the given arguments.
*
* Use {@link memoizeAsyncO} for async functions or other functions returning
* promises.
*
* @example
* ```ts tangle:../export/memoizeo.ts
Expand All @@ -29,64 +41,82 @@ import type { Fn, Fn2, Fn3, Fn4, NumOrString } from "@thi.ng/api";
* @param fn
* @param cache
*/
export const memoizeO =
<A extends NumOrString, B>(
fn: Fn<A, B>,
cache: Record<NumOrString, B> = Object.create(null)
) =>
(x: A): B =>
x in cache ? cache[x] : (cache[x] = fn(x));

/**
* Like {@link memoizeO}, but for functions with 2 arguments.
*
* @param fn
* @param cache
*/
export const memoize2O =
<A extends NumOrString, B extends NumOrString, C>(
fn: Fn2<A, B, C>,
cache: Record<string, C> = Object.create(null)
) =>
(a: A, b: B): C => {
const key = a + "-" + b;
return key in cache ? cache[key] : (cache[key] = fn(a, b));
export function memoizeO<A extends NumOrString, B>(
fn: Fn<A, B>,
cache?: IObjectOf<B>
): Fn<A, B>;
export function memoizeO<A extends NumOrString, B extends NumOrString, C>(
fn: Fn2<A, B, C>,
cache?: IObjectOf<C>
): Fn2<A, B, C>;
export function memoizeO<
A extends NumOrString,
B extends NumOrString,
C extends NumOrString,
D
>(fn: Fn3<A, B, C, D>, cache?: IObjectOf<D>): Fn3<A, B, C, D>;
export function memoizeO<
A extends NumOrString,
B extends NumOrString,
C extends NumOrString,
D extends NumOrString,
E
>(fn: Fn4<A, B, C, D, E>, cache?: IObjectOf<E>): Fn4<A, B, C, D, E>;
export function memoizeO<T extends (...xs: NumOrString[]) => any>(
fn: T,
cache: IObjectOf<ReturnType<T>> = Object.create(null)
): T {
// @ts-ignore
return (...xs: any[]) => {
const key = xs.join("-");
return key in cache ? cache[key] : (cache[key] = fn(...xs));
};
}

/**
* Like {@link memoizeO}, but for functions with 3 arguments.
* Async version of {@link memoizeO}.
*
* @param fn
* @param cache
*/
export const memoize3O =
<A extends NumOrString, B extends NumOrString, C extends NumOrString, D>(
fn: Fn3<A, B, C, D>,
cache: Record<string, D> = Object.create(null)
) =>
(a: A, b: B, c: C): D => {
const key = a + "-" + b + "-" + c;
return key in cache ? cache[key] : (cache[key] = fn(a, b, c));
};

/**
* Like {@link memoizeO}, but for functions with 4 arguments.
* @remarks
* If `fn` throws an error, no result value will be cached and no memoization
* happens for this invocation using the given arguments.
*
* @param fn
* @param cache
*/
export const memoize4O =
<
A extends NumOrString,
B extends NumOrString,
C extends NumOrString,
D extends NumOrString,
E
>(
fn: Fn4<A, B, C, D, E>,
cache: Record<string, E> = Object.create(null)
) =>
(a: A, b: B, c: C, d: D): E => {
const key = a + "-" + b + "-" + c + "-" + d;
return key in cache ? cache[key] : (cache[key] = fn(a, b, c, d));
export function memoizeAsyncO<A extends NumOrString, B>(
fn: Fn<A, MaybePromise<B>>,
cache?: IObjectOf<B>
): Fn<A, Promise<B>>;
export function memoizeAsyncO<A extends NumOrString, B extends NumOrString, C>(
fn: Fn2<A, B, MaybePromise<C>>,
cache?: IObjectOf<C>
): Fn2<A, B, Promise<C>>;
export function memoizeAsyncO<
A extends NumOrString,
B extends NumOrString,
C extends NumOrString,
D
>(
fn: Fn3<A, B, C, MaybePromise<D>>,
cache?: IObjectOf<D>
): Fn3<A, B, C, Promise<D>>;
export function memoizeAsyncO<
A extends NumOrString,
B extends NumOrString,
C extends NumOrString,
D extends NumOrString,
E
>(
fn: Fn4<A, B, C, D, MaybePromise<E>>,
cache?: IObjectOf<E>
): Fn4<A, B, C, D, Promise<E>>;
export function memoizeAsyncO<T extends (...xs: NumOrString[]) => any>(
fn: T,
cache: IObjectOf<ReturnType<T>> = Object.create(null)
): T {
// @ts-ignore
return async (...xs: any[]) => {
const key = xs.join("-");
return key in cache ? cache[key] : (cache[key] = await fn(...xs));
};
}
15 changes: 11 additions & 4 deletions packages/memoize/test/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EquivMap } from "@thi.ng/associative";
import { LRUCache } from "@thi.ng/cache";
import { expect, test } from "bun:test";
import { memoize1, memoize2O, memoizeO } from "../src/index.js";
import { memoize1, memoizeO } from "../src/index.js";

test("memoize1", () => {
const calls: number[] = [];
Expand All @@ -16,7 +16,7 @@ test("memoize1", () => {

test("memoizeO", () => {
const calls: number[] = [];
const f = memoizeO<number, number>((x) => (calls.push(x), x * 10));
const f = memoizeO((x: number) => (calls.push(x), x * 10));
expect(f(1)).toBe(10);
expect(f(2)).toBe(20);
expect(f(2)).toBe(20);
Expand All @@ -27,8 +27,10 @@ test("memoizeO", () => {

test("memoize2O", () => {
const calls: number[][] = [];
const f = memoize2O<number, number, number>(
(a, b) => (calls.push([a, b]), a * b)
const cache: Record<string, number> = {};
const f = memoizeO(
(a: number, b: number) => (calls.push([a, b]), a * b),
cache
);
expect(f(1, 2)).toBe(2);
expect(f(1, 2)).toBe(2);
Expand All @@ -39,6 +41,11 @@ test("memoize2O", () => {
[2, 3],
[2, 1],
]);
expect(cache).toEqual({
"1-2": 2,
"2-1": 2,
"2-3": 6,
});
});

test("memoize1 (equivmap)", () => {
Expand Down

0 comments on commit e31543c

Please sign in to comment.