Advanced Functions

In this course, we will be using functions in a more sophisticated way than you did in CS 111. (You can do these same ideas in Java and Python, too, but you probably didn't.) This will take a lot of getting used do, and you'll make plenty of mistakes, but that's to be expected. Let's start with a few ideas.

First Class Functions

JavaScript functions are first-class values, so they can be stored in variables, passed into functions and returned as values of functions, and so forth. See the Wikipedia entry on First-Class Functions if you'd like to learn more.

What does that mean? Let's get there in a few steps, starting with anonymous functions. That's a function that is a bunch of code, but without a name.

Anonymous Functions

Functions without function names are called anonymous functions. In Python, you use lambda to create anonymous functions, but most versions of CS 111 don't cover lambda, so we'll omit lambda here. However, you may have seen lambda in the context of a key function for sorting, like this:

# python code
student_tuples = [
    ('john', 'A', 15),
    ('jane', 'B', 12),
    ('dave', 'B', 10)
]
sorted( student_tuples, key=lambda student: student[2]) # sort by age

To create an anonymous function in JavaScript, simply use the keyword function without the function name. This syntax is called a function literal or a function expression, and so you would typically put it on the right hand side of an assignment (like other expressions) or put it in the parentheses of a function call. Here's an example:

let f = function (a, b) {return a * b};

This is a screen shot from when I typed that into my browser console. Feel free to do the same in your browser and see what it looks like for you. (For the last part, I clicked on the gray triangle in the blue block to "toggle" open the output.)

screenshot of function f

How would we use such a function? Believe it or not, we use the variable f in the same way we use a function name, by following it with parentheses and arguments:

> f(3,4);
12

Here's another screenshot:

screenshot of f(3,4)

The reason this is important is that it allows us to think about defining a function and then giving the function to some other function to execute. (That's what we did with the key function in the example of Python's sorted function, above: the lambda function was passed to sorted to be used in the sorting algorithm.)

Sorting is complicated, but let's consider a simpler example: doing something with every element of an array.

The MAP method on Arrays

There are methods to operate on each element of an array, particularly the map method. For example, if I have a list of numbers, and I want a list of their squares, I can do this:

function square(x) {
    return x*x;
}

let nums = [1, 2, 3, 4];
let squares = nums.map(square); 
alert(squares);  // [1, 4, 9, 16]

This is complicated and very new, conceptually, so let's take it one step at a time.

First, we defined a square function in the normal way. Nothing surprising there, and we can use it in the normal way:

>> square(3);
9

Second, we created an array of numbers. Again, this isn't anything new; it's just like Python and similar to Java:

let nums = [1, 2, 3, 4];

The last step is the weird one. Because nums is an array, it has a method called map. The map method takes one argument, which has to be a function. That is, we have to pass a function to map to do all the work.

In fact, this function can't be just any function: it has to be a function that works on one argument. So, we couldn't use our function f above, because that requires two arguments. But square is a function of one argument, so we can pass it to the map method, which then gives us back a new array of the results:

let squares = nums.map(square);

And squares looks like [1, 4, 9, 16].

Passing Anonymous Functions

Here's the same code as above, but this time using an anonymous function instead of defining square:

let nums = [1, 2, 3, 4];
let squares = nums.map(function (x) { return x*x }); 
alert(squares); // [1, 4, 9, 16]

The nice thing about using the anonymous function literal as the argument to the .map() method is that all the code is right there; you don't have to look elsewhere for the function code.

Another nice thing about using an anonymous function is that if I have a weird function, I don't have to think of a name for it, I just have to code it. Here's a function that triples a number and adds 1:

let nums = [1, 2, 3, 4];
let vals = nums.map(function (x) { return 3*x+1; });

The result, vals, will contain [ 4, 7, 10, 13 ]. Copy/paste the code above into your browser console if you'd like.

Named Versus Anonymous Functions

A reasonable question is why you'd use an anonymous function instead of a named function. So, let's start with why you'd use a named function.

Named functions are very useful when you want to

  • define the function once, and then
  • call it from many places using the name

The name allows you to refer to it from somewhere else, and a good name means that the code is more readable and understandable. These are all good things.

This is similar to why we might give names to values (by putting them in variables). For example, the quadratic formula uses a value twice to find two roots:

let shared = Math.sqrt(b*b - 4*a*c);
let root1 = (-1*b + shared)/(2*a);
let root2 = (-1*b - shared)/(2*a);

But you might agree that if you only use a value once, maybe it's not worth giving a name to it. For example, you'd probably write the Pythagorean Theorem like this:

let hypotenuse = Math.sqrt(a*a + b*b);

and not like this:

let a2 = a*a;
let b2 = b*b;
let c2 = a2 + b2;
let hypotenuse = Math.sqrt(c2);

Similarly, anonymous functions are most useful when you only need the function once. So, rather than name it and then call it from somewhere else, you just write the code right where you need it.

Very often, the place where you need the function is as the argument to another function. Since giving a function to the browser is an important thing in web programming, we use anonymous functions all the time.

The forEach Method

A method similar to .map is forEach(), which takes a function of up to three arguments: the item, its index in the array, and the array itself. In practice, the third argument is often ignored, and we will omit it here. (Remember that JavaScript allows you to invoke a function with more arguments than it's declared to take. The extra arguments are just ignored. Here, if we supply an anonymous function that takes only two arguments, it means we aren't interested in the third argument — the array.)

The following prints out the contents of an array, using an anonymous function, and ignoring the third argument.

let primes = [2, 3, 5, 7, 11];
primes.forEach(function (item, index) {
     console.log(index+': '+item);
});

Again, try it! Here's what it did for me:

printing an array of primes using forEach

(The final gray "undefined" in the screenshot is the return value of the forEach method. The forEach method is used for what is does, not for what it returns. It always returns undefined.)

Here's another example: we want to convert every element of an array of strings to uppercase. We can use forEach with a three-argument function, assigning the new value to the current array element:

let quote = ['be', 'the', 'change', 'you', 'want'];
quote.forEach(function (str, index, arr) { arr[index] = str.toUpperCase(); });
console.log(quote);  // ['BE', 'THE', 'CHANGE', 'YOU', 'WANT']

What this code does to ask forEach to apply the anonymous function (we can think of it as the uppercasing function) to each array element in turn. JS invokes our function with the array element (str), its index in the array (index), and the array itself (arr). Our function replaces the array element with the uppercase version of the string.

If we don't want to modify the array, we typically omit the third argument. So the following examples use alert to tell us something about each element of the array, in which case we need only the element (str) or the element and its index (index):

let quote = ['be', 'the', 'change', 'you', 'want'];
quote.forEach(function (str) { alert(str.toUpperCase()); });
quote.forEach(function (str, index) { alert(index+': '+str.toUpperCase()); });

(Try it!)

We will use the forEach method in an upcoming version of Ottergram, and we will use the idea of passing functions as arguments many times in this course.

You can use named or anonymous functions; they are nearly (but not quite) interchangeable. Here's the same code with a named function:

function printItemAndIndex(item, index) {
    console.log(index+': '+item);
}

let primes = [2, 3, 5, 7, 11];
primes.forEach(printItemAndIndex);

Ottergram will use an anonymous function in a way that is not easily replaced with a named function, but we'll cross that bridge when we get to it. For now, understand that anonymous functions are useful and not that weird. I urge you to try to get used to them. Modern web development has lots of anonymous functions.

We'll see how anonymous functions can be used in more practical applications later.

forEach versus Loops

You're probably thinking that in Python or Java you would use a for loop to do the kinds of things we did using forEach. Indeed, JavaScript has loops very similar to every other language. However, we will not be using those in this course, for two important reasons:

  • forEach allows us to use a named function if one is handy or convenient, and
  • using forEach gives us practice with passing functions as arguments, which is an important skill in modern programming.

If it helps, you can think of the anonymous function as the body of the for loop: it's executed once for each item of the array.

Callback Functions

When a function f is passed as an argument to a function/method, g, the function f is sometimes described as a callback function, since g will be calling f. For example, look at the MDN description of forEach

Passing Functions in Web Applications

The reason that understanding functions as first-class values and passing them as arguments is important is because of how we will be using functions in web programming. A lot of what we will do will be defining a function and then passing it to the browser, for the browser to invoke. This is similar to passing square to the map function.

To use the terminology of the previous section, web programming often involves writing callback functions.

Functions as Return Values

We've passed functions as arguments to other functions. We can also return functions from functions. Here's an example:

function addRand(max) {
    let rand = Math.floor(max*Math.random());
    return function (n) { return n+rand; };
}

In action:

x = [1, 2, 3, 4, 5];
f1 = addRand(5);
f2 = addRand(100);

A few examples:

f1(1);
f1(2);
y1 = x.map(f1);
y2 = x.map(f2);

So, the return values remember the value of rand that they computed and they use it consistently, every time.

Arrow Functions

A relatively new feature of JavaScript is the ability to define arrow functions, which are even more succinct but also have some technical differences that we'll get into later. Arrow functions can only be one expression, which is returned.

primes = [2, 3, 5, 7 ];
prime_squares = primes.map( x => x*x );
alert(prime_squares); // [ 4, 9, 25, 49 ]
neg_primes = primes.map( x => -1*x );
alert(neg_primes);  // [ -2, -3, -5, -7 ]

I don't find the extra typing for normal functions to be burdensome, so I haven't gone whole hog for arrow functions, but there are a few times when they are really nice. You should add them to your toolkit.