Trampoline recursion stack overflow - javascript

I have this recursive function sum which computes sum of all numbers that were passed to it.
function sum(num1, num2, ...nums) {
if (nums.length === 0) { return num1 + num2; }
return sum(num1 + num2, ...nums);
}
let xs = [];
for (let i = 0; i < 100; i++) { xs.push(i); }
console.log(sum(...xs));
xs = [];
for (let i = 0; i < 10000; i++) { xs.push(i); }
console.log(sum(...xs));
It works fine if only 'few' numbers are passed to it but overflows call stack otherwise. So I have tried to modify it a bit and use trampoline so that it can accept more arguments.
function _sum(num1, num2, ...nums) {
if (nums.length === 0) { return num1 + num2; }
return () => _sum(num1 + num2, ...nums);
}
const trampoline = fn => (...args) => {
let res = fn(...args);
while (typeof res === 'function') { res = res(); }
return res;
}
const sum = trampoline(_sum);
let xs = [];
for (let i = 0; i < 10000; i++) { xs.push(i); }
console.log(sum(...xs));
xs = [];
for (let i = 0; i < 100000; i++) { xs.push(i); }
console.log(sum(...xs));
While the first version isn't able to handle 10000 numbers, the second is. But if I pass 100000 numbers to the second version I'm getting call stack overflow error again.
I would say that 100000 is not really that big of a number (might be wrong here) and don't see any runaway closures that might have caused the memory leak.
Does anyone know what is wrong with it?

The other answer points out the limitation on number of function arguments, but I wanted to remark on your trampoline implementation. The long computation we're running may want to return a function. If you use typeof res === 'function', it's not longer possible to compute a function as a return value!
Instead, encode your trampoline variants with some sort of unique identifiers
const bounce = (f, ...args) =>
({ tag: bounce, f: f, args: args })
const done = (value) =>
({ tag: done, value: value })
const trampoline = t =>
{ while (t && t.tag === bounce)
t = t.f (...t.args)
if (t && t.tag === done)
return t.value
else
throw Error (`unsupported trampoline type: ${t.tag}`)
}
Before we hop on, let's first get an example function to fix
const none =
Symbol ()
const badsum = ([ n1, n2 = none, ...rest ]) =>
n2 === none
? n1
: badsum ([ n1 + n2, ...rest ])
We'll throw a range of numbers at it to see it work
const range = n =>
Array.from
( Array (n + 1)
, (_, n) => n
)
console.log (range (10))
// [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
console.log (badsum (range (10)))
// 55
But can it handle the big leagues?
console.log (badsum (range (1000)))
// 500500
console.log (badsum (range (20000)))
// RangeError: Maximum call stack size exceeded
See the results in your browser so far
const none =
Symbol ()
const badsum = ([ n1, n2 = none, ...rest ]) =>
n2 === none
? n1
: badsum ([ n1 + n2, ...rest ])
const range = n =>
Array.from
( Array (n + 1)
, (_, n) => n
)
console.log (range (10))
// [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
console.log (badsum (range (1000)))
// 500500
console.log (badsum (range (20000)))
// RangeError: Maximum call stack size exceeded
Somewhere between 10000 and 20000 our badsum function unsurprisingly causes a stack overflow.
Besides renaming the function to goodsum we only have to encode the return types using our trampoline's variants
const goodsum = ([ n1, n2 = none, ...rest ]) =>
n2 === none
? n1
? done (n1)
: goodsum ([ n1 + n2, ...rest ])
: bounce (goodsum, [ n1 + n2, ...rest ])
console.log (trampoline (goodsum (range (1000))))
// 500500
console.log (trampoline (goodsum (range (20000))))
// 200010000
// No more stack overflow!
You can see the results of this program in your browser here. Now we can see that neither recursion nor the trampoline are at fault for this program being slow. Don't worry though, we'll fix that later.
const bounce = (f, ...args) =>
({ tag: bounce, f: f, args: args })
const done = (value) =>
({ tag: done, value: value })
const trampoline = t =>
{ while (t && t.tag === bounce)
t = t.f (...t.args)
if (t && t.tag === done)
return t.value
else
throw Error (`unsupported trampoline type: ${t.tag}`)
}
const none =
Symbol ()
const range = n =>
Array.from
( Array (n + 1)
, (_, n) => n
)
const goodsum = ([ n1, n2 = none, ...rest ]) =>
n2 === none
? done (n1)
: bounce (goodsum, [ n1 + n2, ...rest ])
console.log (trampoline (goodsum (range (1000))))
// 500500
console.log (trampoline (goodsum (range (20000))))
// 200010000
// No more stack overflow!
The extra call to trampoline can get annoying, and when you look at goodsum alone, it's not immediately apparent what done and bounce are doing there, unless maybe this was a very common convention in many of your programs.
We can better encode our looping intentions with a generic loop function. A loop is given a function that is recalled whenever the function calls recur. It looks like a recursive call, but really recur is constructing a value that loop handles in a stack-safe way.
The function we give to loop can have any number of parameters, and with default values. This is also convenient because we can now avoid the expensive ... destructuring and spreading by simply using an index parameter i initialized to 0. The caller of the function does not have the ability to access these variables outside of the loop call
The last advantage here is that the reader of goodsum can clearly see the loop encoding and the explicit done tag is no longer necessary. The user of the function does not need to worry about calling trampoline either as it's already taken care of for us in loop
const goodsum = (ns = []) =>
loop ((sum = 0, i = 0) =>
i >= ns.length
? sum
: recur (sum + ns[i], i + 1))
console.log (goodsum (range (1000)))
// 500500
console.log (goodsum (range (20000)))
// 200010000
console.log (goodsum (range (999999)))
// 499999500000
Here's our loop and recur pair now. This time we expand upon our { tag: ... } convention using a tagging module
const recur = (...values) =>
tag (recur, { values })
const loop = f =>
{ let acc = f ()
while (is (recur, acc))
acc = f (...acc.values)
return acc
}
const T =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [T]: t })
const is = (t, x) =>
t && x[T] === t
Run it in your browser to verify the results
const T =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [T]: t })
const is = (t, x) =>
t && x[T] === t
const recur = (...values) =>
tag (recur, { values })
const loop = f =>
{ let acc = f ()
while (is (recur, acc))
acc = f (...acc.values)
return acc
}
const range = n =>
Array.from
( Array (n + 1)
, (_, n) => n
)
const goodsum = (ns = []) =>
loop ((sum = 0, i = 0) =>
i >= ns.length
? sum
: recur (sum + ns[i], i + 1))
console.log (goodsum (range (1000)))
// 500500
console.log (goodsum (range (20000)))
// 200010000
console.log (goodsum (range (999999)))
// 499999500000
extra
My brain has been stuck in anamorphism gear for a few months and I was curious if it was possible to implement a stack-safe unfold using the loop function introduced above
Below, we look at an example program which generates the entire sequence of sums up to n. Think of it as showing the work to arrive at the answer for the goodsum program above. The total sum up to n is the last element in the array.
This is a good use case for unfold. We could write this using loop directly, but the point of this was to stretch the limits of unfold so here goes
const sumseq = (n = 0) =>
unfold
( (loop, done, [ m, sum ]) =>
m > n
? done ()
: loop (sum, [ m + 1, sum + m ])
, [ 1, 0 ]
)
console.log (sumseq (10))
// [ 0, 1, 3, 6, 10, 15, 21, 28, 36, 45 ]
// +1 ↗ +2 ↗ +3 ↗ +4 ↗ +5 ↗ +6 ↗ +7 ↗ +8 ↗ +9 ↗ ...
If we used an unsafe unfold implementation, we could blow the stack
// direct recursion, stack-unsafe!
const unfold = (f, initState) =>
f ( (x, nextState) => [ x, ...unfold (f, nextState) ]
, () => []
, initState
)
console.log (sumseq (20000))
// RangeError: Maximum call stack size exceeded
After playing with it a little bit, it is indeed possible to encode unfold using our stack-safe loop. Cleaning up the ... spread syntax using a push effect makes things a lot quicker too
const push = (xs, x) =>
(xs .push (x), xs)
const unfold = (f, init) =>
loop ((acc = [], state = init) =>
f ( (x, nextState) => recur (push (acc, x), nextState)
, () => acc
, state
))
With a stack-safe unfold, our sumseq function works a treat now
console.time ('sumseq')
const result = sumseq (20000)
console.timeEnd ('sumseq')
console.log (result)
// sumseq: 23 ms
// [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ..., 199990000 ]
Verify the result in your browser below
const recur = (...values) =>
tag (recur, { values })
const loop = f =>
{ let acc = f ()
while (is (recur, acc))
acc = f (...acc.values)
return acc
}
const T =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [T]: t })
const is = (t, x) =>
t && x[T] === t
const push = (xs, x) =>
(xs .push (x), xs)
const unfold = (f, init) =>
loop ((acc = [], state = init) =>
f ( (x, nextState) => recur (push (acc, x), nextState)
, () => acc
, state
))
const sumseq = (n = 0) =>
unfold
( (loop, done, [ m, sum ]) =>
m > n
? done ()
: loop (sum, [ m + 1, sum + m ])
, [ 1, 0 ]
)
console.time ('sumseq')
const result = sumseq (20000)
console.timeEnd ('sumseq')
console.log (result)
// sumseq: 23 ms
// [ 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, ..., 199990000 ]

Browsers have practical limits on the number of arguments a function can take
You can change the sum signature to accept an array rather than a varying number of arguments, and use destructuring to keep the syntax/readability similar to what you have. This "fixes" the stackoverflow error, but is increadibly slow :D
function _sum([num1, num2, ...nums]) { /* ... */ }
I.e.:, if you're running in to problems with maximum argument counts, your recursive/trampoline approach is probably going to be too slow to work with...

The other answer already explained the issue with your code. This answer demonstrates that trampolines are sufficiently fast for most array based computations and offer a higher level of abstraction:
// trampoline
const loop = f => {
let acc = f();
while (acc && acc.type === recur)
acc = f(...acc.args);
return acc;
};
const recur = (...args) =>
({type: recur, args});
// sum
const sum = xs => {
const len = xs.length;
return loop(
(acc = 0, i = 0) =>
i === len
? acc
: recur(acc + xs[i], i + 1));
};
// and run...
const xs = Array(1e5)
.fill(0)
.map((x, i) => i);
console.log(sum(xs));
If a trampoline based computation causes performance problems, then you can still replace it with a bare loop.

Related

Calculating second largest number in array (Javascript) : Seems to work in Sandbox but fails Hackerrank testing

Tried to determine the second largest number in an array (Javascript) on CodeSandbox. It seems to work fine, but it fails the CodeWars testing. I have added a dummy array just to run my own tests in Sandbox.(Have mercy, I'm a beginner and this is my first StackOverFlow question)
const nums = [3, 100.3, 88, 1, -2.4, 9, 18];
const getSecondLargest = (nums) => {
const descending = nums.sort((a, b) => b - a);
return descending[1];
};
console.log(getSecondLargest(nums)); // console returns 88
EDIT: Okay so I with my super-tired brain I said CodeWars, when I actually meant Hackerrank (so sorry!). I realized they didn't necessarily test with NaNs, but they did have repeating numbers, so using the index of [1] isn't ideal. The exercise is from the 10 Days of Javascript - Day 3: Arrays https://hackerrank.com/domains/tutorials/10-days-of-javascript
So I now tried this code below, and it passes...but my code seems a bit janky, is there a cleaner way to write this, and can I combine it with the isNan logic then?
const nums = [3, 100, 88, 100, -2.4, 9, 18];
const getSecondLargest = (nums) => {
const ascending = nums.sort((a, b) => a - b);
if (ascending[ascending.length - 2] === ascending[ascending.length - 1]) {
return ascending[ascending.length - 3];
} else {
return ascending[ascending.length - 2];
}
};
console.log(getSecondLargest(nums)); // console returns 88
It looks like there maybe strings in the array and you need to handle that. Here are a few ways:
One is to filter the non-numerical stuff out before sorting. You can use isNaN() to test if an object "is not a number".
const getSecondLargest = (nums) => {
const descending = nums
.filter(n => !isNaN(n))
.sort((a, b) => b - a);
return descending.length < 2 ? undefined : descending[1];
};
Another option is to handle the strings in sorting. Push them to the end of the array:
const getSecondLargest = (nums) => {
const descending = nums.sort((a, b) => {
if (isNaN(a) && isNaN(b)) return 0;
if (isNaN(a)) return 1;
if (isNaN(b)) return -1;
return b - a;
});
return descending.length < 2 || isNaN(descending[1]) ? undefined : descending[1];
};
A third way is a simple for loop that keeps track of the 2 highest values:
const getSecondLargest = (nums) => {
let max1 = undefined;
let max2 = undefined;
for (let n of nums) {
if (isNaN(n)) continue;
if (max2 === undefined || n > max2) {
if (max1 === undefined || n > max1 ) {
max2 = max1;
max1 = n;
}
else {
max2 = n;
}
}
}
return max2;
}

Chain methods and call function one by one

so basically I need to implement a lazy evaluation. .add() takes a function as parameter and any other arbitrary arguments. The function that is passed as an argument is run later (when evaluate is called) with other arguments(if any) as parameters to that function.
Basically my issue stands when i run .evaluate() which takes an array as parameter, the functions that were passed to add() as parameters are not called one at a time and returning a result and have it as a parameter for the next.
class Lazy {
constructor() {
this.functions = [];
this.counter = 0;
this.resultedArray = [];
}
add(func, ...args) {
if (args.length > 0) {
this.counter++;
const argsArrayNumeric = args.filter((item) => typeof item === "number");
this.functions.push({
func: func,
args: args,
});
} else {
if (func.length <= args.length + 1 || func.length === args.length) {
this.counter++;
this.functions.push({ func: func });
} else {
console.log("Missing parameters for " + func.name + " function");
}
}
return this;
}
evaluate(array) {
if (this.counter > 0) {
let result;
this.functions.map((obj, index) => {
array.map((item, index) => {
console.log(obj);
if (obj.func && obj.args) {
result = obj.func(item, ...obj.args);
} else {
result = obj.func(item);
}
this.resultedArray.push(result);
});
});
console.log(this.resultedArray);
} else {
console.log(array);
}
}
}
const s = new Lazy();
const timesTwo = (a) => {
return a * 2;
};
const plus = (a, b) => {
return a + b;
};
s.add(timesTwo).add(plus, 1).evaluate([1, 2, 3]);
//correct result is [3,5,7] but i have [ 2, 4, 6, 2, 3, 4 ]
There are a few problems in evaluate:
push will be executed too many times when you have more than one function.
Don't use map when you don't really map. .map() returns a result.
From the expected output (in the original version of the question) it seems you need to apply the functions from right to left, not from left to right.
this.counter does not really play a role. The length of the this.functions array should be all you need.
the result should not be printed in the method, but returned. It is the caller's responsibility to print it or do something else with it.
All this can be dealt with using reduce or reduceRight (depending on the expected order) like this:
evaluate(array) {
return this.functions.reduceRight((result, obj) =>
result.map((item) => obj.func(item, ...(obj.args || [])))
, array);
}
And in the main program, print the return value:
console.log(s.add(plus, 1).add(timesTwo).evaluate([1, 2, 3]));
The add method has also some strange logic, like:
When the first if condition is false, then the else block kicks in with args.length == 0. It is then strange to see conditions on args.length... it really is 0!
If the first condition in func.length <= args.length + 1 || func.length === args.length is false, then surely the second will always be false also. It should not need to be there.
argsArrayNumeric is never used.
All in all, it seems the code could be reduced to this snippet:
class Lazy {
constructor() {
this.functions = [];
}
add(func, ...args) {
if (func.length > args.length + 1) {
throw ValueError(`Missing parameters for ${func.name} function`);
}
this.functions.push({ func, args });
return this;
}
evaluate(array) {
return this.functions.reduceRight((result, obj) =>
result.map((item) => obj.func(item, ...(obj.args || [])))
, array);
}
}
const timesTwo = (a) => a * 2;
const plus = (a, b) => a + b;
const s = new Lazy();
console.log(s.add(plus, 1).add(timesTwo).evaluate([1, 2, 3])); // [3, 5, 7]
I think you're working too hard at this. I believe this does almost the same thing (except it doesn't report arity errors; that's easy enough to include but doesn't add anything to the discussion):
class Lazy {
constructor () {
this .fns = []
}
add (fn, ...args) {
this .fns .push ({fn, args});
return this
}
evaluate (xs) {
return xs .map (
x => this .fns .reduce ((a, {fn, args}) => fn (...args, a), x)
)
}
}
const timesTwo = (a) => a * 2
const plus = (a, b) => a + b
const s = new Lazy () .add (timesTwo) .add (plus, 1)
console .log (s .evaluate ([1, 2, 3]))
But I don't actually see much of a reason for the class here. A plain function will do much the same:
const lazy = (fns = [], laze = {
add: (fn, ...args) => lazy (fns .concat ({fn, args})),
evaluate: (xs) => xs .map (
x => fns .reduce ((a, {fn, args}) => fn (...args, a), x)
)
}) => laze
const timesTwo = (a) => a * 2
const plus = (a, b) => a + b
const s = lazy () .add (timesTwo) .add (plus, 1)
console .log (s .evaluate ([1, 2, 3]))
This is slightly different in that s is not mutated on each add, but a new object is returned. While we could change that and mutate and return the original object easily enough, the functional programming purist in me would actually consider moving more in that direction. In fact, all we're maintaining here is a list of {fn, args} objects. It might make the most sense to take that on directly, like this:
const add = (lazy, fn, ...args) => lazy .concat ({fn, args})
const evaluate = (lazy, xs) => xs .map (
x => lazy .reduce ((a, {fn, args}) => fn (...args, a), x)
)
const timesTwo = (a) => a * 2
const plus = (a, b) => a + b
const q = []
const r = add (q, timesTwo)
const s = add (r, plus, 1)
// or just
// const s = add (add ([], timesTwo), plus, 1)
console .log (evaluate (s, [1, 2, 3]))
In this version, we don't need a Lazy constructor or a lazy factory function, as our data structure is a plain array. While this is the format I prefer, any of these should do something similar to what you're trying.
Update
Based on comments, I include one more step on my journey, between the first and second snippets above:
const lazy = () => {
const fns = []
const laze = {
add: (fn, ...args) => fns .push ({fn, args}) && laze,
evaluate: (xs) => xs .map (
x => fns .reduce((a, {fn, args}) => fn (...args, a), x)
)
}
return laze
}
const timesTwo = (a) => a * 2
const plus = (a, b) => a + b
const s = lazy () .add (timesTwo) .add (plus, 1)
console .log (s .evaluate ([1, 2, 3]))
This version may be more familiar than the second snippet. It stores fns in a closure, hiding implementation details in a way we often don't with JS classes. But that list is still mutable through the add method of the object we return. Further ones proceed down the road to immutability.

Javascript - Counting array elements by reduce method until specific value occurs doesn't give a correct output

const arr = [5,6,0,7,8];
const sum = (arr,num) => arr.reduce((total)=>(num==0 ? total : total+num), 0)
console.log(sum(arr, 0))
Please check how can I make it work. Did some mistake but don't know what exactly. Output is a function instead of a result.
This is awkward to do in .reduce because it goes through the entire array. If we do a naive implementation you can see the problem:
const arr = [5,6,0,7,8];
const sum = (arr,num) => arr.reduce((total, x)=>(num==x ? total : total+x), 0)
console.log(sum(arr, 0))
We now make the check correctly - num==x will return true when x is zero (the value of num). However, the result is wrong because this only returns true once but any other iteration it's still true. And here is the same thing with more logging that describes each step of the process:
const arr = [5,6,0,7,8];
const sum = (arr,num) => arr.reduce((total, x)=> {
const boolCheck = num==x;
const result = boolCheck ? total : total+x;
console.log(
`total: ${total}
num: ${num}
x: ${x}
boolCheck: ${boolCheck}
result: ${result}`);
return result;
}, 0)
console.log(sum(arr, 0))
So, you need to add some flag that persists between iterations, so it doesn't get lost.
One option is to have an external flag that you change within the reduce callback:
const arr = [5,6,0,7,8];
const sum = (arr,num) => {
let finished = false;
return arr.reduce((total, x) => {
if(x === num)
finished = true;
return finished ? total : total+x;
}, 0)
}
console.log(sum(arr, 0))
Alternatively, you can have that flag internal to the reduce callback and pass it around between calls. It works the same way in the end but makes the callback function pure. At the cost of some unorthodox construct:
const arr = [5,6,0,7,8];
const sum = (arr,num) => {
return arr.reduce(({total, finished}, x) => {
if(x === num)
finished = true;
total = finished ? total : total+x;
return {total, finished};
}, {total: 0, finished: false})
.total
}
console.log(sum(arr, 0))
If you want to use reduce but you're OK with using other methods, then you can use Array#indexOf to find the first instance of a value and Array#slice the array that contains any value up to the target value:
const arr = [5,6,0,7,8];
const sum = (arr,num) => {
const endIndex = arr.indexOf(num);
return arr.slice(0, endIndex)
.reduce((total, x)=> total+x, 0)
}
console.log(sum(arr, 0))
Or in as one chained expression:
const arr = [5,6,0,7,8];
const sum = (arr,num) => arr
.slice(0, arr.indexOf(num))
.reduce((total, x)=> total+x, 0);
console.log(sum(arr, 0))
Other libraries may have a takeUntil or takeWhile operation which is even closer to what you want - it gets you an array from the beginning up to a given value or condition. You can then reduce the result of that.
Here is an example of this using Lodash#takeWhile
By using chaining here, Lodash will do lazy evaluation, so it will only go through the array once, instead of scanning once to find the end index and going through the array again to sum it.
const arr = [5,6,0,7,8];
const sum = (arr,num) => _(arr)
.takeWhile(x => x !== num)
.reduce((total, x)=>total+x, 0)
console.log(sum(arr, 0))
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
As a note, if you are using Lodash, then you may as well use _.sum(). I didn't above just to illustrate how a generic takeUntil/takeWhile looks.
const arr = [5, 6, 0, 7, 8];
const sum = (arr, num) => _(arr)
.takeWhile(x => x !== num)
.sum()
console.log(sum(arr, 0))
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
Since you need to stop summing values part way through the array, this might be most simply implemented using a for loop:
const arr = [5, 6, 0, 7, 8];
const num = 0;
let sum = 0;
for (let i = 0; i < arr.length; i++) {
if (arr[i] == num) break;
sum += arr[i];
}
console.log(sum);
If you want to use reduce, you need to keep a flag that says whether you have seen the num value so you can stop adding values from the array:
const arr = [5, 6, 0, 7, 8];
const sum = (arr, num) => {
let seen = false;
return arr.reduce((c, v) => {
if (seen || v == num) {
seen = true;
return c;
}
return c + v;
}, 0);
}
console.log(sum(arr, 0));
console.log(sum(arr, 8));
call it as follows:
console.log(sum(arr, 0)());
You need parenthesis to execute the function ()
sum(arr, 0)
Without parenthesis you store a reference to the function in the variable

Usage of Promise.All in recursion doesn't seems to be working

Actual doSomething function posts ele to a remote API to do some calculations.
My calc function supposed to get the summation of the remote API's calculation for each element, It should run for every element without affecting how nested they are located.
However, Currently, I can't get this to work. How do I fix this?
const doSomething = (ele) => new Promise(resolve => {
console.log(ele);
resolve(ele * 2);//for example
})
const calc = (arr) => new Promise(
async(resolve) => {
console.log(arr.filter(ele => !Array.isArray(ele)));
let sum = 0;
const out = await Promise.all(arr.filter(ele => !Array.isArray(ele))
.map(ele => doSomething(ele)));
sum += out.reduce((a, b) => a + b, 0);
const out2 = await Promise.all(arr.filter(ele => Array.isArray(ele))
.map(ele => calc(ele)));
sum += out2.reduce((a, b) => a + b, 0);
resolve(sum);
}
)
const process = async () => {
console.log('processing..');
const arr = [1, 2, 3, 4, 5, [6,7], 1, [8,[10,11]]];
const out = await calc(arr);
console.log(out);
}
process();
While it may look like I've addressed issues that are non-existent - the original code in the question had ALL the flaws I address in this answer, including Second and Third below
yes, the code in the question now works! But it clearly was flawed
First: no need for Promise constructor in calc function, since you use Promise.all which returns a promise, if you make calc async, just use await
Second: dosomething !== doSomething
Third: out2 is an array, so sum += out2 is going to mess you up
Fourth: .map(ele => doSomething(ele)) can be written .map(doSoemthing) - and the same for the calc(ele) map
So, working code becomes:
const doSomething = (ele) => new Promise(resolve => {
resolve(ele * 2); //for example
})
const calc = async(arr) => {
const out = await Promise.all(arr.filter(ele => !Array.isArray(ele)).map(doSomething));
let sum = out.reduce((a, b) => a + b, 0);
const out2 = await Promise.all(arr.filter(ele => Array.isArray(ele)).map(calc));
sum += out2.reduce((a, b) => a + b, 0);
return sum;
}
const process = async() => {
console.log('processing..');
const arr = [1, 2, 3, 4, 5, [6, 7], 1, [8, [10, 11]]];
const out = await calc(arr);
console.log(out);
}
process();
Can I suggest a slightly different breakdown of the problem?
We can write one function that recursively applies your function to all (nested) elements of your array, and another to recursively total the results.
Then we await the result of the first call and pass it to the second.
I think these functions are simpler, and they are also reusable.
const doSomething = async (ele) => new Promise(resolve => {
setTimeout(() => resolve(ele * 2), 1000);
})
const recursiveCall = async (proc, arr) =>
Promise .all (arr .map (ele =>
Array .isArray (ele) ? recursiveCall (proc, ele) : proc (ele)
))
const recursiveAdd = (ns) =>
ns .reduce ((total, n) => total + (Array .isArray (n) ? recursiveAdd (n) : n), 0)
const process = async() => {
console.log('processing..');
const arr = [1, 2, 3, 4, 5, [6, 7], 1, [8, [10, 11]]];
const processedArr = await recursiveCall (doSomething, arr);
const out = recursiveAdd (processedArr)
console.log(out);
}
process();
I think a generic deepReduce solves this problem well. Notice it's written in synchronous form -
const deepReduce = (f, init = null, xs = []) =>
xs.reduce
( (r, x) =>
Array.isArray(x)
? deepReduce(f, r, x)
: f(r, x)
, init
)
Still, we can use deepReduce asynchronously by initialising with a promise and reducing with an async function -
deepReduce
( async (r, x) =>
await r + await doSomething(x)
, Promise.resolve(0)
, input
)
.then(console.log, console.error)
See the code in action here -
const deepReduce = (f, init = null, xs = []) =>
xs.reduce
( (r, x) =>
Array.isArray(x)
? deepReduce(f, r, x)
: f(r, x)
, init
)
const doSomething = x =>
new Promise(r => setTimeout(r, 200, x * 2))
const input =
[1, 2, 3, 4, 5, [6,7], 1, [8,[10,11]]]
deepReduce
( async (r, x) =>
await r + await doSomething(x)
, Promise.resolve(0)
, input
)
.then(console.log, console.error)
// 2 + 4 + 6 + 8 + (10 + 14) + 2 + (16 + (20 + 22))
// => 116
console.log("doing something. please wait...")
further generalisation
Above we are hand-encoding a summing function, (+), with the empty sum 0. In reality, this function could be more complex and maybe we want a more general pattern so we can construct our program piecewise. Below we take synchronous add and convert it to an asynchronous function using liftAsync2(add) -
const add = (x = 0, y = 0) =>
x + y // <-- synchronous
const main =
pipe
( deepMap(doSomething) // <-- first do something for every item
, deepReduce(liftAsync2(add), Promise.resolve(0)) // <-- then reduce
)
main([1, 2, 3, 4, 5, [6,7], 1, [8,[10,11]]])
.then(console.log, console.error)
// 2 + 4 + 6 + 8 + (10 + 14) + 2 + (16 + (20 + 22))
// => 116
deepMap and deepReduce generics. These are in curried form so they can plug directly into pipe, but that is only a matter of style -
const deepReduce = (f = identity, init = null) => (xs = []) =>
xs.reduce
( (r, x) =>
Array.isArray(x)
? deepReduce(f, r)(x)
: f(r, x)
, init
)
const deepMap = (f = identity) => (xs = []) =>
xs.map
( x =>
Array.isArray(x)
? deepMap(f)(x)
: f(x)
)
liftAsync2 takes a common binary (has two parameters) function and "lifts" it into the asynchronous context. pipe and identity are commonly available in most functional libs or easy to write yourself -
const identity = x =>
x
const pipe = (...fs) =>
x => fs.reduce((r, f) => f(r), x)
const liftAsync2 = f =>
async (x, y) => f (await x, await y)
Here's all of the code in a demo you can run yourself. Notice because deepMap synchronously applies doSomething to all nested elements, all promises are run in parallel. This is in direct contrast to the serial behaviour in the first program. This may or may not be desirable so it's important to understand the difference in how these run -
const identity = x =>
x
const pipe = (...fs) =>
x => fs.reduce((r, f) => f(r), x)
const liftAsync2 = f =>
async (x, y) => f (await x, await y)
const deepReduce = (f = identity, init = null) => (xs = []) =>
xs.reduce
( (r, x) =>
Array.isArray(x)
? deepReduce(f, r)(x)
: f(r, x)
, init
)
const deepMap = (f = identity) => (xs = []) =>
xs.map
( x =>
Array.isArray(x)
? deepMap(f)(x)
: f(x)
)
const doSomething = x =>
new Promise(r => setTimeout(r, 200, x * 2))
const add =
(x, y) => x + y
const main =
pipe
( deepMap(doSomething)
, deepReduce(liftAsync2(add), Promise.resolve(0))
)
main([1, 2, 3, 4, 5, [6,7], 1, [8,[10,11]]])
.then(console.log, console.error)
// 2 + 4 + 6 + 8 + (10 + 14) + 2 + (16 + (20 + 22))
// => 116
console.log("doing something. please wait...")

Can we implement tail recursion modulo cons et al. through trampolines?

You can regard trampolines as compiler optimizations reified in the program. So what is stopping us from adapting more general optimization techniques in exactly the same manner.
Here is a sketch of tail recursion modulo cons:
const loop = f => {
let step = f();
while (step && step[step.length - 1] && step[step.length - 1].type === recur) {
let step_ = step.pop();
step.push(...f(...step_.args));
}
return step;
};
const recur = (...args) =>
({type: recur, args});
const push = (xs, x) => (xs.push(x), xs);
const map = f => xs =>
loop((i = 0) =>
i === xs.length
? []
: push([f(xs[i])], recur(i + 1)));
const xs =
map(x => x * 2) (Array(1e6).fill(0).map((x, i) => i))
.slice(0,5);
console.log(xs); // [0, 2, 4, 6, 8]
This kind of optimization depends on the associativity property of an expression. Multiplication is associative too and hence there is tail recursion modulo multiplication. However, I have to cheat to implement it in Javascript:
const loop = f => {
let step = f();
const acc = [];
while (step && step[1] && step[1].type === recur) {
acc.push(step[0]);
step = f(...step[1].args);
}
return acc.reduce((acc, f) => f(acc), step);
};
const recur = (...args) =>
({type: recur, args});
const mul = x => step => [y => x * y, step];
const pow = (x_, n_) =>
loop((x = x_, n = n_) =>
n === 0 ? 1
: n === 1 ? x
: mul(x) (recur(x, n - 1)));
console.log(
pow(2, 1e6)); // Infinity, no stack overflow
As you can see I cannot use a regular mul, which isn't particular satisfying. Is this connected with Javascript beeing a strict language? Is there a better way to achieve tail recursion modulo multiplication in JS without having to introduce awkward binary operators?
Instead of using loop/recur (which I consider an ugly and unnecessary hack), consider using folds:
const recNat = (zero, succ) => n => {
let result = zero;
while (n > 0) {
result = succ(result);
n = n - 1;
}
return result;
};
const mul = x => y => x * y;
const pow = x => recNat(1, mul(x));
console.log([0,1,2,3,4,5,6,1e6].map(pow(2))); // [1,2,4,8,16,32,64,Infinity]
Almost every recursive function can be defined using folds (a.k.a. structural recursion, a.k.a. induction). For example, even the Ackermann function can be defined using folds:
const recNat = (zero, succ) => n => {
let result = zero;
while (n > 0) {
result = succ(result);
n = n - 1;
}
return result;
};
const add = x => y => x + y;
const ack = recNat(add(1),
ackPredM => recNat(ackPredM(1),
ackMPredN => ackPredM(ackMPredN)));
console.time("ack(4)(1)");
console.log(ack(4)(1)); // 65533
console.timeEnd("ack(4)(1)");
The above code snippet takes about 18 seconds to compute the answer on my laptop.
Now, you might ask why I implemented recNat using iteration instead of natural recursion:
const recNat = (zero, succ) => function recNatZS(n) {
return n <= 0 ? zero : succ(recNatZS(n - 1));
};
I used iteration for the same reason you used iteration to implement loop. Trampolining. By implementing a different trampoline for every data type you're going to fold, you can write functional code without having to worry about stack overflows.
Bottom line: Use folds instead of explicit recursion. They are a lot more powerful than you think.

Categories