@oazmi/kitchensink
Version:
a collection of personal utility functions
317 lines (316 loc) • 16.3 kB
JavaScript
/** utility functions for creating higher order functions.
*
* @module
*/
import { bindMethodToSelfByName } from "./binder.js";
import { HybridTree, HybridWeakMap, LimitedStack, StrongTree, TREE_VALUE_UNSET } from "./collections.js";
export const memorizeCore = (fn, weak_ref = false) => {
const
// TODO: use HybridWeakMap for memory instead of Map, so that key references to objects and functions are held loosely/weakly, and garbage collectible
memory = weak_ref ? new HybridWeakMap() : new Map(), get = bindMethodToSelfByName(memory, "get"), set = bindMethodToSelfByName(memory, "set"), has = bindMethodToSelfByName(memory, "has"), memorized_fn = (arg) => {
const arg_exists = has(arg), value = arg_exists ? get(arg) : fn(arg);
if (!arg_exists) {
set(arg, value);
}
return value;
};
return { fn: memorized_fn, memory };
};
/** memorize the return value of a single parameter function.
* further calls with memorized arguments will return the value much quicker.
*
* @example
* ```ts
* import { assertEquals as assertEq } from "jsr:@std/assert"
*
* let fn_call_count = 0
* const fn = (arg: string | number) => {
* fn_call_count++
* return `you owe ${arg} to the bank.`
* }
* const memorized_fn = memorize(fn)
*
* assertEq(memorized_fn("a camel") , "you owe a camel to the bank.")
* assertEq(fn_call_count , 1)
* assertEq(memorized_fn("a camel") , "you owe a camel to the bank.")
* assertEq(fn_call_count , 1)
* assertEq(memorized_fn("5") , "you owe 5 to the bank.")
* assertEq(fn_call_count , 2)
* assertEq(memorized_fn(5) , "you owe 5 to the bank.")
* assertEq(fn_call_count , 3)
* assertEq(memorized_fn("your soul"), "you owe your soul to the bank.")
* assertEq(fn_call_count , 4)
* assertEq(memorized_fn("your soul"), "you owe your soul to the bank.")
* assertEq(fn_call_count , 4)
* ```
*/
export const memorize = (fn) => {
return memorizeCore(fn).fn;
};
/** similar to {@link memorize}, but halts its memorization after `n`-unique unmemorized calls are made to the function.
*
* @example
* ```ts
* import { assertEquals as assertEq } from "jsr:@std/assert"
*
* let fn_call_count = 0
* const fn = (arg: string | number) => {
* fn_call_count++
* return `you owe ${arg} to the bank.`
* }
* const memorized_fn = memorizeAtmostN(2, fn)
*
* assertEq(memorized_fn("a camel") , "you owe a camel to the bank.")
* assertEq(fn_call_count , 1)
* assertEq(memorized_fn("a camel") , "you owe a camel to the bank.")
* assertEq(fn_call_count , 1)
* assertEq(memorized_fn("5") , "you owe 5 to the bank.")
* assertEq(fn_call_count , 2)
* // from here on, memorization will be halted
* assertEq(memorized_fn("5") , "you owe 5 to the bank.")
* assertEq(fn_call_count , 2)
* assertEq(memorized_fn(5) , "you owe 5 to the bank.")
* assertEq(fn_call_count , 3)
* assertEq(memorized_fn("your soul"), "you owe your soul to the bank.")
* assertEq(fn_call_count , 4)
* assertEq(memorized_fn("your soul"), "you owe your soul to the bank.")
* assertEq(fn_call_count , 5)
* assertEq(memorized_fn("a camel") , "you owe a camel to the bank.")
* assertEq(fn_call_count , 5)
* ```
*/
export const memorizeAtmostN = (n, fn) => {
const memorization_controls = memorizeCore(fn), memory_has = bindMethodToSelfByName(memorization_controls.memory, "has"), memorized_fn = memorization_controls.fn, memorized_atmost_n_fn = (arg) => {
if (memory_has(arg) || (--n >= 0)) {
return memorized_fn(arg);
}
return fn(arg);
};
return memorized_atmost_n_fn;
};
/** memorizes a function's return value up-until `n`-calls,
* and after this, unmemorized call arguments will either return the optional `default_value` (if it was provided),
* or it will return value of the `n`th call (final call that got memorized).
*
* @example
* ```ts
* import { assertEquals as assertEq } from "jsr:@std/assert"
*
* let fn_call_count = 0
* const fn = (arg: string | number) => {
* fn_call_count++
* return `you owe ${arg} to the bank.`
* }
* const memorized_fn = memorizeAfterN(2, fn, "DEFAULT VALUE!")
*
* assertEq(memorized_fn("a camel") , "you owe a camel to the bank.")
* assertEq(fn_call_count , 1)
* assertEq(memorized_fn("a camel") , "you owe a camel to the bank.")
* assertEq(fn_call_count , 1)
* assertEq(memorized_fn("5") , "you owe 5 to the bank.")
* assertEq(fn_call_count , 2)
* // from here on, memorization will be halted and only `"DEFAULT VALUE!"` will be returned for unmemorized args
* assertEq(memorized_fn("5") , "you owe 5 to the bank.")
* assertEq(fn_call_count , 2)
* assertEq(memorized_fn(5) , "DEFAULT VALUE!")
* assertEq(fn_call_count , 2)
* assertEq(memorized_fn("your soul"), "DEFAULT VALUE!")
* assertEq(fn_call_count , 2)
* assertEq(memorized_fn("your soul"), "DEFAULT VALUE!")
* assertEq(fn_call_count , 2)
* assertEq(memorized_fn("a camel") , "you owe a camel to the bank.")
* assertEq(fn_call_count , 2)
* ```
*/
export const memorizeAfterN = (n, fn, default_value) => {
const memorization_controls = memorizeCore(fn), memory_has = bindMethodToSelfByName(memorization_controls.memory, "has"), memorized_fn = memorization_controls.fn, memorized_after_n_fn = (arg) => {
const value = memory_has(arg) || (--n >= 0) ? memorized_fn(arg) : default_value;
if (n === 0) {
default_value ??= value;
}
return value;
};
return memorized_after_n_fn;
};
/** memorize function and limit the caching memory used for it, through the use of LRU-scheme.
*
* [LRU](https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU) stands for _least recently used_,
* and the LRU-memorization implemented here memorizes atmost `max_capacity` number of unique arguments,
* before dropping its capacity down to `min_capacity` by discarding the oldest memorized results.
*
* since we are not actually tracking the number of calls made for each argument, this is not a real "LRU-cache",
* since a real one would not discard the oldest memorized argument if it is being called consistiently.
* but that's exactly what we do here: we remove the oldest one irrespective of how often/how-recently it has been used.
*
* TODO: for a more true LRU caching system, we will need to to reference count each memorized argument (I think),
* and that can be achieved via my `RcList` (reference counted list) from {@link "collections"} module.
* but we will first have to create a hybrid of `LimitedStack` and `RcList` before swapping it here with `LimitedStack`.
*
* TODO: put on some darn examples! but that's so much of a hassle. someone please kill me.
* > Hi! It's Ronald McBigDonalds here! What can I do to satiate your hunger today? <br>
* > Get me a BiGGuLP of Seppuku, with a CEO shooter on the side. Ariga-Thanks! <br>
* > Wakarimashta! One juicy order of **bigg** last meal is on it way! Please enjoy your once-in-a-lifetime ketchup splatter moment.
*/
export const memorizeLRU = (min_capacity, max_capacity, fn) => {
const memorization_controls = memorizeCore(fn), memory_has = bindMethodToSelfByName(memorization_controls.memory, "has"), memory_del = bindMethodToSelfByName(memorization_controls.memory, "delete"), memorized_fn = memorization_controls.fn, memorized_args_lru = new LimitedStack(min_capacity, max_capacity, (discarded_items) => {
discarded_items.forEach(memory_del);
}), memorized_args_lru_push = bindMethodToSelfByName(memorized_args_lru, "push"), memorized_lru_fn = (arg) => {
const arg_memorized = memory_has(arg);
if (!arg_memorized) {
memorized_args_lru_push(arg);
}
return memorized_fn(arg);
};
return memorized_lru_fn;
};
/** memorize the result of a function only once.
* after that, further calls to the function will not invoke `fn` anymore,
* and instead simply return the same memorized value all the time.
*/
export const memorizeOnce = (fn) => {
return memorizeAfterN(1, fn);
};
export const memorizeMultiCore = (fn, weak_ref = false) => {
const tree = weak_ref ? new HybridTree() : new StrongTree(), memorized_fn = (...args) => {
const subtree = tree.getDeep(args.toReversed()), args_exist = subtree.value !== TREE_VALUE_UNSET, value = args_exist ? subtree.value : fn(...args);
if (!args_exist) {
subtree.value = value;
}
return value;
};
return { fn: memorized_fn, memory: tree };
};
/** memorize the results of a multi-parameter function.
*
* since references to object type arguments are held strongly in the memorized function's cache, you will probably
* want to manage clearing entries manually, using either {@link Map} methods, or {@link StrongTree} methods.
*/
export const memorizeMulti = (fn) => {
return memorizeMultiCore(fn, false).fn;
};
/** memorize the results of a multi-parameter function, using a weak cache.
*
* the used arguments are cached _weakly_, meaning that if a non-primitive object `obj` was used as an argument,
* then `obj` is **not** strongly bound to the memorized function's cache, meaning that if `obj` becomes inaccessible in all scopes,
* then `obj` will become garbage collectible, which will then also clear the cache's reference to `obj` (and its memorized result).
*/
export const memorizeMultiWeak = (fn) => {
return memorizeMultiCore(fn, true).fn;
};
/** curry a function `fn`, with optional `thisArg` option for binding as the `this`-object.
*
* what is currying? it allows a multi-parameter function to be transformed into a higher order function that always takes one argument,
* and spits out another function which also take only one argument, and so on... until all parameters have been filled,
* upon which the final function finally evaluates into the return value (type parameter {@link R} in this case).
*
* note that this function relies on `fn.length` property to identify the number of **required** arguments taken by `fn`.
* this means that default valued arguments (such as `c` in `fn: (a: number, b: number, c = 5) => number`), or rest/spread
* arguments (such as `args` in `fn: (a: number, b: number, ...args: number[]) => number`), are not considered as required,
* and thus do not increment the count of `fn.length`.
*
* currying is usually poorly implemented through the use of closure.
* for instance:
*
* ```ts ignore
* const curried_fn = ((arg0) => (arg1) => (arg2) => fn(arg1, arg2, arg3))
* const output = curried_fn("arg0")("arg1")("arg2")
* ```
*
* this is bad because when you evaluate a curry with N-parameters, you also have to make N-calls (albeit it being tail-calls),
* instead of just one call, should you have had all the parameters from the beginning.
* not to mention that all javascript engines famously do not perform tail-call optimizations.
*
* so here, I've implemented currying using the `bind` method, which means that once all parameters are filled, the function goes through only one call (no overheads).
* the same example from before would translate into the following when binding is used:
*
* ```ts ignore
* const thisArg = undefined
* const curried_fn = fn.bind(thisArg, arg0).bind(thisArg, arg1).bind(thisArg, arg2)
* const output = curried_fn("arg0")("arg1")("arg2")
* ```
*
* do note that it is very possible for the javascript engine to internally create tail-recursion closure for every argument binding,
* resulting in the same unoptimized function that we discredited just a while ago.
* without benchmarking, I cannot say for sure which implementation is more performant.
*
* @param fn the function to curry
* @param thisArg provide an optional argument to use as the `this` object inside of `fn`
* @returns a series of single argument partial functions that does not evaluate until all parameters have been provided
*
* @example
* ```ts
* import { assertEquals as assertEq } from "jsr:@std/assert"
*
* const abcd = (a: number, b: string, c: boolean, d: symbol): string => (String(a) + b + String(c) + " " + String(d))
* const abcd_curry = curry(abcd)
*
* abcd_curry satisfies ((arg: number) => (arg: string) => (arg: boolean) => (arg: symbol) => string)
*
* assertEq(
* ((((abcd_curry(42) satisfies ((arg: string) => (arg: boolean) => (arg: symbol) => string)
* )(" hello to za warudo! ") satisfies ((arg: boolean) => (arg: symbol) => string)
* )(true) satisfies ((arg: symbol) => string)
* )(Symbol.iterator) satisfies (string)
* ),
* "42 hello to za warudo! true Symbol(Symbol.iterator)",
* )
* ```
*/
export const curry = (fn, thisArg) => {
// note that we don't actually bind `fn` to `thisArg`, not until `fn.length <= 1`.
// this is because then we would be binding `fn` to `thisArg` again and again, on every curried recursive call.
// it might be more performant to bind `fn` to `thisArg` on only the final execution call,
// when all parameters, except for the last one, have been provided.
// which is what the `(fn.bind(thisArg) as (arg: ARGS[0]) => R) as any` line below does on the final call
return fn.length > 1 ?
((arg) => curry(fn.bind(undefined, arg))) :
fn.bind(thisArg);
};
/** come here, come all! greet the **Types' Olympic Champion** of winter 2024.
*
* it took a while to correctly apply a multitude of gymnastics to get it functioning, but the dedication has paid off!
* please give `curryMulti` a round of applause!
* and don't forget that currying a diverse variety of types all at once brings strength!
* > (said a nation right before its downfall)
*
* now that introductions are over: {@link curryMulti} behaves very much like {@link curry},
* the only difference being that you can bind an arbitrary number of arguments to the curried `fn` function,
* instead of just a single argument at a time (like in the case of {@link curry}).
*
* @param fn the function to multi-curry
* @param thisArg provide an optional argument to use as the `this` object inside of `fn`
* @param remaining_args number of arguments remaining until all parameters (required kind, ideally) are filled. intended for internal use onkly
* @returns a curried function that consumes variable number of arguments, until all required parameters are available, after which a return value is spat out
*
* @example
* ```ts
* import { assertEquals as assertEq } from "jsr:@std/assert"
*
* const abcd = (a: number, b: string, c: boolean, d: symbol): string => (String(a) + b + String(c) + " " + String(d))
* const abcd_diversity_curry = curryMulti(abcd)
*
* abcd_diversity_curry satisfies CurryMultiSignature<(a: number, b: string, c: boolean, d: symbol) => string, string, any>
*
* assertEq(
* (((abcd_diversity_curry(
* 42, " hello to za warudo! ") satisfies CurryMultiSignature<(c: boolean, d: symbol) => string, string, any>
* )(true) satisfies CurryMultiSignature<(d: symbol) => string, string, any>
* )(Symbol.iterator) satisfies (string)
* ),
* "42 hello to za warudo! true Symbol(Symbol.iterator)",
* )
* ```
*/
export const curryMulti = (fn, thisArg, remaining_args = fn.length) => {
return (...args_a) => {
remaining_args -= args_a.length;
// note that we don't actually bind `fn` to `thisArg`, not until `remaining_args === 0`.
// this is because then we would be binding `fn` to `thisArg` again and again, on every curried recursive call.
// it might be more performant to bind `fn` to `thisArg` on only the final execution call, when all parameters have been provided.
// which is what the `(curried_fn as () => R).call(thisArg) as R` line below does on the final call.
const curried_fn = fn.bind(undefined, ...args_a);
return (remaining_args <= 0 ?
curried_fn.call(thisArg) :
curryMulti(curried_fn, thisArg, remaining_args));
};
};