Fresh Engels

What They Don't Teach You In AP CSP - Lesson One - Rethinking Functions

In a previous post about how the AP Computer Science Principles class at my school failed to teach well, I mentioned that there were many things I wish they had taught differently or at all. That is why I have decided to take matters into my own hands and teach the things you should learn there by myself. One of the most important things, I decided, was to really understand how to use functions.

Before we begin, you should know that I am writing this whole series to match the curriculum that I took. That means we will be using code.org JavaScript and terminology.

With all that out of the way, there's more to functions than you may know. Let's get right into it.

What You Already Know

I was taught about functions in two separate stages.

The first stage was using functions as ways to repeat code verbatim, and only that. For example, in a console program, we may print some menu multiple times. Instead of repeating the same code in two separate places, we make a function that prints the menu and call it in those two places.

The second stage incorporated parameters and return values. Now, instead of writing multiple functions with no parameters that have only minor changes between them, or read a variable from outside the function, we can declare the value(s) we need as parameters of the function. We also no longer need to set a variable from outside the function to get some result when we can just return it.

Although we had been calling predefined functions on day one, there was one function that may have struck you as somewhat unusual. That function is onEvent.

A Quick Reminder About onEvent

You can find the official documentation for onEvent here, but I'll put what's important up here. The usual format of a call to onEvent is as follows:

onEvent(id, event, function (eventInfo) {
    ...
});

It registers an event listener on id, listening for event event, and when that event occurs, it will run the callback, which is passed information about the event, although the information was often ignored. The callback is run every time the event occurs.

The most common use of onEvent in my class was to have something happen every time a button is clicked.

Why onEvent is Unusual

Ever wonder why when you call onEvent, you write function () { ... } as an argument? Writing a function as an argument was not used anywhere else in my curriculum. Even more unusual, this is like a regular function definition but it isn't given a name, just used inline. How does that all work?

A Note on Type Signatures

Any type definition followed by angle brackets and some generic type names (Like T, K, V, and R) is a generic type. Generic types can be used multiple times with different types while still remaining type-safe. Generic types are then instantiated with actual types. For example, Dictionary<K, V> is generic and has two types K and V that need to be specified. You can create a Dictionary<Number, bool> and treat all Ks as Numbers and all Vs as bools for that instance.

Dictionary<K, V> is my representation of a JS object that maps values of type K to values of type V. For example, Dictionary<string, Number> yields a Number when given a string (assuming the key was valid, otherwise yields undefined).

* is the Cartesian Product, which results in a type that represents all possible combinations of the values of the two types. For example, you can think of string * Number as a pair, where the first element is any string and the second element is any Number.

(T1, T2, T3, ... Tn) -> R represents a function that takes n arguments with the types specified, and returns a value of type R.

() -> R represents a function that takes no parameters and returns a value of type R.

(T1, T2, T3, ..., Tn) -> () represents a function that takes n arguments with the types specified, and returns nothing.

() -> () represents a function that takes no parameters and returns nothing.

Functions as First-Class Citizens

The first thing to know is that in JavaScript, functions are first-class citizens. that means we can pass them around, produce them, and consume them, all the same way as other data types like Number. This is an invaluable feature I use for most of my JavaScript programs.

One of the reasons that technique is useful is because it allows you to determine what function is used dynamically, and not having to worry about the implementation of the function, only the interface (or signature).

Here's a simple calculator I wrote in this style. Note that although I used var to conform to code.org JS, all of these are variables are constant.

var operators = {
  "add": new NamedItem("+", function (n1, n2) { return n1 + n2; }),
  "subtract": new NamedItem("-", function (n1, n2) { return n1 - n2; }),
  "multiply": new NamedItem("*", function (n1, n2) { return n1 * n2; }),
  "divide": new NamedItem("/", function (n1, n2) { return n1 / n2; }),
};

var n1 = promptNum("Enter number one");
var n2 = promptNum("Enter number two");
var operator = prompt("What do you want to do? (" + Object.keys(operators).join(", ") + ")").toLowerCase();
var chosenOperator = operators[operator];

if (typeof chosenOperator === "undefined")
    console.log("Don't make me add validation to this simple demonstration!");
else
    console.log(n1 + " " + chosenOperator.name + " " + n2 + " = " + chosenOperator.item(n1, n2));

function NamedItem(name, item) {
  this.name = name;
  this.item = item;
}

Note how we determine which function is used by indexing a dictionary of type Dictionary<string, NamedItem<(Number, Number) -> Number>> (NamedItem<T> is string * T). This storage of functions as first class citizens allows us not to worry about which function is called, we only care that it has type (Number, Number) -> Number.

This idea is often summarized as "program to an interface, not an implementation." Without it, we would have to use branching (if statements) to choose and run the function we want. This is cumbersome and hard to modify.

But when we take advantage of functions being first-class citizens in JavaScript, all we need to do to change the behavior of some code is modify data. We could also have different data configurations with different function implementations and pass the different configurations into the same function.

By not concerning ourself with what function is called, we gain a lot in terms of flexibility.

Anonymous (Lambda) Functions

Let's take another look at the common style of calls to onEvent:

onEvent(id, event, function (eventInfo) {
    ...
});

It has the signature (string, string, (eventInfo -> ())) -> (). It would seem that any time a signature mentions a function, you must write it inline in this format (function (t1, t2, t3, ..., tn) { ... }), which makes it an anonymous, or lambda, function, but that's not correct. In fact, this:

onEvent('myButton', 'click', function () {
    console.log('myButton was clicked!');
});

is as legal as this:

function myButtonClickHandler() {
    console.log('myButton was clicked!');
}

onEvent('myButton', 'click', myButtonClickHandler); // Note the lack of parenthesis, we are not calling this function, only referencing it.

and this:

var myButtonClickHandler = function () {
    console.log('myButton was clicked!');
}

onEvent('myButton', 'click', myButtonClickHandler); // Again, no parenthesis.

Understanding this is quite simple - the second example with a named function is functionally (heh) equivalent to the third example, assigning a variable with an anonymous function.

When you use the name of a variable in an expression, it evaluates to the content of the variable. Since the content of the variable in the third example is a function, it evaluates to that function. Since the variable just binds an anonymous function to a name, the content of the variable is an anonymous function. Therefore, the third example is equivalent to the first example.

A named function is just an anonymous function with a name. You can pass around named functions in the same place you may write a lambda.

Higher-Order Functions

There's a name for functions that produce and/or consume other functions. These functions are called Higher-Order Functions (HOFs). onEvent is a HOF because it takes a function as an argument. It is a consuming HOF.

What about a producer? Well, here's a simple example:

function makeFiveAdder() {
    return function (n) {
        return 5 + n;
    };
}

we can use it like so:

var addFive = makeFiveAdder();
console.log(addFive(5)) // => 10;

Obviously, this isn't very useful. There's no need to have multiple five-adders. But what if it wasn't always just five?

function addCurry(n1) {
    return function (n2) {
        return n1 + n2;
    };
}

The signature of addCurry is Number -> Number -> Number. That is, addCurry takes a Number and returns a function that takes a Number and returns a Number.

Now we can use it like so:

console.log(addCurry(5)(5)) // => 10

var addFive = addCurry(5); // Type: Number -> Number
console.log(addFive(5)) // => 10

This works because functions can read values from the enclosing scope. A function that produces a function that reads data passed as a parameter to the outer function is called a closure, because it encloses a value that is used in the inner function. This is an example of that, but it's also an example of another concept called currying (which is why I called the function addCurry), where you transform a function with n parameters into a series of functions each taking one parameter. Here, we transformed binary addition ((Number, Number) -> Number) into its curried form (Number -> Number -> Number).

Both of these techniques allow for partial function application, where we don't call the function with all parameters at once, but instead use the result of a function that generates a function (that may generate a new function and so on or the result we're looking for) that we can use at some other time, without needing to pass it all parameters at that time. These features are exceptionally useful for many different tasks. For example, we can make similar event listeners with a closure that returns a function ready to be called when the event occurs. The possibilities are endless.

Next time you work on a programming project, consider how you can use the ideas I've described here to make your code cleaner, easier to understand, and more maintainable.

You will think about the solution in a whole new way because of this paradigm shift, and there is nothing more valuable than training yourself to think in new ways.