Your code is based off of a bad building block (each
) and therefore the rest of your code suffers the consequences. My answer will start you off with a better building block, reduce
(aka foldLeft
), and show you how to build up from there.
What you'll notice about this answer is that we break each function down into really simple pieces and then use higher-order functions to put everything together.
- avoid use of
each
which limits us to side-affecting functions
- no need to check for
Array.isArray
- no imperative style
for
-loops with mutable iterators, i
- no need to check array
.length
property
I highly encourage you to step thru the evaluation of this code and see how all of the parts work. If you can get an understanding of how either of these programs work, you'll be well on your way to mastering some of most important fundamentals of functional programming: recursion, immutability, referential transparency, higher-order functions, currying, and function composition†
ES6 offers arrow functions, destructuring assignment, and spread syntax which makes functional programs a breeze to write in JavaScript – tho it will feel quite different to read, at first. I'll offer pre-ES6 below this snippet.
Related: What do multiple arrow functions mean in JavaScript?
// reduce is your new, gold-standard building block
const reduce = f => y => ([x,...xs]) => {
if (x === undefined)
return y
else
return reduce (f) (f (y) (x)) (xs)
}
// derive map from reduce
const map = f => reduce (acc => x => [...acc, f(x)]) ([])
// get max of 2 numbers
const max = x => y => x > y ? x : y
// get max of a list of numbers
const maximum = reduce (max) (-Infinity)
// get each max of a list of a list of numbers
const maximums = map (maximum)
// see the result of your hard work
console.log(maximums ([ [ 1, 3, 2 ], [ 7, 5, 6 ] ]))
// => [ 3, 7 ]
If you're struggling with the code above, this code (below) is written in ES5 (more accurately, pre-ES6). The more familiar syntax might help you along while you're still cutting your teeth. It's more verbose in ES5 but it works (almost) identically.
// reduce is your new, gold-standard building block
function reduce (f) {
return function (y) {
return function (xs) {
if (xs.length === 0)
return y
else
return reduce (f) (f (y) (xs[0])) (xs.slice(1))
}
}
}
// derive map from reduce
function map (f) {
return reduce (function (acc) {
return function (x) {
return acc.concat([ f(x) ])
}
}) ([])
}
// get max of 2 numbers
function max (x) {
return function (y) {
return x > y ? x : y
}
}
// get max of a list of numbers
var maximum = reduce (max) (-Infinity);
// get each max of a list of a list of numbers
var maximums = map (maximum);
// see the result of your hard work
console.log(maximums ([ [ 1, 3, 2 ], [ 7, 5, 6 ] ]))
// => [ 3, 7 ]
† Of course there are other fundamentals of functional programming, but this is certainly a good start
Still stuck?
reduce
is inarguably the most sophisticated function in the programs above. If you're struggling with it, we can cheat a little bit to short-cut our understanding of the program. JavaScript offers a built-in Array.prototype.reduce which (almost) works the same. We could write our reduce
using JavaScript's and get the rest for free !
Just one more thing. JavaScript's reduce expects a binary function but the rest of our program is expecting curried functions (sequences of unary functions). To get around that, we will first make a small uncurry
combinator to fit everything together
// convert sequence of unary functions to a binary function
const uncurry = f => (x,y) => f (x) (y)
// we can cheat using JavaScript built-in reduce with uncurry
const reduce = f => y => xs => xs.reduce(uncurry(f), y)
// the rest stays the same !
// ...
// derive map from reduce
const map = f => reduce (acc => x => [...acc, f(x)]) ([])
// get max of 2 numbers
const max = x => y => x > y ? x : y
// get max of a list of numbers
const maximum = reduce (max) (-Infinity)
// get each max of a list of a list of numbers
const maximums = map (maximum)
// see the result of your hard work
console.log(maximums ([ [ 1, 3, 2 ], [ 7, 5, 6 ] ]))
// => [ 3, 7 ]
Extra credit 1
So I told you reduce
is your Gold Standard building block. Aside from map
, did you know that you can also use reduce
to implement countless other functions? Some include: filter
, find
, some
, every
, keys
, and entries
.
Extra credit 2
Some functions mentioned in Extra Credit 1 should short-circuit when the final answer can be returned before the end of the array is reached. Which ones can be short-circuited? And how can we rewrite reduce
to facilitate this early-exit behaviour?