admin管理员组文章数量:1279124
In all the examples I can find on memoization / internal function cache in Functional Programming in JavaScript, the examples are either mutating or reassigning the cache.
Here's an example taken from
function memoizer(fun){
let cache = {}
return function (n){
if (cache[n] != undefined ) {
return cache[n]
} else {
let result = fun(n)
cache[n] = result
return result
}
}
}
The only tiny improvement I can e up with is to use reassigning instead of mutating the cache object:
function memoizer(fun){
let cache = {}
return function (n){
if (cache[n] != undefined ) {
return cache[n]
} else {
let result = fun(n)
cache = {... cache, [n]: result}
return result
}
}
}
But my question is how to do this in a "pure" functional way, without mutations or reassigning a let/var?
In all the examples I can find on memoization / internal function cache in Functional Programming in JavaScript, the examples are either mutating or reassigning the cache.
Here's an example taken from https://scotch.io/tutorials/understanding-memoization-in-javascript#toc-a-functional-approach
function memoizer(fun){
let cache = {}
return function (n){
if (cache[n] != undefined ) {
return cache[n]
} else {
let result = fun(n)
cache[n] = result
return result
}
}
}
The only tiny improvement I can e up with is to use reassigning instead of mutating the cache object:
function memoizer(fun){
let cache = {}
return function (n){
if (cache[n] != undefined ) {
return cache[n]
} else {
let result = fun(n)
cache = {... cache, [n]: result}
return result
}
}
}
But my question is how to do this in a "pure" functional way, without mutations or reassigning a let/var?
Share Improve this question edited Dec 7, 2020 at 21:08 rags2riches-prog 1,7611 gold badge12 silver badges24 bronze badges asked Dec 7, 2020 at 20:04 Dac0d3rDac0d3r 1,8547 gold badges47 silver badges87 bronze badges 5- 3 Re-creating the whole cache seems pretty inefficient. – Pointy Commented Dec 7, 2020 at 20:09
- 1 You would need to wrap the function so that it takes a tuple of old cache and arguments and returns another tuple of new cache and result. This makes it very cumbersome to use, but it's possible. – Bergi Commented Dec 7, 2020 at 20:16
- 1 I'm not saying that there's no place on SO for interesting puzzle questions (not downvoting or VTC this), but on a practical level isn't this kinda just pure wankery? Your function (and by construction all the functions you feed it) will always return the same value for the same input. – Jared Smith Commented Dec 7, 2020 at 22:59
- 1 I'd use a lazy unfold that represents the entire result set (codomain) of your function but only lazily, i.e. on demand evaluated. I think this is the mechanism used in purely functional programming. However, I never implemented it so its just a teaser. – user5536315 Commented Dec 8, 2020 at 7:46
- @scriptum Thanks, but can't really find much about "lazy unload" but did find this about "unload" ramdajs./docs/#unfold and nabilhassein.github.io/blog/unfold - but you're talking about doing it lazily / on demand. Can you give a little more info on what you had in mind? – Dac0d3r Commented Dec 8, 2020 at 10:53
5 Answers
Reset to default 6With the definition of a "pure function" being:
In puter programming, a pure function is a function that has the following properties:[1][2]
Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices). Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).
I think we can see that what you have is a pure function, if the same values are passed to the function, you will always get the same result; there is nothing in that method that would be affected by any external state beyond it –– that is unless the passed fun
argument isn't pure, but that isn't an issue with your memoizer
method itself.
EDIT
Because the cache
object is not a static variable, mutating it does not violate any of the rules of what makes a pure function pure.
Definition of "side effects" as used by Wikipedia in the explanation above:
In puter science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation.
This partially answers the question how to memoize all kind of functions in a purely functional manner. The solution is pletely different from your imperative one and the code is not production ready but just a proof of concept. Please note that I am also a rookie on this subject.
The answer is partial, because I am only going to show a memoize function whose domain (arguments) are integers. A more advanced polymorphic memoization function can handle all kinds of argument types and also recursive functions.
The basic idea is to have a function that operates on an infinite list of integers. A list can be only infinite if it is non-strictly evaluated, that is to say only evaluated when needed and only just enough. We will later see that non-strictness is not enough for memoization, we need proper lazy evaluation.
In the first example I am going to use explicit thunks (e.g. nullary functions of shape () => "do something"
for the sake of simplicity:
// INFINITE LIST
// we only mimick such a type
const iterate = f => {
const go = x =>
[x, () => go(f(x))];
return go;
};
// Functor
const map = f => {
const go = ([x, thunk]) =>
[log(f(x)), () => go(thunk())];
return go;
};
// element lookup
const lookup = ([x, thunk]) => i =>
i === 0
? x
: lookup(thunk()) (i - 1);
// memoization
const memoize = f =>
lookup(
map(f)
(iterate(x => x + 1) (0)));
// auxiliary function
const log = x => (console.log(x), x);
const fib = n => {
return n > 1
? fib(n - 1) + fib(n - 2)
: n;
}
// MAIN
const foo = memoize(fib);
console.log("yields:", foo(6)); // logs 1, 1, 2, 3, 5, 8
console.log("yields:", foo(6)); // logs 1, 1, 2, 3, 5, 8
This works with an infinite sequence of fibonacci numbers and yields the expected result, but there is still no memoization. Right now it is just a cumbersome way to calculate places from the fibunacci sequence.
While this algorithm is non-strict, we need proper lazy evaluation as already mentioned. Lazy evaluation means non-strict evaluation with sharing of once puted thunks. We can mimic lazy evaluation with the native Proxy
type in Javascript. Please note that the explicit thunks from the above example are now replaced with implicit ones, i.e. you don't need to call (thunk()
) but just use them as if they were ordinary values. Here is a working sketch:
const NULL = "null";
const THUNK = "thunk";
const thunk = thunk =>
new Proxy(thunk, new ThunkProxy());
class ThunkProxy {
constructor() {
this.memo = NULL;
}
apply(g, that, args) {
if (this.memo === NULL) {
this.memo = g();
while (this.memo && this.memo[THUNK] === true)
this.memo = this.memo.valueOf();
}
return this.memo(...args);
}
get(g, k) {
if (k === THUNK)
return true;
else if (this.memo === NULL) {
this.memo = g();
while (this.memo && this.memo[THUNK] === true)
this.memo = this.memo.valueOf();
}
if (k === "valueOf")
return () => this.memo
else if (k === "toString")
return () => this.memo.toString();
else if (k === Symbol.toStringTag)
return Object.prototype.toString.call(this.memo).slice(8, -1);
while (this.memo[k] && this.memo[k] [THUNK] === true)
this.memo[k] = this.memo[k].valueOf();
if (typeof this.memo[k] === "function")
return this.memo[k].bind(this.memo);
else return this.memo[k];
}
has(g, k) {
if (k === THUNK)
return true;
else if (this.memo === NULL) {
this.memo = g();
while (this.memo && this.memo[THUNK] === true)
this.memo = this.memo.valueOf();
}
return k in this.memo;
}
}
const iterate = f => {
const go = x =>
[x, thunk(() => go(f(x)))];
return go;
};
const lookup = ([head, tail]) => i =>
i === 0
? head
: lookup(tail) (i - 1);
const map = f => {
const go = ([head, tail]) =>
[log(f(head)), thunk(() => go(tail))];
return go;
};
const memoize = f =>
lookup(
map(f)
(iterate(x => x + 1) (0)));
const log = x => (console.log(x), x);
const fib = n => {
return n > 1
? fib(n - 1) + fib(n - 2)
: n;
}
const foo = memoize(fib);
console.log("yields:", foo(6)); // logs 0, 1, 1, 2, 3, 5, 8, "yields: 8"
console.log("yields:", foo(6)); // logs "yields: 8"
console.log("yields:", foo(10)); // logs 13, 21, 34, 55, "yields: 55"
Finally we've achieved the desired memoization effect, but the way I mimic lazy evaluation in Javascript is impure as well - I am just cheating. In a purely functional language like Haskell, however, lazy evaluation is an implementation detail of the language and thus not part of the syntax you actually use. This way the language itself remains pure.
Please note that fib
is a recursive function and memoize
does not handle the recursive steps but only intermediate and the final result.
(define (fib-memo n memo k)
(let ((p (assv n memo)))
(if p
(let ((v (cdr p)))
(k v memo))
(fib-memo (- n 1) memo
(lambda (v1 memo^)
(fib-memo (- n 2) memo^
(lambda (v2 memo^^)
(let ((v (+ v1 v2)))
(k v (cons (cons n v) memo^^))))))))))
(define (fib n)
(fib-memo n '((1 . 1) (0 . 0)) (lambda (v memo) v)))
The Scheme code above is a purely functional memoized version of fibonacci using continuation-passing style to accumulate information as the putation progresses.
As long as the variable cache
remains private to the function I can't think of anything that could go wrong by mutating it. Incidentally recreating a new cache object isn't helpful because you do re-assign to the same reference anyway.
You have to update the cache somehow for your memoizer to be useful. You just need to make sure you're doing it in a safe way. Don't read or write from a global cache for example.
I'm sure there are ways to do this with monads (or other functional programming constructs) but I'd don't feel qualified enough to talk about it. Keeping your function pure (and by the look of it it is) is already a big win in my opinion.
Note that in the event where applying the memoized function to n does return undefined
(or null
) this would never be cached because with cache[n] != undefined
you cannot differentiate between a call you didn't make yet and a call that genuinely returned undefined
. You should perhaps do something like cache.hasOwnProperty(n)
instead.
See also Is mutating accumulator in reduce function considered bad practice?
An alternative implementation of memoizer
could use Function#bind
but admittedly this may not be a better way of doing it.
const memoizer =
fn =>
((cache, n) =>
n in cache
? cache[n]
: cache[n] = fn(n)).bind(null, {});
In a pure functional language we are not allowed to mutate values. However, it is possible to change the state of a variable from uninitialized to initialized during execution. This can be leveraged for memorization.
We provide an example of how to implement the Fibonacci Recursion in the pure functional language nix below.
As lists are lazily evaluated in nix, we can incrementally initialize values in a list, and pute them on first access. Subsequent accesses do not re-trigger putation but used the "cached" value:
let
# iota n = [1 2 3 ... n]
iota = n: if n == 0 then [] else (iota (n - 1)) ++ [n];
fib = n:
let
results = map (x: g x) ([0] ++ (iota n));
f = x:
if x <= 2 then 1 else
(elemAt results (x - 1)) + (elemAt results (x - 2));
g = x: trace "Called f ${toString x}" (f x);
in
g n;
in
fib 5;
; nix repl -f fib.nix
trace: Called f 5
trace: Called f 4
trace: Called f 3
trace: Called f 2
trace: Called f 1
5
Code example with tests is available on GitHub
本文标签: How to implement memoization with quotpurequot Functional Programming in JavaScriptStack Overflow
版权声明:本文标题:How to implement memoization with "pure" Functional Programming in JavaScript - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741300185a2371048.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论