diff --git a/docs/.vitepress/zh_hans.mts b/docs/.vitepress/zh_hans.mts index ed39f8aa..a13c0d9f 100644 --- a/docs/.vitepress/zh_hans.mts +++ b/docs/.vitepress/zh_hans.mts @@ -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' }, diff --git a/docs/ko/reference/function/memoize.md b/docs/ko/reference/function/memoize.md index 767e3139..be7da32d 100644 --- a/docs/ko/reference/function/memoize.md +++ b/docs/ko/reference/function/memoize.md @@ -1,61 +1,66 @@ # memoize -주어진 함수의 결과를 인수에 기반하여 캐싱함으로써 메모이제이션해요. +연산 결과를 캐싱하는 새로운 메모이제이션된 함수를 반환해요. 메모이제이션된 함수는 같은 인자에 대해서 중복해서 연산하지 않고, 캐시된 결과를 반환해요. + +인자를 0개 또는 1개만 받는 함수만 메모이제이션할 수 있어요. 2개 이상의 인자를 받는 함수를 메모이제이션하려면, +여러 인자를 1개의 객체나 배열로 받도록 리팩토링하세요. + +인자가 배열이나 객체여서 원시 값이 아닌 경우, 올바르게 캐시 키를 계산할 수 있도록 `getCacheKey` 함수를 옵션으로 제공하세요. ## 인터페이스 ```typescript -export function memoize any, K = Parameters[0]>( +function memoize any>( fn: F, - options: MemoizeOptions> = {} -): F & { cache: Cache> }; + options: { + cache?: MemoizeCache>; + getCacheKey?: (args: Parameters[0]) => unknown; + } = {} +): F & { cache: MemoizeCache> }; -export interface MemoizeOptions { - cache?: Cache; - resolver?: (...args: any[]) => K; -} - -export interface Cache { - set: (key: K, value: V) => void; - get: (key: K) => V | undefined; - has: (key: K) => boolean; - delete: (key: K) => boolean | void; - clear: () => void; +interface MemoizeCache { + 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>, optional): 캐시 키를 생성할 함수와 결과를 저장할 캐시 객체를 포함해요. - - `resolver ((...args: any[]) => K, optional)`: 캐시 키를 생성할 함수. 제공되지 않으면 메모이제이션된 함수의 첫 번째 인수를 키로 사용해요. - - `cache (Cache>, optional)`: 결과를 저장할 캐시 객체. 기본값은 새로운 Map 인스턴스입니다. +- `fn` (`F`) - 메모이제이션할 함수. 0개 또는 1개 인자를 받아야 해요. +- `options`: 메모이제이션 옵션. + - `options.cache` (`MemoizeCache>`): 연산 결과를 저장할 캐시 객체. 기본값은 새로운 `Map`이에요. + - `options.getCacheKey` (`(args: A) => unknown`): 원시 값이 아닌 인자에 대해서 캐시 키를 올바르게 계산할 수 있는 함수. ### 반환 값 -`(F & { cache: Cache> })`: 캐시 속성을 가진 메모이제이션된 함수. +(`F & { cache: MemoizeCache> }`): 메모이제이션된 함수. 추가로 내부 캐시를 노출하는 `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 implements Cache { +// 커스텀 `MemoizeCache` 정의하기 +class CustomCache implements MemoizeCache { private cache = new Map(); set(key: K, value: T): void { this.cache.set(key, value); @@ -77,33 +82,8 @@ class CustomCache implements Cache { } } const customCache = new CustomCache(); -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 ``` diff --git a/docs/reference/function/memoize.md b/docs/reference/function/memoize.md index 601f1fdc..a3c21eba 100644 --- a/docs/reference/function/memoize.md +++ b/docs/reference/function/memoize.md @@ -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 any, K = Parameters[0]>( +function memoize any>( fn: F, - options: MemoizeOptions> = {} -): F & { cache: Cache> }; + options: { + cache?: MemoizeCache>; + getCacheKey?: (args: Parameters[0]) => unknown; + } = {} +): F & { cache: MemoizeCache> }; -interface MemoizeOptions { - cache?: Cache; - resolver?: (...args: any[]) => K; -} - -interface Cache { - set: (key: K, value: V) => void; - get: (key: K) => V | undefined; - has: (key: K) => boolean; - delete: (key: K) => boolean | void; - clear: () => void; +interface MemoizeCache { + 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>, 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>, 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>`): 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> })`: The memoized function with a cache property. +(`F & { cache: MemoizeCache> }`): 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 implements Cache { +class CustomCache implements MemoizeCache { private cache = new Map(); set(key: K, value: T): void { this.cache.set(key, value); @@ -78,33 +86,8 @@ class CustomCache implements Cache { } } const customCache = new CustomCache(); -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 ``` diff --git a/docs/zh_hans/reference/function/memoize.md b/docs/zh_hans/reference/function/memoize.md new file mode 100644 index 00000000..8f5c7ddd --- /dev/null +++ b/docs/zh_hans/reference/function/memoize.md @@ -0,0 +1,88 @@ +# memoize + +创建一个函数的备忘版本。备忘函数会基于接收到的参数缓存结果,因此如果再次传递相同的参数,它会返回缓存的结果,而不是重新计算。 + +这个功能适用于接受零个或一个参数的函数。如果你的函数接受多个参数,你应该将其重构为接受一个组合了这些参数的对象或数组。 + +如果参数不是原始类型(例如数组或对象),请提供一个 `getCacheKey` 函数来生成唯一的缓存键,以确保正确缓存。 + +## 签名 + +```typescript +function memoize any>( + fn: F, + options: { + cache?: MemoizeCache>; + getCacheKey?: (args: Parameters[0]) => unknown; + } = {} +): F & { cache: MemoizeCache> }; + +interface MemoizeCache { + 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>`): 用于存储结果的缓存对象。默认为一个新的 `Map`。 + - `options.getCacheKey` (`(args: A) => unknown`): 可选函数,用于为每个参数生成唯一的缓存键。 + +### 返回 + +(`F & { cache: MemoizeCache> }`): 备忘版本的函数,并带有一个额外的 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 implements MemoizeCache { + private cache = new Map(); + 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(); +const memoizedSumWithCustomCache = memoize(sum, { cache: customCache }); +console.log(memoizedSumWithCustomCache([1, 2])); // 3 +console.log(memoizedSumWithCustomCache([1, 2])); // 3 (缓存结果) +console.log(memoizedAddWithCustomCache.cache.size); // 1 +``` diff --git a/src/function/index.ts b/src/function/index.ts index 4b351ee9..a05203d0 100644 --- a/src/function/index.ts +++ b/src/function/index.ts @@ -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'; diff --git a/src/function/memoize.spec.ts b/src/function/memoize.spec.ts index 1db51ae4..670d095c 100644 --- a/src/function/memoize.spec.ts +++ b/src/function/memoize.spec.ts @@ -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 = new Map(); diff --git a/src/function/memoize.ts b/src/function/memoize.ts index fd1ccf82..27c31f87 100644 --- a/src/function/memoize.ts +++ b/src/function/memoize.ts @@ -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>} [options] - An options object with a resolver function and/or a custom cache object. - * @returns {F & { cache: Cache> }} - 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[0], ReturnType>} [options={}] - Optional configuration for the memoization. + * @param {MemoizeCache} [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> }} - 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 implements MemoizeCache { + * private cache = new Map(); + * + * 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(); + * 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 any, K = Parameters[0]>( +export function memoize any>( fn: F, - options: MemoizeOptions> = {} -): F & { cache: Cache> } { - const { cache = new Map>(), resolver } = options; + options: { + cache?: MemoizeCache>; + getCacheKey?: (args: Parameters[0]) => unknown; + } = {} +): F & { cache: MemoizeCache> } { + const { cache = new Map>(), 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[0]): ReturnType { + const key = getCacheKey ? getCacheKey(arg) : arg; - const memoizedFn = function (this: unknown, ...args: Parameters): ReturnType { - 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> }; + + return memoizedFn as F & { cache: MemoizeCache> }; } -export interface MemoizeOptions { - cache?: Cache; - resolver?: (...args: any[]) => K; -} - -export interface Cache { +export interface MemoizeCache { set(key: K, value: V): void; get(key: K): V | undefined; has(key: K): boolean;