I have 2 for loops that work well to create a grid with rows and columns, but I would like to improve the solution using recursion as it is more cleaner and recommended (in Functional Programming).
The desired output is a single array of pairs used in css grid
const createGrid = (rows,columns) => {
let grid=[]
for (let y = 3; y <= rows; y += 2){
let row = []
for (let x = 3; x <= columns; x += 2){
let col = [y, x]
row = [...row,col]
}
grid =[...grid, ...row]
}
return grid
}
Are there also any guidelines as to how to convert for loops to recursion solutions when possible?
primitive recursion
Here's one possible way to make a grid using recursion -
const makeGrid = (f, rows = 0, cols = 0) =>
rows <= 0
? []
: [ ...makeGrid(f, rows - 1, cols), makeRow(f, rows, cols) ]
const makeRow = (f, row = 0, cols = 0) =>
cols <= 0
? []
: [ ...makeRow(f, row, cols - 1), f(row, cols) ]
const g =
makeGrid((x, y) => ({ xPos: x, yPos: y }), 2, 3)
console.log(JSON.stringify(g))
// [ [ {"xPos":1,"yPos":1}
// , {"xPos":1,"yPos":2}
// , {"xPos":1,"yPos":3}
// ]
// , [ {"xPos":2,"yPos":1}
// , {"xPos":2,"yPos":2}
// , {"xPos":2,"yPos":3}
// ]
// ]
The functional param f allows us to construct grid cells in a variety of ways
const g =
makeGrid((x, y) => [ x - 1, y - 1 ], 3, 2)
console.log(JSON.stringify(g))
// [ [ [ 0, 0 ]
// , [ 0, 1 ]
// ]
// , [ [ 1, 0 ]
// , [ 1, 1 ]
// ]
// , [ [ 2, 0 ]
// , [ 2, 1 ]
// ]
// ]
work smarter, not harder
Per Bergi's comment, you can reduce some extra argument passing by using a curried cell constructor -
const makeGrid = (f, rows = 0, cols = 0) =>
rows <= 0
? []
: [ ...makeGrid(f, rows - 1, cols), makeRow(f(rows), cols) ]
const makeRow = (f, cols = 0) =>
cols <= 0
? []
: [ ...makeRow(f, cols - 1), f(cols) ]
const g =
makeGrid
( x => y => [ x, y ] // "curried" constructor
, 2
, 3
)
console.log(JSON.stringify(g))
// [ [ [ 1, 1 ]
// , [ 1, 2 ]
// , [ 1, 3 ]
// ]
// , [ [ 2, 1 ]
// , [ 2, 2 ]
// , [ 2, 3 ]
// ]
// ]
have your cake and eat it too
Alternatively, we can incorporate the suggestion and still accept a binary function at the call site using partial application -
const makeGrid = (f, rows = 0, cols = 0) =>
rows <= 0
? []
: [ ...makeGrid(f, rows - 1, cols)
, makeRow(_ => f(rows, _), cols) // <-- partially apply f
]
const makeRow = (f, cols = 0) =>
cols <= 0
? []
: [ ...makeRow(f, cols - 1), f(cols) ]
const g =
makeGrid
( (x,y) => [ x, y ] // ordinary constructor
, 2
, 3
)
console.log(JSON.stringify(g))
// [ [ [ 1, 1 ]
// , [ 1, 2 ]
// , [ 1, 3 ]
// ]
// , [ [ 2, 1 ]
// , [ 2, 2 ]
// , [ 2, 3 ]
// ]
// ]
Nth dimension
Above we are limited to 2-dimensional grids. What if we wanted 3-dimensions or even more?
const identity = x =>
x
const range = (start = 0, end = 0) =>
start >= end
? []
: [ start, ...range(start + 1, end) ] // <-- recursion
const map = ([ x, ...more ], f = identity) =>
x === undefined
? []
: [ f(x), ...map(more, f) ] // <-- recursion
const makeGrid = (r = [], d = 0, ...more) =>
d === 0
? r
: map(range(0, d), x => makeGrid(r(x), ...more)) // <-- recursion
const g =
makeGrid
( x => y => z => [ x, y, z ] // <-- constructor
, 2 // <-- dimension 1
, 2 // <-- dimension 2
, 3 // <-- dimension 3
, // ... <-- dimension N
)
console.log(JSON.stringify(g))
Output
[ [ [ [0,0,0]
, [0,0,1]
, [0,0,2]
]
, [ [0,1,0]
, [0,1,1]
, [0,1,2]
]
]
, [ [ [1,0,0]
, [1,0,1]
, [1,0,2]
]
, [ [1,1,0]
, [1,1,1]
, [1,1,2]
]
]
]
any dimensions; flat result
Per you comment, you want a flat array of pairs. You can achieve that by simply substituting map for flatMap, as demonstrated below -
const identity = x =>
x
const range = (start = 0, end = 0) =>
start >= end
? []
: [ start, ...range(start + 1, end) ]
const flatMap = ([ x, ...more ], f = identity) =>
x === undefined
? []
: [ ...f(x), ...flatMap(more, f) ] // <-- flat!
const makeGrid = (r = [], d = 0, ...more) =>
d === 0
? r
: flatMap(range(0, d), x => makeGrid(r(x), ...more))
const g =
makeGrid
( x => y => [{ x, y }] // <-- constructor
, 2 // <-- dimension 1
, 2 // <-- dimension 2
, // ... <-- dimension N
)
console.log(JSON.stringify(g))
// [ { x: 0, y: 0 }
// , { x: 0, y: 1 }
// , { x: 1, y: 0 }
// , { x: 1, y: 1 }
// ]
The functional constructor demonstrates its versatility again -
const g =
makeGrid
( x => y =>
[[ 3 + x * 2, 3 + y * 2 ]] // whatever you want
, 3
, 3
)
console.log(JSON.stringify(g))
// [[3,3],[3,5],[3,7],[5,3],[5,5],[5,7],[7,3],[7,5],[7,7]]
learn more
As other have show, this particular version of makeGrid using flatMap is effectively computing a cartesian product. By the time you've wrapped your head around flatMap, you already know the List Monad!
more cake, please!
If you're hungry for more, I want to give you a primer on one of my favourite topics in computational study: delimited continuations. Getting started with first class continuations involves developing an intuition on some ways in which they are used -
reset
( call
( (x, y) => [[ x, y ]]
, amb([ 'J', 'Q', 'K', 'A' ])
, amb([ '♡', '♢', '♤', '♧' ])
)
)
// [ [ J, ♡ ], [ J, ♢ ], [ J, ♤ ], [ J, ♧ ]
// , [ Q, ♡ ], [ Q, ♢ ], [ Q, ♤ ], [ Q, ♧ ]
// , [ K, ♡ ], [ K, ♢ ], [ K, ♤ ], [ K, ♧ ]
// , [ A, ♡ ], [ A, ♢ ], [ A, ♤ ], [ A, ♧ ]
// ]
Just like the List Monad, above amb encapsulates this notion of ambiguous (non-deterministic) computations. We can easily write our 2-dimensional simpleGrid using delimited continuations -
const simpleGrid = (f, dim1 = 0, dim2 = 0) =>
reset
( call
( f
, amb(range(0, dim1))
, amb(range(0, dim2))
)
)
simpleGrid((x, y) => [[x, y]], 3, 3)
// [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]]
Creating an N-dimension grid is a breeze thanks to amb as well. The implementation has all but disappeared -
const always = x =>
_ => x
const multiGrid = (f = always([]), ...dims) =>
reset
( apply
( f
, dims.map(_ => amb(range(0, _)))
)
)
multiGrid
( (x, y, z) => [[ x, y, z ]] // <-- not curried this time, btw
, 3
, 3
, 3
)
// [ [0,0,0], [0,0,1], [0,0,2]
// , [0,1,0], [0,1,1], [0,1,2]
// , [0,2,0], [0,2,1], [0,2,2]
// , [1,0,0], [1,0,1], [1,0,2]
// , [1,1,0], [1,1,1], [1,1,2]
// , [1,2,0], [1,2,1], [1,2,2]
// , [2,0,0], [2,0,1], [2,0,2]
// , [2,1,0], [2,1,1], [2,1,2]
// , [2,2,0], [2,2,1], [2,2,2]
// ]
Or we can create the desired increments and offsets using line in the cell constructor -
const line = (m = 1, b = 0) =>
x => m * x + b // <-- linear equation, y = mx + b
multiGrid
( (...all) => [ all.map(line(2, 3)) ] // <-- slope: 2, y-offset: 3
, 3
, 3
, 3
)
// [ [3,3,3], [3,3,5], [3,3,7]
// , [3,5,3], [3,5,5], [3,5,7]
// , [3,7,3], [3,7,5], [3,7,7]
// , [5,3,3], [5,3,5], [5,3,7]
// , [5,5,3], [5,5,5], [5,5,7]
// , [5,7,3], [5,7,5], [5,7,7]
// , [7,3,3], [7,3,5], [7,3,7]
// , [7,5,3], [7,5,5], [7,5,7]
// , [7,7,3], [7,7,5], [7,7,7]
// ]
So where do reset, call, apply, and amb come from? JavaScript does not support first class continuations, but nothing stops us from implementing them on our own -
const call = (f, ...values) =>
({ type: call, f, values }) //<-- ordinary object
const apply = (f, values) =>
({ type: call, f, values }) //<-- ordinary object
const shift = (f = identity) =>
({ type: shift, f }) //<-- ordinary object
const amb = (xs = []) =>
shift(k => xs.flatMap(x => k(x))) //<-- returns ordinary object
const reset = (expr = {}) =>
loop(() => expr) //<-- ???
const loop = f =>
// ... //<-- follow the link!
Given the context of your question, it should be obvious that this is a purely academic exercise. Scott's answer offers sound rationale on some of the trade-offs we make. Hopefully this section shows you that higher-powered computational features can easily tackle problems that initially appear complex.
First class continuations unlock powerful control flow for your programs. Have you ever wondered how JavaScript implements function* and yield? What if JavaScript didn't have these powers baked in? Read the post to see how we can make these (and more) using nothing but ordinary functions.
continuations code demo
See it work in your own browser! Expand the snippet below to generate grids using delimited continuations... in JavaScript! -
// identity : 'a -> 'a
const identity = x =>
x
// always : 'a -> 'b -> 'a
const always = x =>
_ => x
// log : (string, 'a) -> unit
const log = (label, x) =>
console.log(label, JSON.stringify(x))
// line : (int, int) -> int -> int
const line = (m, b) =>
x => m * x + b
// range : (int, int) -> int array
const range = (start = 0, end = 0) =>
start >= end
? []
: [ start, ...range(start + 1, end) ]
// call : (* -> 'a expr, *) -> 'a expr
const call = (f, ...values) =>
({ type: call, f, values })
// apply : (* -> 'a expr, * array) -> 'a expr
const apply = (f, values) =>
({ type: call, f, values })
// shift : ('a expr -> 'b expr) -> 'b expr
const shift = (f = identity) =>
({ type: shift, f })
// reset : 'a expr -> 'a
const reset = (expr = {}) =>
loop(() => expr)
// amb : ('a array) -> ('a array) expr
const amb = (xs = []) =>
shift(k => xs .flatMap (x => k (x)))
// loop : (unit -> 'a expr) -> 'a
const loop = f =>
{ // aux1 : ('a expr, 'a -> 'b) -> 'b
const aux1 = (expr = {}, k = identity) =>
{ switch (expr.type)
{ case call:
return call(aux, expr.f, expr.values, k)
case shift:
return call
( aux1
, expr.f(x => trampoline(aux1(x, k)))
, identity
)
default:
return call(k, expr)
}
}
// aux : (* -> 'a, (* expr) array, 'a -> 'b) -> 'b
const aux = (f, exprs = [], k) =>
{ switch (exprs.length)
{ case 0:
return call(aux1, f(), k) // nullary continuation
case 1:
return call
( aux1
, exprs[0]
, x => call(aux1, f(x), k) // unary
)
case 2:
return call
( aux1
, exprs[0]
, x =>
call
( aux1
, exprs[1]
, y => call(aux1, f(x, y), k) // binary
)
)
case 3: // ternary ...
case 4: // quaternary ...
default: // variadic
return call
( exprs.reduce
( (mr, e) =>
k => call(mr, r => call(aux1, e, x => call(k, [ ...r, x ])))
, k => call(k, [])
)
, values => call(aux1, f(...values), k)
)
}
}
return trampoline(aux1(f()))
}
// trampoline : * -> *
const trampoline = r =>
{ while (r && r.type === call)
r = r.f(...r.values)
return r
}
// simpleGrid : ((...int -> 'a), int, int) -> 'a array
const simpleGrid = (f, dim1 = 0, dim2 = 0) =>
reset
( call
( f
, amb(range(0, dim1))
, amb(range(0, dim2))
)
)
// multiGrid : (...int -> 'a, ...int) -> 'a array
const multiGrid = (f = always([]), ...dims) =>
reset
( apply
( f
, dims.map(_ => amb(range(0, _)))
)
)
// : unit
log
( "simple grid:"
, simpleGrid((x, y) => [[x, y]], 3, 3)
)
// : unit
log
( "multiGrid:"
, multiGrid
( (...all) => [ all.map(line(2, 3)) ]
, 3
, 3
, 3
)
)
At first, on reading your code, I thought you generated one style of grid, so that makeGrid (7, 9) would result in something like this:
[
[[3, 3], [3, 5], [3, 7], [3, 9]],
[[5, 3], [5, 5], [5, 7], [5, 9]],
[[7, 3], [7, 5], [7, 7], [7, 9]]
]
Instead, it returns a single array of pairs:
[[3, 3], [3, 5], [3, 7], [3, 9], [5, 3], [5, 5], [5, 7], [5, 9], [7, 3], [7, 5], [7, 7], [7, 9]]
I'm pretty sure I'm not the only one. Bergi suggested a fix in the comments to change it to the former. (That's what changing grid =[...grid, ...row] to grid =[...grid, row] would do.) And the wonderful answer from Thankyou is predicated on the same assumption.
This is a problem.
When the reader can't quickly understand what your code does, it becomes much harder to maintain... even for yourself just a few weeks later.
The reason you may hear advice to replace loops with recursion is related to this. Loops are all about explicit imperative instructions to get what you want, depending on mutating variables, which then you have to keep track of, and easily subject to off-by-one errors. Recursion is usually more declarative, a way of saying that the result you're looking for is just a matter of combining these simpler results with our current data, and pointing out how to get the simpler results, through either a base case or a recursive call.
The advantage in readability and understandability, though, is the key, not the fact that the solution is recursive.
Don't get me wrong, recursion is one of my favorite programming techniques. The answer from Thankyou is beatiful and elegant. But it's not the only technique which will fix the problems that explicit for-loops present. Usually one of the first things I do when trying to move junior programmer to intermediate and beyond is to replace for-loops with more meaningful constructs. Most loops are trying to do one of a few things. They're trying to convert every element of a list into something new (map), trying to choose some important subset of the elements (filter), trying to find the first important element (find), or trying to combine all the elements into a single value (reduce). By using these instead, the code become more explicit.
Also important, as seen in the answer from Thankyou, is splitting out reusable pieces of the code so that your main function can focus on the important parts. The version below extracts a function rangeBy, which adds a step parameter to my usual range function. range creates a range of integers so that, for instance, range (3, 12) yields [3, 4, 5, 6, 7, 8, 9, 10, 11, 12] rangeBy adds an initial step parameter, so that range (2) (3, 12) yields [3, 5, 7, 9, 11].
We use that rangeBy function along with a map, and its cousin, flatMap to make a more explicit version of your looped function:
const rangeBy = (step) => (lo, hi) =>
[... Array (Math .ceil ((hi - lo + 1) / step))]
.map ((_, i) => i * step + lo)
const createGrid = (rows, columns) =>
rangeBy (2) (3, rows) .flatMap (y =>
rangeBy (2) (3, columns) .map (x =>
[y, x]
)
)
console .log (createGrid (7, 9))
Knowing what rangeBy does, we can mentally read this as
const createGrid = (rows, columns) =>
[3, 5, 7, ..., rows] .flatMap (y =>
[3, 5, 7, ..., columns] .map (x =>
[y, x]
)
)
Note that if you want the behavior I was expecting, you can achieve it just by replacing flatMap with map in createGrid. Also, if you do so, it's trivial to add the more generic behavior that Thankyou offers, by replacing [y, x] with f (x, y) and passing f as a parameter. What remains hard-coded in this version is the conversion of rows and columns into arrays of odd numbers starting with 3. We could make the actual arrays the arguments to our function, and applying rangeBy outside. But at that point, we're probably looking at a different function ideally named cartesianProduct.
So recursion is an amazing and useful tool. But it's a tool, not a goal. Simple, readable code, however, is an important goal.
Update
I meant to mention this originally and simply forgot. The following version demonstrates that the currying in rangeBy is far from fundamental. We can use a single call easily:
const rangeBy = (step, lo, hi) =>
[... Array (Math .ceil ((hi - lo + 1) / step))]
.map ((_, i) => i * step + lo)
const createGrid = (rows, columns) =>
rangeBy (2, 3, rows) .flatMap (y =>
rangeBy (2, 3, columns) .map (x =>
[y, x]
)
)
console .log (createGrid (7, 9))
The main rationale for currying rangeBy is that when it's written like this:
const rangeBy = (step) => (lo, hi) =>
[... Array (Math .ceil ((hi - lo + 1) / step))]
.map ((_, i) => i * step + lo)
we can write the more common range by simply applying 1 to the above. That is,
const range = rangeBy (1)
range (3, 12) //=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
This is so useful that it's become my usual style for writing functions. But it is not a significant part of the simplification of your problem.
Functional programming is more about higher-order functions than direct recursion. I believe the following is equivalent to your example, using _.range from underscore.js and map and flatMap from the standard library.
const rowRange = _.range(3, rows + 1, 2);
const colRange = _.range(3, columns + 1, 2);
return rowRange.flatMap(row => colRange.map(col => [col, row]));
I'm trying to write a function that implements foldl in JavaScript. I'm trying to use recursion in the function but not being able to implement it.
var foldl = function(f, acc, array) {
if (array.length == 0) {
return acc;
} else {
return f(array[0], foldl(f, acc, array.slice(-1)));
}
}
console.log(foldl(function(x, y) {
return x + y
}, 0, [1, 2, 3]));
console.log(foldl(function(x,y){return x+y}, 0, [1,2,3]));
Error Message..
RangeError: Maximum call stack size exceeded
Your challenge, as mentioned above, is that you're returning an array of the last element. And you're always returning an array of the last element.
What is missing from the answers above is that they're only good for folding to the right.
For the right case, you can just use .slice(1), and that will pull everything after the head.
For the fold left case, you need to specify how far you need to go .slice(0, arr.length - 1).
const foldr = (f, acc, arr) => {
if (!arr.length) {
return acc;
} else {
const head = arr[0];
const tail = arr.slice(1);
return foldr(f, f(acc, head), tail);
}
};
foldr((x, y) => x + y, 0, [1, 2, 3])// 6
const foldl = (f, acc, arr) => {
if (!arr.length) {
return acc;
} else {
const head = arr[arr.length - 1];
const tail = arr.slice(0, arr.length - 1);
return foldl(f, f(acc, head), tail);
}
};
foldl((x, y) => x + y, 0, [3, 2, 1]); // 6
This:
array.slice(-1)
should be:
array.slice(1)
slice(-1) returns the array containing the last element. As you're using the first element of the array, you want the array without that element instead. slice(1) will return the array without the first element.
slice(-1) return only the last element. If you want to recurse over the array, use slice(1) instead, which will return all elements except the first:
var foldl = function(f, acc, array) {
if (array.length == 0) {
return acc;
} else {
return f(array[0], foldl(f, acc, array.slice(1)));
}
}
console.log(foldl(function(x, y) {
return x + y
}, 0, [1, 2, 3]));
Note that foldl and foldr can both be implemented in a way that iterates thru the input list in first-to-rest (left-to-right) order. Awkward negative indexes or calculating precise slice positions are not needed.
const Empty =
Symbol ()
const foldl = (f, acc, [ x = Empty, ...xs ]) =>
x === Empty
? acc
: foldl (f, f (acc, x), xs)
const foldr = (f, acc, [ x = Empty, ...xs ]) =>
x === Empty
? acc
: f (foldr (f, acc, xs), x)
const pair = (a,b) =>
`(${a} ${b})`
const data =
[ 1, 2, 3 ]
console.log (foldl (pair, 0, data))
// (((0 1) 2) 3)
console.log (foldr (pair, 0, data))
// (((0 3) 2) 1)
You can use xs[0] and xs.slice(1) if you don't want to use the destructuring assignment
const foldl = (f, acc, xs) =>
xs.length === 0
? acc
: foldl (f, f (acc, xs[0]), xs.slice (1))
const foldr = (f, acc, xs) =>
xs.length === 0
? acc
: f (foldr (f, acc, xs.slice (1)), xs [0])
const pair = (a,b) =>
`(${a} ${b})`
const data =
[ 1, 2, 3 ]
console.log (foldl (pair, 0, data))
// (((0 1) 2) 3)
console.log (foldr (pair, 0, data))
// (((0 3) 2) 1)
...xs via destructuring assignment used in the first solution and slice used in the second solution create intermediate values and could be a performance hit if xs is of considerable size. Below, a third solution that avoids this
const foldl = (f, acc, xs, i = 0) =>
i >= xs.length
? acc
: foldl (f, f (acc, xs[i]), xs, i + 1)
const foldr = (f, acc, xs, i = 0) =>
i >= xs.length
? acc
: f (foldr (f, acc, xs, i + 1), xs [i])
const pair = (a,b) =>
`(${a} ${b})`
const data =
[ 1, 2, 3 ]
console.log (foldl (pair, 0, data))
// (((0 1) 2) 3)
console.log (foldr (pair, 0, data))
// (((0 3) 2) 1)
Above our foldl and foldr are almost perfect drop-in replacements for natives Array.prototype.reduce and Array.prototype.reduceRight respectively. By passing i and xs to the callback, we get even closer
const foldl = (f, acc, xs, i = 0) =>
i >= xs.length
? acc
: foldl (f, f (acc, xs[i], i, xs), xs, i + 1)
const foldr = (f, acc, xs, i = 0) =>
i >= xs.length
? acc
: f (foldr (f, acc, xs, i + 1), xs[i], i, xs)
const pair = (acc, value, i, self) =>
{
console.log (acc, value, i, self)
return acc + value
}
console.log (foldl (pair, 'a', [ 'b', 'c', 'd' ]))
// a b 0 [ b, c, d ]
// ab c 1 [ b, c, d ]
// abc d 2 [ b, c, d ]
// => abcd
console.log (foldr (pair, 'z', [ 'w', 'x', 'y' ]))
// z y 2 [ x, y, z ]
// zy x 1 [ x, y, z ]
// zyx w 0 [ x, y, z ]
// => zyxw
And finally, reduce and reduceRight accept a context argument. This is important if the folding function f refers to this. If you want to support a configurable context in your own folds, it's easy
const foldl = (f, acc, xs, context = null, i = 0) =>
i >= xs.length
? acc
: foldl ( f
, f.call (context, acc, xs[i], i, xs)
, xs
, context
, i + 1
)
const foldr = (f, acc, xs, context = null, i = 0) =>
i >= xs.length
? acc
: f.call ( context
, foldr (f, acc, xs, context, i + 1)
, xs[i]
, i
, xs
)
const obj =
{ a: 1, b: 2, c: 3, d: 4, e: 5 }
// some function that uses `this`
const picker = function (acc, key) {
return [ ...acc, { [key]: this[key] } ]
}
console.log (foldl (picker, [], [ 'b', 'd', 'e' ], obj))
// [ { b: 2 }, { d: 4 }, { e: 5 } ]
console.log (foldr (picker, [], [ 'b', 'd', 'e' ], obj))
// [ { e: 5 }, { d: 4 }, { b: 2 } ]
I'm trying to sum a nested array [1,2,[3,4],[],[5]] without using loops but I don't see what's wrong with what I have so far.
function sumItems(array) {
let sum = 0;
array.forEach((item) => {
if (Array.isArray(item)) {
sumItems(item);
} else {
sum += item;
}
});
return sum;
}
try with
function sumItems(array) {
let sum = 0;
array.forEach((item) => {
if(Array.isArray(item)) {
sum += sumItems(item);
} else {
sum += item;
}
})
return sum;
}
recursion is a functional heritage
Recursion is a concept that comes from functional style. Mixing it with imperative style is a source of much pain and confusion for new programmers.
To design a recursive function, we identify the base and inductive case(s).
base case - the list of items to sum is empty; ie, item is Empty. return 0
inductive case 1 - the list of items is not empty; ie, there must be at least one item. if the item is a list, return its sum plus the sum of the rest of the items
inductive case 2 - there is at least one item that is not an array. return this item plus the sum of the rest of the items
const Empty =
Symbol ()
const sumDeep = ([ item = Empty, ...rest ] = []) =>
item === Empty
? 0
: Array.isArray (item)
? sumDeep (item) + sumDeep (rest)
: item + sumDeep (rest)
console.log
( sumDeep ([ [ 1, 2 ], [ 3, 4 ], [ 5, [ 6, [] ] ] ]) // 21
, sumDeep ([ 1, 2, 3, 4, 5, 6 ]) // 21
, sumDeep ([]) // 0
, sumDeep () // 0
)
As a result of this implementation, all pain and suffering are removed from the program. We do not concern ourselves with local state variables, variable reassignment, or side effects like forEach and not using the return value of a function call.
recursion caution
And a tail-recursive version which can be made stack-safe. Here, we add a parameter cont to represent our continuation which effectively allows us sequence the order of + operations without growing the stack – changes in bold
const identity = x =>
x
const sumDeep = ([ item = Empty, ...rest ] = [], cont = identity) =>
item === Empty
? cont (0)
: Array.isArray (item)
? sumDeep (item, a =>
sumDeep (rest, b =>
cont (a + b)))
: sumDeep (rest, a =>
cont (item + a))
Usage is identitcal
console.log
( sumDeep ([ [ 1, 2 ], [ 3, 4 ], [ 5, [ 6, [] ] ] ]) // 21
, sumDeep ([ 1, 2, 3, 4, 5, 6 ]) // 21
, sumDeep ([]) // 0
, sumDeep () // 0
)
performance enhancement
As #גלעד ברקן points out, array destructuring syntax used above (eg ...rest) create copies of the input array. As demonstrated in his/her answer, an index parameter can be used which will avoid creating copies. This variation shows how the index technique can also be used in a tail-recursive way
const identity = x =>
x
const sumDeep = (items = [], i = 0, cont = identity) =>
i >= items.length
? cont (0)
: Array.isArray (items [i])
? sumDeep (items [i], 0, a =>
sumDeep (items, i + 1, b =>
cont (a + b)))
: sumDeep (items, i + 1, a =>
cont (items [i] + a))
console.log
( sumDeep ([ [ 1, 2 ], [ 3, 4 ], [ 5, [ 6, [] ] ] ]) // 21
, sumDeep ([ 1, 2, 3, 4, 5, 6 ]) // 21
, sumDeep ([]) // 0
, sumDeep () // 0
)
Here's a version without using loops:
function f(arr, i){
if (i == arr.length)
return 0;
if (Array.isArray(arr[i]))
return f(arr[i], 0) + f(arr, i + 1);
return arr[i] + f(arr, i + 1);
}
console.log(f([1,2,[3,4],[],[5]], 0));
You could define a callback for using with Array#reduce, which check if an item is an array and uses this function again for that array.
function add(s, v) {
return Array.isArray(v)
? v.reduce(add, s)
: s + v;
}
var array = [1, 2, [3, 4], [], [5]];
console.log(array.reduce(add, 0));
You may do as follows;
var sumNested = ([a,...as]) => (as.length && sumNested(as)) + (Array.isArray(a) ? sumNested(a) : a || 0);
console.log(sumNested([1,2,3,[4,[5,[6]]],7,[]]));
The function argument designation [a,…as] means that when the function is fed with a nested array like [1,2,3,[4,[5,[6]]],7,[]] then a is assigned to the head which is 1 and as is assigned to the tail of the initial array which is [2,3,[4,[5,[6]]],7,[]]. The rest should be easy to understand.
function arraySum (array) {
if (array.length > 0) {
return arraySum(array[0]) + arraySum(array.slice(1));
}
if (array.length === 0) {
return 0;
} else {
return array;
}
};
This is similar to some of the other solutions but might be easier for some to read:
function Sum(arr) {
if (!arr.length) return 0;
if (Array.isArray(arr[0])) return Sum(arr[0]) + Sum(arr.slice(1));
return arr[0] + Sum(arr.slice(1));
}
console.log(Sum([[1],2,[3,[4,[5,[6,[7,[8,9,10],11,[12]]]]]]])) // 78