Hackernoon logoHigher Order Functions: Behind the Scenes by@odemeulder

Higher Order Functions: Behind the Scenes

Author profile picture

@odemeulderOlivier De Meulder

This post is NOT a post to show you how to use forEach, map, filter and reduce, NOR is it a post showing the benefits of higher order functions. There are plenty of other articles that do that.

When I want to understand a basic concept, I like to try to implement it myself, even if it is a very basic version. People learn in different ways, this works for me. So let me take you on this journey as I try to create my own forEach, map, reduce and filter functions in JavaScript.

myForEach

In order to disambiguate from the built-in forEach, letโ€™s call my new function myForEach.

First thing to do is to look at the call signature. So, the forEach function you know does something like this:

let arr = [ 1, 2, 3, 4 ]
// log each item of the array
arr.forEach(console.log)
// double each item of the array and log it to the console
arr.forEach((val) => console.log(val * 2))

Weโ€™re sort of looking for a function that takes two parameters, an array and callback function. The callback function could be a function definition or an anonymous function. And what we want this to do is to iterate over each item in the array and call the callback on that item. So letโ€™s give it a shot.

function myForEach(arr, callback) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i])
}
}
let arr = [ 1, 2, 3, 4 ]
// log each item of the array
myForEach(arr, console.log)
// double each item of the array and log it to the console
myForEach(arr, val => console.log(val * 2))

Seems simple enough and it works. Butย โ€ฆ butย โ€ฆ itโ€™s not exactly the same. If you try arr.myForEach(console.log), you get TypeError: arr.myForEach is not a function. That is because we declared myForEach as a global function. And really, it should be part of the prototype of the Array object. Note the purpose of this article is not to explain how prototypical inheritance works. In order to achieve what we want, we need to do this as follows.

Array.prototype.myForEach = (callback) => {
for (let i = 0; i < this.length; i++) {
callback(arr[i])
}
}
// log each item of the array
arr.myForEach(console.log)

When I tried running the code above, nothing happened. What is wrong?

Trying to get to fancy with arrow functions. Arrow functions donโ€™t have their own this or arguments binding. So when I call this inside the function definition, it does not refer to the Array object itself, as I naively expected. Here is the correct way to do it. This StackOverflow post has the explanation.

Array.prototype.myForEach = function (callback) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i])
}
}
// log each item of the array
arr.myForEach(console.log)

Beautiful. Now that we have the mechanics down, we can move on to the other higher order functions.

myMap

The signature is as follows. Imagine we have an array of numbers, and want to get an array of the squares of these numbers.

let arr = [ 1, 2, 3, 4 ]
let arrNew = arr.map(val => val * val)
// or 
const squareNumber = n => n * n
let arrNew2 = arr.map(squareNumber)
console.log(arrNew)
// outputs [ 1, 4, 9, 16 ]

So we we apply to an array: a function that takes a callback, and returns a new array. The callback is a function that takes an input value and usually transforms that input value into something else.

Array.prototype.myMap = function(callback) {  
let ret = []
for (var i = 0; i < this.length; i++) {
let newVal = callback(this[i])
ret.push(newVal)
}
return ret
}

We iterate through the array (represented by this), apply the callback function, and push the resulting value onto a new array. And subsequently return that new array.

We could write this more succinctly using our new myForEach function.

Array.prototype.myMap = function(callback) {  
let ret = []
this.myForEach(val => ret.push(callback(val)))
return ret
}

Moving on.

myFilter

By now weโ€™re starting to get the hang of it. Here is the filter call signature.

let people = [ 
{ 'name': 'Bob', 'age': 70 },
{ 'name': 'Sue', 'age': 30 },
{ 'name': 'Joe', 'age': 18 } ]
let youngPeople = people.filter( person => person.age < 69 )
// returns array with two objects.
// [ { 'name': 'Sue', 'age': 30 },
// { 'name': 'Joe', 'age': 18 } ]
// or
const isYoung = person => person.age < 59
let youngPeople = people.filter(isYoung)

So the filter expects a callback which returns a true or false, also referred to as a predicate. The function returns a new array. Based on our experience we can quickly whip this up.

Array.prototype.myFilter = function(callback) {
let ret = []
this.myForEach( (val) => {
if (callback(val) === true) ret.push(val)
}
return ret
}

Pretty straight forward. Note I could have written if (callback(val)) instead of if (callback(val) === true), but I did this to emphasize that we expect the callback to return true or false.

myReduce

The reduce function is a little more complex to grok. The textbook example of reduce is to sum elements of an array. This is what it would look like.

let arr = [ 1, 2, 3, 4 ]
let sum = arr.reduce( (acc, val) => acc + val )
console.log(sum)
// prints out 10

The callback takes two arguments, the first one is often referred to as the accumulator, and the second one is the value of a particular item of the array. In this example, the accumulator keeps a running total, and with each iteration over the array, its value is added to the accumulator. And at the end the accumulator contains the value we are trying to get.

This is what the function looks like in its most basic form.

Array.prototype.myReduce = function (callback) {
let ret
this.myForEach( val => {
if (ret === undefined) ret = val
else ret = callback(ret, val)
}
return ret
}

Itโ€™s a little trickier than the other higher order functions we defined above. Note how we need to check of ret is undefined or not. This brings up another point. The JavaScript reduce function lets you specify an initial value. Thatโ€™s not hard to add to our function. We just add an init parameter to the function definition and set the ret variable to init initially.

Array.prototype.myReduce = function (callback, init) {
let ret = init
this.myForEach( val => {
if (ret === undefined) ret = val
else ret = callback(ret, val)
}
return ret
}
const sumTwoValues = (a, b) => a + b
let sum = arr.myReduce( sumTwoValues , 0 ) // added 0 as an argument for initial value.
console.log(sum)
// prints 10

Improvements

For each of the functions above, we could make it such that we include the index of the array element in the callback signature and make it available in the callback. This should allow us to do the following for example.

var arr = [ 1, 2, 3, 4, 5, 6 ]
arr.forEach( (val, idx) => { if (idx % 2 === 0) console.log(val) }
// prints the value of every other item in the array.
arr.map( (val, idx) => idx % 2 === 0 ? val * 2 : val * 3 )
// create a new array where every even item has been doubled and every odd item has been trippled
arr.map( (val, idx) => idx == 0) 
// return an array with only the first item

The implementation is quite easy. In our myForEach function, we pass the value and the index to the callback. And since we use myForEach in all the other functions, we can easily include the index in the callback call.

Array.prototype.myForEach = function(callback) {
for (var i = 0; i < this.length; i++) {
callback(this[i], i)
}
}
Array.prototype.myMap = function(callback) {
let ret = []
this.myForEach((val, idx) => ret.push(callback(val, idx)))
return ret
}
Array.prototype.myFilter = function(callback) {
let ret = []
this.myForEach((val, idx) => { if (callback(val, idx)) ret.push(val)} )
return ret
}
Array.prototype.myReduce = function(callback, init) {
let ret = init
this.myForEach( ( val, idx ) => {
if (ret === undefined) ret = val
else ret = callback(ret, val, idx)
})
return ret
}

One more thing we should do is some error checking. For instance we should make sure that the callback is a function. We should check that the array is not null.

Array.prototype.myForEach = function(callback) {
if (this == null) {
throw new TypeError('array is null')
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function'
}

for (var i = 0; i < this.length; i++) {
callback(this[i], i)
}
}
Array.prototype.myMap = function(callback) {
if (this == null) {
throw new TypeError('array is null')
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function'
}
let ret = []
this.myForEach((val, idx) => ret.push(callback(val, idx)))
return ret
}
Array.prototype.myFilter = function(callback) {
if (this == null) {
throw new TypeError('array is null')
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function'
}
let ret = []
this.myForEach((val, idx) => { if (callback(val, idx)) ret.push(val)} )
return ret
}
Array.prototype.myReduce = function(callback, init) {
if (this == null) {
throw new TypeError('array is null')
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function'
}
let ret = init
this.myForEach( ( val, idx ) => {
if (ret === undefined) ret = val
else ret = callback(ret, val, idx)
})
return ret
}

Thatโ€™s the end. The actual Javascript forEach, map, filter and reduce functions are a more complex. But by coding this base implementations, you may better understand how they work under the hood.

If youโ€™ve enjoyed it, send me some love and click the heart below.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.