Higher Order Functions Continued (array methods, function methods)
Higher Order Functions
Greeeaaaat. What's a higher order function, though? →
A higher order function is a function that does at least one of the following things: →
- accepts a function or functions as a parameter
- returns a function
Array Methods
We learned about (and even re-implemented) four array methods that accepted functions as arguments (specifically as callback functions). What were these array methods, and what do they do? →
- forEach - calls callback on each array element
- filter - returns a new filtered array based on test/callback
- map - returns a new transformed array based on callback
- reduce - reduces original array to single value based on callback
forEach
An array's forEach method executes a callback on every array element.
- 1 parameter - a callback function that is executed with the current element, index and original array
- returns undefined
- example →
// log double the value of every element in the array
const numbers = [3, 4, 5];
numbers.forEach(function(element) {
console.log(element);
});
Also, our homegrown implementation as a standalone function.
function forEach(arr, action) {
for (let i = 0; i < arr.length; i++) {
action(arr[i]);
}
}
Transforming with Map
Create a function called map that creates a new array based on calling a function on each element of an array passed in: →
- 2 parameters - an array to base new array off of and a function to transform each element
- returns a new array with each element of original transformed by callback function
- test it by creating an array of words and transforming that array into an array of words each with two exclamation points at the end
What would the algorithm be?→
- create a new array to hold the transformed elements
- go over every element in the original array
- call the function on each element
- add the result of calling the function to the other array
Map Continued
Here's a potential implementation, along with example usage, of our own map
implementation. →
function map(arr, transform) {
const transformed = [];
arr.forEach(function(element) {
transformed.push(transform(element));
});
return transformed;
}
const result = map(['hello', 'hey', 'hi'], function(greeting) {return greeting + '!!'});
console.log(result);
Using an Array's Map Method
Again, JavaScript arrays already have a map method…
- 1 parameter - a callback (the function that transforms each element)
- the callback is executed with the current value, index and original array
- the callback returns a new value/object to be added
- map returns a new array with every element transformed
Try using it to change every word in the list ['hey','yo','sup']
to uppercase with an exclamation point. →
words = ['hey', 'yo', 'sup']
const shoutedWords = words.map(function(word) {
return word.toUpperCase() + '!';
});
console.log(shoutedWords);
Reducing an Array to a Single Value
Create a function called reduce… that repeatedly calls a function to reduce an array to a single value. →
- 3 parameters
- the original array
- a callback function to perform the reduction
- a start value to initialize the variable that will hold the single value to be returned
- the callback should
- take both the current accumulated value, and the current element
- return the new accumulated value
- an example in the next slide… →
Reduce Continued
// result is 20
console.log(reduce([4, 12, 5], function(accum, ele) {
return accum + ele;
}, 0));
What do you think the algorithm for reduce would look like? →
- create a variable, an accumulator, that will be returned
- initialize it to start
- for every element in the original array…
- apply the callback…
- set the accumulator to the result of the callback
- return the accumulator
Reduce Continued
Here's an example of finding the minimum (uses first element as initial min) with reduce:
const numbers = [-5, -2, -1, -10, -3];
console.log(reduce(numbers, function(accum, ele) {
if(accum < ele) {
return accum;
} else {
return ele;
}
}, numbers[0]));
Using an Array's Reduce Method
JavaScript arrays have a built-in reduce method. (Note that in other functional languages, reduce is sometimes called fold.)
- 2 parameters a callback function (the function that reduces the array) and the optional start value of the accumulator (if the start doesn't exist, it uses the first element of the array that it was called on)
- callback is executed with accumulator, element value, element index and original array object
- callback returns a value (the new value for the internal accumulator)
- reduce returns a single value (that value can be an Array, Object, etc.)
Try using it to calculate the product of all of the elements in [2, 5, 4, 3,]
. →
[2, 5, 4, 3,].reduce(function(product, currentNumber ){
return product * currentNumber;
}, 1);
An Aside on Arrow Function Usage
For now, we'll use arrow functions as: →
- a quick way of creating anonymous callback functions…
- for example, if we need to pass a one-time use function as an argument to a higher order function (like map):
const nums = [1, 2, 3, 4, 5]; console.log(nums.filter(x => x % 2 === 0));
- or… occasionally, we can use them to define regular functions as well:
const isEven = (x) => {return x % 2 === 0;};
- we'll see later that arrow functions are sometimes useful because the
this
value within its body is the same asthis
in the scope that it was created in (this will make more sense when we discussthis
!)
Functions as Objects
Continuing on with the notion that functions are just values or objects… do you think functions also have properties and methods?
Why yes - functions have both properties and methods! For example:
const f = function(x, y, z) {
return x - y + z;
}
console.log(f.length);
Let's check out some methods that you can call on function objects:
bind
apply
call
Methods on Function Objects
So, what do these methods do?
- call - calls a function with given
this
and individual arguments - apply - calls a function with given
this
and array as arguments - bind - creates a new function with given
this
, and optionally with set argument values
(we'll talk about this in a moment)
Call and Apply
Both call
and apply
immediately execute the function that they're called on.
- they differ in the way that arguments are passed to the original function object (
call
passes each argument individually, whileapply
passes an array) - the example below sets
this
to null - we'll see more aboutthis
later
function areaTriangle(base, height) {
return (1 / 2 ) * base * height;
}
const result1 = areaTriangle.call(null, 10, 20);
console.log(result1);
const result2 = areaTriangle.apply(null, [10, 20]);
console.log(result2);
Bind
Unlike the previous methods, bind
doesn't execute the function that it's called on. Instead, bind
:
- takes a
this
argument - and an optional set of fixed parameters
- returns a new function
It's actually an implementation of partial application…
- partial application - fixing a number of arguments to a function, producing another function of smaller arity
- arity - the number of arguments or operands that a function or operation accepts
Bind Example
Fixing the first parameter, called base
, of our function.
const areaTriangleBase100 = areaTriangle.bind(null, 100);
// call with only one argument now
console.log(areaTriangleBase100(3));
Note that you'll also see bind
used to fix/set a function or method's this
.
ES6 Spread and Rest
Hey… so remember the rest operator, ...args
? What was it? →
- if the last named argument of a function has
...
before it (therest
operator) - then the arguments passed in at that position are condensed into a single
Array
- for example:
function f(a, b, ...args) { console.log(args); } f('foo', 'bar', 'baz', 'qux'); // prints out ['baz', 'qux']
- notice that every value after and including the 3rd argument are collected into an Array
- again, this allows for an arbitrary number of trailing arguments to be passed in to a function
- (this is called a variadic function, a function that can have and indefinite number of arguments / arity!)
ES6 Spread and Rest Continued
An operator that shares the same syntax but does the opposite of the rest operator is the spread operator.
- the spread operator takes an Array and breaks it up into parts!
- this can be used in function calls:
f(...someArray)
- as well as in
Array
literals:[1, 2, ...someArray]
Spread Operator in Function Calls
The parameters for res are value and radix… and in this case, we're able to expand the incoming Array to fit the arguments by using the spread operator:
const stuff = ['101', 2];
const res = parseInt(...stuff);
console.log(res);
- the first element of stuff becomes the first argument to
parseInt
, the second becomes the last argument - if there are too few or too many elements in the
Array
, JavaScript will behave as if there were too few or too many arguments - which is to say… fine - excess arguments are ignored and arguments not given a value are undefined…
Spread Operator in Function Calls Continued
What does the following code print out? →
const words = ['foo', 'bar'];
function logThreeThings(a, b, c) {
console.log(a, b, c);
}
logThreeThings(...words);
foo bar undefined
Spread Operator in Array Literals
The spread operator can also be used to expand Arrays within Array literals:
const words = ['foo', 'bar', 'baz'];
const moreWords = ['start', ...words, 'end']
console.log(moreWords);
// [ 'start', 'foo', 'bar', 'baz', 'end']
You can also use the spread operator to make a shallow copy of an Array:
const arrs = [[1, 2], [3, 4]];
const newArrs = [...arrs];
console.log(newArrs); // yay copied!
arrs[0].push('surprise!');
console.log(newArrs); // beware, shallow!
Let's Try Creating a Wrapping Function
How about creating a function that logs how much time a function takes to execute? →
- create a function,
logExecutionTime
, that takes a function,f
, as an argument - it returns a new version of
f
that logs how much time it took to execute (in addition to doing whatever it usually does as a function, of course!) - we can use
console.time
andconsole.timeEnd
to take the timings
Function Timing Implementation
Here's one possible way to do it. →
- create a function that just calls the function passed into it
- and gives back whatever that function returns
- the only extra thing that it will do is take the time before and after the function call
function logExecutionTime(f) {
return function(arg) {
console.time('function timing');
const val = f(arg);
console.timeEnd('function timing');
return val;
};
}
function wasteTime(limit) { for(let i=0;i < limit; i++) { }}
wasteTime = logExecutionTime(wasteTime);
wasteTime(5000000);
Another Look at Function Timing
Hm. So… there's a limitation with regards to the kinds of functions that we can time. Can you spot it? →
Hint: How would it handle the following function? →
function wasteTime2(start, limit) {
for(let i = start; i < limit; i++) { }
}
What if the timed function needs more than one argument? →
function logExecutionTime(f) {
return function(...args) {
console.time('function timing');
// use spread and rest
const val = f(...args);
// old way with es5, with apply
// const val = f.apply(null, arguments);
console.timeEnd('function timing');
return val;
};
}
wasteTime = logExecutionTime(wasteTime);
wasteTime(-5000000, 5000000);
Decorators
A function that accepts another function as a parameter, wraps its call in another function, and returns that wrapper as a new function… is called a function decorator.
Why might function decorators be a useful? When would it make sense to implement a decorator rather than to modify an actual function? →
- they allow us to modify how a function runs, even one that we did not write!
- decorators can be reused! for example, you can make any function a timed function, but should you need to change the implementation of timing, you only change it in one place
- there's potential for combining / chaining decorators!