feat(memoize): Update behavior or memoize to get a custom getCacheKey and only accept unary or zero-argument function

This commit is contained in:
Sojin Park 2024-08-15 23:10:15 +09:00
parent 3cb5c804e1
commit b1ac921779
7 changed files with 264 additions and 202 deletions

View File

@ -120,6 +120,7 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'negate', link: '/zh_hans/reference/function/negate' },
{ text: 'once', link: '/zh_hans/reference/function/once' },
{ text: 'noop', link: '/zh_hans/reference/function/noop' },
{ text: 'memoize', link: '/zh_hans/reference/function/memoize' },
{ text: 'ary', link: '/zh_hans/reference/function/ary' },
{ text: 'unary', link: '/zh_hans/reference/function/unary' },
{ text: 'bind (兼容性)', link: '/zh_hans/reference/compat/function/bind' },

View File

@ -1,61 +1,66 @@
# memoize
주어진 함수의 결과를 인수에 기반하여 캐싱함으로써 메모이제이션해요.
연산 결과를 캐싱하는 새로운 메모이제이션된 함수를 반환해요. 메모이제이션된 함수는 같은 인자에 대해서 중복해서 연산하지 않고, 캐시된 결과를 반환해요.
인자를 0개 또는 1개만 받는 함수만 메모이제이션할 수 있어요. 2개 이상의 인자를 받는 함수를 메모이제이션하려면,
여러 인자를 1개의 객체나 배열로 받도록 리팩토링하세요.
인자가 배열이나 객체여서 원시 값이 아닌 경우, 올바르게 캐시 키를 계산할 수 있도록 `getCacheKey` 함수를 옵션으로 제공하세요.
## 인터페이스
```typescript
export function memoize<F extends (...args: any[]) => any, K = Parameters<F>[0]>(
function memoize<F extends (...args: any) => any>(
fn: F,
options: MemoizeOptions<K, ReturnType<F>> = {}
): F & { cache: Cache<K, ReturnType<F>> };
options: {
cache?: MemoizeCache<any, ReturnType<F>>;
getCacheKey?: (args: Parameters<F>[0]) => unknown;
} = {}
): F & { cache: MemoizeCache<any, ReturnType<F>> };
export interface MemoizeOptions<K, V> {
cache?: Cache<K, V>;
resolver?: (...args: any[]) => K;
}
export interface Cache<K, V> {
set: (key: K, value: V) => void;
get: (key: K) => V | undefined;
has: (key: K) => boolean;
delete: (key: K) => boolean | void;
clear: () => void;
interface MemoizeCache<K, V> {
set(key: K, value: V): void;
get(key: K): V | undefined;
has(key: K): boolean;
delete(key: K): boolean | void;
clear(): void;
size: number;
}
```
### 파라미터
- `fn (T)`: 메모이제이션할 함수예요.
- `options` (MemoizeOptions<K, ReturnType<F>>, optional): 캐시 키를 생성할 함수와 결과를 저장할 캐시 객체를 포함해요.
- `resolver ((...args: any[]) => K, optional)`: 캐시 키를 생성할 함수. 제공되지 않으면 메모이제이션된 함수의 첫 번째 인수를 키로 사용해요.
- `cache (Cache<K, ReturnType<F>>, optional)`: 결과를 저장할 캐시 객체. 기본값은 새로운 Map 인스턴스입니다.
- `fn` (`F`) - 메모이제이션할 함수. 0개 또는 1개 인자를 받아야 해요.
- `options`: 메모이제이션 옵션.
- `options.cache` (`MemoizeCache<any, ReturnType<F>>`): 연산 결과를 저장할 캐시 객체. 기본값은 새로운 `Map`이에요.
- `options.getCacheKey` (`(args: A) => unknown`): 원시 값이 아닌 인자에 대해서 캐시 키를 올바르게 계산할 수 있는 함수.
### 반환 값
`(F & { cache: Cache<K, ReturnType<F>> })`: 캐시 속성을 가진 메모이제이션된 함수.
(`F & { cache: MemoizeCache<any, ReturnType<F>> }`): 메모이제이션된 함수. 추가로 내부 캐시를 노출하는 `cache` 프로퍼티를 가져요.
## 예시
```typescript
import { memoize } from 'es-toolkit/function';
// 기본 캐시를 사용하는 예제
const add = (a: number, b: number) => a + b;
import { memoize, MemoizeCache } from 'es-toolkit/function';
// 기본 사용법
const add = (x: number) => x + 10;
const memoizedAdd = memoize(add);
console.log(memoizedAdd(1, 2)); // 3
console.log(memoizedAdd(1, 2)); // 3, 캐시된 값 반환
console.log(memoizedAdd(5)); // 15
console.log(memoizedAdd(5)); // 15 (캐시된 결과)
console.log(memoizedAdd.cache.size); // 1
// 커스텀 리졸버를 사용하는 예제
const resolver = (a: number, b: number) => `${a}-${b}`;
const memoizedAddWithResolver = memoize(add, { resolver });
console.log(memoizedAddWithResolver(1, 2)); // 3
console.log(memoizedAddWithResolver(1, 2)); // 3, 캐시된 값 반환
console.log(memoizedAddWithResolver.cache.size); // 1
// 커스텀 `getCacheKey` 정의하기
const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0);
const memoizedSum = memoize(sum, { getCacheKey: (arr: number[]) => arr.join(',') });
console.log(memoizedSum([1, 2])); // 3
console.log(memoizedSum([1, 2])); // 3 (캐시된 결과)
console.log(memoizedSum.cache.size); // 1
// 커스텀 캐시 구현을 사용하는 예제
class CustomCache<K, T> implements Cache<K, T> {
// 커스텀 `MemoizeCache` 정의하기
class CustomCache<K, T> implements MemoizeCache<K, T> {
private cache = new Map<K, T>();
set(key: K, value: T): void {
this.cache.set(key, value);
@ -77,33 +82,8 @@ class CustomCache<K, T> implements Cache<K, T> {
}
}
const customCache = new CustomCache<string, number>();
const memoizedAddWithCustomCache = memoize(add, { cache: customCache });
console.log(memoizedAddWithCustomCache(1, 2)); // 3
console.log(memoizedAddWithCustomCache(1, 2)); // 3, 캐시된 값 반환
const memoizedSumWithCustomCache = memoize(sum, { cache: customCache });
console.log(memoizedSumWithCustomCache([1, 2])); // 3
console.log(memoizedSumWithCustomCache([1, 2])); // 3 (캐시된 결과)
console.log(memoizedAddWithCustomCache.cache.size); // 1
// 커스텀 리졸버와 캐시를 사용하는 예제
const customResolver = (a: number, b: number) => `${a}-${b}`;
const memoizedAddWithBoth = memoize(add, { resolver: customResolver, cache: customCache });
console.log(memoizedAddWithBoth(1, 2)); // 3
console.log(memoizedAddWithBoth(1, 2)); // 3, 캐시된 값 반환
console.log(memoizedAddWithBoth.cache.size); // 1
// `this` 바인딩을 사용하는 예제
const obj = {
b: 2,
memoizedAdd: memoize(
function (a: number) {
return a + this.b;
},
{
resolver: function (a: number) {
return `${a}-${this.b}`;
},
}
),
};
console.log(obj.memoizedAdd(1)); // 3
obj.b = 3;
console.log(obj.memoizedAdd(1)); // 4
```

View File

@ -1,62 +1,70 @@
# memoize
Memoizes a given function by caching its result based on the arguments provided.
Creates a memoized version of the provided function. The memoized function caches
results based on the argument it receives, so if the same argument is passed again,
it returns the cached result instead of recomputing it.
This works with functions that take zero or one argument. If your function takes
multiple arguments, you should refactor it to accept a single object or array
that combines those arguments.
If the argument is not primitive (e.g., arrays or objects), provide a
`getCacheKey` function to generate a unique cache key for proper caching.
## Signature
```typescript
function memoize<F extends (...args: any[]) => any, K = Parameters<F>[0]>(
function memoize<F extends (...args: any) => any>(
fn: F,
options: MemoizeOptions<K, ReturnType<F>> = {}
): F & { cache: Cache<K, ReturnType<F>> };
options: {
cache?: MemoizeCache<any, ReturnType<F>>;
getCacheKey?: (args: Parameters<F>[0]) => unknown;
} = {}
): F & { cache: MemoizeCache<any, ReturnType<F>> };
interface MemoizeOptions<K, V> {
cache?: Cache<K, V>;
resolver?: (...args: any[]) => K;
}
interface Cache<K, V> {
set: (key: K, value: V) => void;
get: (key: K) => V | undefined;
has: (key: K) => boolean;
delete: (key: K) => boolean | void;
clear: () => void;
interface MemoizeCache<K, V> {
set(key: K, value: V): void;
get(key: K): V | undefined;
has(key: K): boolean;
delete(key: K): boolean | void;
clear(): void;
size: number;
}
```
### Parameters
- `fn (T)`: The function to be memoized.
- `options` (MemoizeOptions<K, ReturnType<F>>, optional): Includes a function to generate the cache key and an object to store the results.
- `resolver ((...args: any[]) => K, optional)`: A function to generate the cache key. If not provided, the first argument of the memoized function is used as the key.
- `cache (Cache<K, ReturnType<F>>, optional)`: The cache object to store the results. The default is a new Map instance.
- `fn` (`F`) - The function to be memoized, which takes zero or just one argument.
- `options`: Optional configuration for the memoization.
- `options.cache` (`MemoizeCache<any, ReturnType<F>>`): The cache object used to store results. Defaults to a new `Map`.
- `options.getCacheKey` (`(args: A) => unknown`): An optional function to generate a unique cache key for each argument.
### Returns
`(F & { cache: Cache<K, ReturnType<F>> })`: The memoized function with a cache property.
(`F & { cache: MemoizeCache<any, ReturnType<F>> }`): The memoized function with an additional `cache` property that exposes the internal cache.
## Examples
```typescript
import { memoize } from 'es-toolkit/function';
import { memoize, MemoizeCache } from 'es-toolkit/function';
// Example using the default cache
const add = (a: number, b: number) => a + b;
const add = (x: number) => x + 10;
const memoizedAdd = memoize(add);
console.log(memoizedAdd(1, 2)); // 3
console.log(memoizedAdd(1, 2)); // 3, returns cached value
console.log(memoizedAdd(5)); // 15
console.log(memoizedAdd(5)); // 15 (cached result)
console.log(memoizedAdd.cache.size); // 1
// Example using a custom resolver
const resolver = (a: number, b: number) => `${a}-${b}`;
const memoizedAddWithResolver = memoize(add, { resolver });
console.log(memoizedAddWithResolver(1, 2)); // 3
console.log(memoizedAddWithResolver(1, 2)); // 3, returns cached value
console.log(memoizedAddWithResolver.cache.size); // 1
const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0);
const memoizedSum = memoize(sum, { getCacheKey: (arr: number[]) => arr.join(',') });
console.log(memoizedSum([1, 2])); // 3
console.log(memoizedSum([1, 2])); // 3 (cached result)
console.log(memoizedSum.cache.size); // 1
// Example using a custom cache implementation
class CustomCache<K, T> implements Cache<K, T> {
class CustomCache<K, T> implements MemoizeCache<K, T> {
private cache = new Map<K, T>();
set(key: K, value: T): void {
this.cache.set(key, value);
@ -78,33 +86,8 @@ class CustomCache<K, T> implements Cache<K, T> {
}
}
const customCache = new CustomCache<string, number>();
const memoizedAddWithCustomCache = memoize(add, { cache: customCache });
console.log(memoizedAddWithCustomCache(1, 2)); // 3
console.log(memoizedAddWithCustomCache(1, 2)); // 3, returns cached value
const memoizedSumWithCustomCache = memoize(sum, { cache: customCache });
console.log(memoizedSumWithCustomCache([1, 2])); // 3
console.log(memoizedSumWithCustomCache([1, 2])); // 3 (cached result)
console.log(memoizedAddWithCustomCache.cache.size); // 1
// Example using both a custom resolver and cache
const customResolver = (a: number, b: number) => `${a}-${b}`;
const memoizedAddWithBoth = memoize(add, { resolver: customResolver, cache: customCache });
console.log(memoizedAddWithBoth(1, 2)); // 3
console.log(memoizedAddWithBoth(1, 2)); // 3, returns cached value
console.log(memoizedAddWithBoth.cache.size); // 1
// Example using `this` binding
const obj = {
b: 2,
memoizedAdd: memoize(
function (a: number) {
return a + this.b;
},
{
resolver: function (a: number) {
return `${a}-${this.b}`;
},
}
),
};
console.log(obj.memoizedAdd(1)); // 3
obj.b = 3;
console.log(obj.memoizedAdd(1)); // 4
```

View File

@ -0,0 +1,88 @@
# memoize
创建一个函数的备忘版本。备忘函数会基于接收到的参数缓存结果,因此如果再次传递相同的参数,它会返回缓存的结果,而不是重新计算。
这个功能适用于接受零个或一个参数的函数。如果你的函数接受多个参数,你应该将其重构为接受一个组合了这些参数的对象或数组。
如果参数不是原始类型(例如数组或对象),请提供一个 `getCacheKey` 函数来生成唯一的缓存键,以确保正确缓存。
## 签名
```typescript
function memoize<F extends (...args: any) => any>(
fn: F,
options: {
cache?: MemoizeCache<any, ReturnType<F>>;
getCacheKey?: (args: Parameters<F>[0]) => unknown;
} = {}
): F & { cache: MemoizeCache<any, ReturnType<F>> };
interface MemoizeCache<K, V> {
set(key: K, value: V): void;
get(key: K): V | undefined;
has(key: K): boolean;
delete(key: K): boolean | void;
clear(): void;
size: number;
}
```
### 参数
- `fn` (`F`) - 要备忘的函数,它接受零个或一个参数。
- `options`: 备忘配置的可选项。
- `options.cache` (`MemoizeCache<any, ReturnType<F>>`): 用于存储结果的缓存对象。默认为一个新的 `Map`
- `options.getCacheKey` (`(args: A) => unknown`): 可选函数,用于为每个参数生成唯一的缓存键。
### 返回
(`F & { cache: MemoizeCache<any, ReturnType<F>> }`): 备忘版本的函数,并带有一个额外的 cache 属性,用于暴露内部缓存。
## Examples
```typescript
import { memoize, MemoizeCache } from 'es-toolkit/function';
// 使用默认缓存的示例
const add = (x: number) => x + 10;
const memoizedAdd = memoize(add);
console.log(memoizedAdd(5)); // 15
console.log(memoizedAdd(5)); // 15 (缓存结果)
console.log(memoizedAdd.cache.size); // 1
// 使用自定义解析器的示例
const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0);
const memoizedSum = memoize(sum, { getCacheKey: (arr: number[]) => arr.join(',') });
console.log(memoizedSum([1, 2])); // 3
console.log(memoizedSum([1, 2])); // 3 (缓存结果)
console.log(memoizedSum.cache.size); // 1
// 使用自定义缓存实现的示例
class CustomCache<K, T> implements MemoizeCache<K, T> {
private cache = new Map<K, T>();
set(key: K, value: T): void {
this.cache.set(key, value);
}
get(key: K): T | undefined {
return this.cache.get(key);
}
has(key: K): boolean {
return this.cache.has(key);
}
delete(key: K): boolean {
return this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
get size(): number {
return this.cache.size;
}
}
const customCache = new CustomCache<string, number>();
const memoizedSumWithCustomCache = memoize(sum, { cache: customCache });
console.log(memoizedSumWithCustomCache([1, 2])); // 3
console.log(memoizedSumWithCustomCache([1, 2])); // 3 (缓存结果)
console.log(memoizedAddWithCustomCache.cache.size); // 1
```

View File

@ -5,7 +5,7 @@ export { noop } from './noop.ts';
export { once } from './once.ts';
export { throttle } from './throttle.ts';
export { negate } from './negate.ts';
export { memoize } from './memoize.ts';
export { memoize, MemoizeCache } from './memoize.ts';
export { ary } from './ary.ts';
export { unary } from './unary.ts';
export { partial } from './partial.ts';

View File

@ -1,23 +1,35 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { memoize } from './memoize';
describe('memoize', () => {
it('should memoize results based on the first argument', () => {
const fn = (a: number, b: number, c: number) => a + b + c;
const memoized = memoize(fn);
expect(memoized(1, 2, 3)).toBe(6); // {1: 6}
expect(memoized(1, 3, 5)).toBe(6); // {1: 6}
it('should memoize results of an unary function', () => {
const add10 = vi.fn((x: number) => x + 10);
const memoizedAdd10 = memoize(add10);
expect(memoizedAdd10(5)).toBe(15);
expect(memoizedAdd10(5)).toBe(15);
expect(add10).toBeCalledTimes(1);
const now = () => Date.now();
const memoizedNow = memoize(now);
expect(memoizedNow()).toBe(memoizedNow());
});
it('should memoize results using a custom resolver function', () => {
const fn = function (a: number, b: number, c: number) {
return a + b + c;
};
const resolver = (...args: number[]) => args.join('-');
const memoized = memoize(fn, { resolver });
const sum = vi.fn(function sum(arr: number[]) {
return arr.reduce((x, y) => x + y, 0);
});
expect(memoized(1, 2, 3)).toBe(6); // {1-2-3: 6}
expect(memoized(1, 3, 5)).toBe(9); // {1-2-3: 6, 1-3-5: 6}
const memoizedSum = memoize(sum, {
getCacheKey: x => x.join(','),
});
expect(memoizedSum([1, 2, 3])).toBe(6);
expect(memoizedSum([1, 2, 3])).toBe(6);
expect(sum).toBeCalledTimes(1);
});
it('should use `this` context for resolver function', () => {
@ -49,13 +61,6 @@ describe('memoize', () => {
expect(actual).toEqual(props);
});
it('should throw TypeError if resolver is not a function', () => {
expect(() => {
// @ts-expect-error: resolver is not a function
memoize(() => {}, { resolver: true });
}).toThrowError(TypeError);
});
it('should allow custom cache implementation', () => {
class CustomCache {
private __data__: Map<object, string> = new Map();

View File

@ -1,98 +1,103 @@
/**
* Memoizes a given function by caching its result based on the arguments provided.
* Creates a memoized version of the provided function. The memoized function caches
* results based on the argument it receives, so if the same argument is passed again,
* it returns the cached result instead of recomputing it.
*
* @template F - The type of the function to memoize.
* @template K - The type of the cache key.
* @param func
* @param {F} fn - The function to memoize.
* @param {MemoizeOptions<K, ReturnType<F>>} [options] - An options object with a resolver function and/or a custom cache object.
* @returns {F & { cache: Cache<K, ReturnType<F>> }} - The memoized function with a cache property.
* This function works with functions that take zero or just one argument. If your function
* originally takes multiple arguments, you should refactor it to take a single object or array
* that combines those arguments.
*
* @throws {TypeError} If the provided function or resolver is not valid.
* If the argument is not primitive (e.g., arrays or objects), provide a
* `getCacheKey` function to generate a unique cache key for proper caching.
*
* @param {F} fn - The function to be memoized. It should accept a single argument and return a value.
* @param {MemoizeOptions<Parameters<F>[0], ReturnType<F>>} [options={}] - Optional configuration for the memoization.
* @param {MemoizeCache<any, V>} [options.cache] - The cache object used to store results. Defaults to a new `Map`.
* @param {(args: A) => unknown} [options.getCacheKey] - An optional function to generate a unique cache key for each argument.
*
* @returns {F & { cache: MemoizeCache<any, ReturnType<F>> }} - The memoized function with an additional `cache` property that exposes the internal cache.
*
* @example
* // Basic usage with default cache
* const add = (a, b) => a + b;
* // Example using the default cache
* const add = (x: number) => x + 10;
* const memoizedAdd = memoize(add);
* console.log(memoizedAdd(1, 2)); // 3
*
* console.log(memoizedAdd(5)); // 15
* console.log(memoizedAdd(5)); // 15 (cached result)
* console.log(memoizedAdd.cache.size); // 1
*
* @example
* // Using a custom resolver
* const resolver = (...args) => args.join('-');
* const memoizedAddWithResolver = memoize(add, { resolver });
* console.log(memoizedAddWithResolver(1, 2)); // 3
* console.log(memoizedAddWithResolver.cache.size); // 1
* // Example using a custom resolver
* const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0);
* const memoizedSum = memoize(sum, { getCacheKey: (arr: number[]) => arr.join(',') });
* console.log(memoizedSum([1, 2])); // 3
* console.log(memoizedSum([1, 2])); // 3 (cached result)
* console.log(memoizedSum.cache.size); // 1
*
* @example
* // Using a custom cache
* class CustomCache {
* constructor() {
* this.store = {};
* // Example using a custom cache implementation
* class CustomCache<K, T> implements MemoizeCache<K, T> {
* private cache = new Map<K, T>();
*
* set(key: K, value: T): void {
* this.cache.set(key, value);
* }
* set(key, value) {
* this.store[key] = value;
*
* get(key: K): T | undefined {
* return this.cache.get(key);
* }
* get(key) {
* return this.store[key];
*
* has(key: K): boolean {
* return this.cache.has(key);
* }
* has(key) {
* return key in this.store;
*
* delete(key: K): boolean {
* return this.cache.delete(key);
* }
* delete(key) {
* delete this.store[key];
*
* clear(): void {
* this.cache.clear();
* }
* clear() {
* this.store = {};
* }
* get size() {
* return Object.keys(this.store).length;
*
* get size(): number {
* return this.cache.size;
* }
* }
* const customCache = new CustomCache();
* const memoizedAddWithCustomCache = memoize(add, { cache: customCache });
* console.log(memoizedAddWithCustomCache(1, 2)); // 3
* const customCache = new CustomCache<string, number>();
* const memoizedSumWithCustomCache = memoize(sum, { cache: customCache });
* console.log(memoizedSumWithCustomCache([1, 2])); // 3
* console.log(memoizedSumWithCustomCache([1, 2])); // 3 (cached result)
* console.log(memoizedAddWithCustomCache.cache.size); // 1
*
* @example
* // Using both custom resolver and custom cache
* const resolver = (...args) => args.join('-');
* const customCache = new CustomCache();
* const memoizedAddWithBoth = memoize(add, { resolver, cache: customCache });
* console.log(memoizedAddWithBoth(1, 2)); // 3
* console.log(memoizedAddWithBoth.cache.size); // 1
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function memoize<F extends (...args: any[]) => any, K = Parameters<F>[0]>(
export function memoize<F extends (...args: any) => any>(
fn: F,
options: MemoizeOptions<K, ReturnType<F>> = {}
): F & { cache: Cache<K, ReturnType<F>> } {
const { cache = new Map<K, ReturnType<F>>(), resolver } = options;
options: {
cache?: MemoizeCache<any, ReturnType<F>>;
getCacheKey?: (args: Parameters<F>[0]) => unknown;
} = {}
): F & { cache: MemoizeCache<any, ReturnType<F>> } {
const { cache = new Map<unknown, ReturnType<F>>(), getCacheKey } = options;
if (typeof fn !== 'function' || (resolver && typeof resolver !== 'function')) {
throw new TypeError('Expected a function and an optional resolver function');
}
const memoizedFn = function (this: unknown, arg: Parameters<F>[0]): ReturnType<F> {
const key = getCacheKey ? getCacheKey(arg) : arg;
const memoizedFn = function (this: unknown, ...args: Parameters<F>): ReturnType<F> {
const key = resolver ? resolver.apply(this, args) : (args[0] as K);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn.apply(this, args);
const result = fn.call(this, arg);
cache.set(key, result);
return result;
};
memoizedFn.cache = cache;
return memoizedFn as F & { cache: Cache<K, ReturnType<F>> };
return memoizedFn as F & { cache: MemoizeCache<any, ReturnType<F>> };
}
export interface MemoizeOptions<K, V> {
cache?: Cache<K, V>;
resolver?: (...args: any[]) => K;
}
export interface Cache<K, V> {
export interface MemoizeCache<K, V> {
set(key: K, value: V): void;
get(key: K): V | undefined;
has(key: K): boolean;