Chapter 3Functions
In the previous chapter, we were introduced to how to write
functions. We saw some simple ones that manipulated numbers, like
square
:
function square(x) { return x * x; }
And we defined functions that produced images that we then used to construct animations. In this chapter, we are going to take a much deeper dive into functions, and focus on how we design functions in order to solve problems. On the way, we’ll explore programming in a more thorough way, and develop some concrete tools that we can use to approach problems.
Designing Functions
When we introduced functions in the last chapter, we described them (first) as a way to produce values that differed. But, there are other ways of thinking about them. Functions can also reduce duplication (as we saw in the example with drawing the frames of the bike motion). In this chapter we want discuss the process of actually creating them, as a creative design process.
Fundamentally, a function is a way of transforming one piece of
information into another. In the example of square
above, the first
piece of information (or the input) was a number, and the second (or
the output) was that number multiplied by itself. This relationship
(between a number and it multiplied by itself) matches what we think
of as squaring, so the name matches it well.
All programming is about similar transformations. And, it is mostly about the structure (ie, what it looks like, what it contains) of that information. Up until now, we have introduced different types of data that we can represent with javascript (like numbers, strings, images, etc) but haven’t talked much about how we choose what representation to use for a given task. It may seem, up until now, that there isn’t really any choice - if we are dealing with a number, we represent it as a number, and so on. But, we are usually writing programs about bigger ideas than just numbers or strings.
For example, let’s say we wanted to represent our friend "Austin" in
our program. How should we do it? We could use the string "Austin"
,
but later on, it’s possible that we would want to represent another
"Austin" in our program, and that might be problematic. Or we might
realize that we want to send an email to them, so maybe we should
represent them by the string "austin@tlcjs.org", which is their
(fictional, at least for now!) email address. An email address seems
more unique, but what if "Austin" changes their email? Perhaps we
should just represent "Austin", and any other people we have to deal
with in our program, by (unique) numbers (for example, "Austin" could
be 42
), and if we need their email (or one of their emails) we can
store that somewhere else, connected to 42
, our unique number for
"Austin".
Of course, we have the same problem in the opposite direction - if we have the number 42, what does it represent? It could be the number of members of TLC, it could be the age of someone, it could be someones favorite number, and any number of other things.
Figuring out what data exist in our program, and how it relates to the (real) problem we are trying to solve is a critical part of programming, and more concretely, designing functions, because all functions have input and output data.
Design Recipe
The first step in the Design Recipe (which is what this chapter is teaching) is to figure out what data you need to solve your problem. You should describe what information you have, and how you will represent it in the program. This should be in comments, as it is crucial information both for yourself and anyone else who will see this later.
As an example problem (which we’ll follow through the whole recipe), we want to figure out how a hypothetical bake sale is going to work.
We want to raise money by making lemonade and cookies (which we sell at 25c/cup and 50c/cookie), but unfortunately, the only place we have to sell them is run by a capitalist who makes us pay to use the space. They have two options we can choose between:
The function we’ll construct will take as input whether we want to pay the flat $50 fee, and how many cups of lemonade / cookies we sell. It’ll give back our total profit.
Thinking about the type of information that we need, and how we’ll represent it, we come up with:
// The number of cookies, glasses of lemonade are numbers. // Whether we will pay the flat fee is a boolean (we either // will or won't). We'll return a number in cents, as our // prices are in cents.
For this problem, it may seem pretty trivial - but in many problems it isn’t always clear how to represent a given part of the problem (and not understanding is a good way to get lost!).
The second step is to figure out what the input and output to our function will be. In the first step, we should have figured out all the types of data that will appear, but we still have to define exactly what the signature of the function is, where a signature is a description of the type of all the inputs and the type of the output of a function.
You should also write this as a comment, as it is similarly very helpful information for yourself and others who might be looking at your program (or trying to change it). In our bake sale example, we end up with the following signature:
// number, number, boolean -> number
Where that is notation for a function taking three inputs: a number (cups of lemonade sold), another number (number of cookies sold), and a boolean (true means we pay the flat fee) and producing a number (our total profit) as output.
The third step is writing down the purpose of the function, in human language. Up until now, everything we have been writing down has been about the external attributes of the function - what it takes in and what it produces. There may be many different functions that have those inputs/outputs. This is the step where we describe, in the simplest possible way (but still, precisely), what the function should actually do. In our case, we might write:
// Takes the number of lemonades * 25 plus the number of // cookies * 50 and then subtracts either 5000 (if the // flat fee is chosen) or 10% of this total (if flat fee not // chosen). This number, which is the number of cents, is // what we return.
The fourth step is to write down the header for the function, which is the name, arguments, and some stub for the return value (where a stub is a value that matches the signature, but is as simple as you can think up). Part of this step involves coming up with names for the function and all the arguments. In this case, we might write:
function bakeSale(lemonades, cookies, useFixedCost) { return 0; }
Note that for the stub, I could have easily written 2
, or 1000
(which are all numbers). The point of the stub is not to have a
sensible value, just to have something that matches the right type of
data that your signature specified.
The fifth step is to write down example uses of your function, which are usually called "tests" (as they "test" that your function behaves properly).
function bakeSale(lemonades, cookies, useFixedCost) { return 0; } shouldEqual(bakeSale(0,0,true), -5000); shouldEqual(bakeSale(0,0,false), 0); shouldEqual(bakeSale(200, 0, true), 0); shouldEqual(bakeSale(200, 200, true), 10000); shouldEqual(bakeSale(200, 200, false), 13500);
When writing tests, it is good to think both of the common cases (for example, selling a few hundred of each lemonade and cookies), but also the strange cases (like selling nothing at all, or all of one product).
When you run your program (which, at this point, you should), you’ll see a report of how many tests ran and passed (and if they failed, you’ll get a report of what went wrong). When you run them in files that you save, you’ll get the line number when they fail (but that’s not available within the in-text editors).
In this case, most of them failed (the ones that passed happened to
work because we used 0
as our stub, so like a broken clock that is
right twice a day, our always-returning-0 function is sometimes
right!). This is expected! Since you haven’t written the body of the
function yet, the tests should be failing. If they weren’t, they
couldn’t be checking much!
The sixth step is to fill in the body of the function with a template for each of the arguments. A template is a bit of code that is how you would normally use the particular value. Each type of data has its own template. Of the types of data that we’ve seen so far:
The first three have a very simple template - it is just the
identifier of the argument. The template for booleans is an if
expression. So if we had a function:
// (1) Involves booleans. // (2) Boolean -> Boolean // (3) This function inverts a boolean value, turning true // to false and vice versa. function not(b) { // (4) return true; } // (5) shouldEqual(not(true), false); shouldEqual(not(false), true);
Where I’ve identified each step of the design recipe completed thus
far. To example the template for b
(the only argument), we would write:
// (1) Involves booleans. // (2) Boolean -> Boolean // (3) This function inverts a boolean value, turning true to // false and vice versa. function not(b) { // (4) if (b) { // ... } else { // ... } return true; } // (5) shouldEqual(not(true), false); shouldEqual(not(false), true);
The purpose of the template is to give you a place to start - if you
have an argument that is a boolean, you are probably going to have an
if
somewhere. For our function, we have three arguments, so we have
three templates to fill in. The first two are numbers, so the template
is just the identifier, but the third one is a boolean, so it has a
template similar to not
above.
// The number of cookies, glasses of lemonade are numbers. // Whether we will pay the flat fee is a boolean (we either // will or won't). We'll return a number in cents, as our // prices are in cents. // number, number, boolean -> number // Takes the number of lemonades * 25 plus the number of // cookies * 50 and then subtracts either 5000 (if the flat // fee is chosen) or 10% // of this total (if flat fee not chosen). This number, // which is the number of cents, is what we return. function bakeSale(lemonades, cookies, useFixedCost) { // ... lemonades; // ... cookies; // ... if (useFixedCost) { // ... } else { // ... } return 0; } shouldEqual(bakeSale(0, 0, true), -5000); shouldEqual(bakeSale(0, 0, false), 0); shouldEqual(bakeSale(200, 0, true), 0); shouldEqual(bakeSale(200, 200, true), 10000); shouldEqual(bakeSale(200, 200, false), 13500);
Now that we have all the parts on the page (so to speak), we can write the body. The templates are a guide, and can be removed if you end up not needing them (or using them in a different way), but they are often a useful way to start
Often, the best place to look for guidance of how to finish the program is either the purpose statement (which states, in english, what it should do), or the tests (which shows us some examples of what it has done). Using the purpose statement, we know that to calculate the total cents, we multiply the number of lemonades by 25 and the number of cookies by 50. That gets us further (and we have used up two of our templates):
// The number of cookies, glasses of lemonade are numbers. // Whether we will pay the flat fee is a boolean (we either // will or won't). We'll return a number in cents, as our // prices are in cents. // number, number, boolean -> number // Takes the number of lemonades * 25 plus the number of // cookies * 50 and then subtracts either 5000 (if the flat // fee is chosen) or 10% // of this total (if flat fee not chosen). This number, // which is the number of cents, is what we return. function bakeSale(lemonades, cookies, useFixedCost) { var total = lemonades * 25 + cookies * 50; if (useFixedCost) { // ... } else { // ... } return 0; } shouldEqual(bakeSale(0,0,true), -5000); shouldEqual(bakeSale(0,0,false), 0); shouldEqual(bakeSale(200, 0, true), 0); shouldEqual(bakeSale(200, 200, true), 10000); shouldEqual(bakeSale(200, 200, false), 13500);
Now we want to use the useFixedCost
template, so we have to think
what happens if we want to use the fixed cost option (which is that we
have to pay a fixed 5000 cents) and what happens if we don’t (which is
that we lose 10%, or multiply the total by 0.9). This lets us complete
the function:
// The number of cookies, glasses of lemonade are numbers. // Whether we will pay the flat fee is a boolean (we either will or won't). // We'll return a number in cents, as our prices are in cents. // number, number, boolean -> number // Takes the number of lemonades * 25 plus the number of cookies * 50 // and then subtracts either 5000 (if the flat fee is chosen) or 10% // of this total (if flat fee not chosen). This number, which is the // number of cents, is what we return. function bakeSale(lemonades, cookies, useFixedCost) { var total = lemonades * 25 + cookies * 50; if (useFixedCost) { return total - 5000; } else { return total * 0.9; } return 0; } shouldEqual(bakeSale(0,0,true), -5000); shouldEqual(bakeSale(0,0,false), 0); shouldEqual(bakeSale(200, 0, true), 0); shouldEqual(bakeSale(200, 200, true), 10000); shouldEqual(bakeSale(200, 200, false), 13500);
As you are writing the function, you can continue to run your tests. Often, if you have many tests, some of them will start to pass as you write the function. In our case, all our tests are now passing!
Summary of Design Recipe
Exercises
// Involves booleans. // Boolean -> Boolean // This function inverts a boolean value, turning true to false and vice versa. function not(b) { if (b) { // ... } else { // ... } } shouldEqual(not(true), false); shouldEqual(not(false), true);
// A person's name as a string. // String -> String // This function will enthusiastically greet a person by name. // If the name is blank, return "Hello!!" function enthusiasticallyGreet(name) { if (false) { // replace false with test that name is empty string! // ... } else { // ... } } shouldEqual(enthusiasticallyGreet("Austin"), "Hello, Austin!!"); shouldEqual(enthusiasticallyGreet("Pea"), "Hello, Pea!!"); shouldEqual(enthusiasticallyGreet(""), "Hello!!");
// Involves numbers // Number -> Number -> Number // This function takes two numbers as input and return the // one that is smaller. function min(x, y) { //.. } // Your tests here.
Using that function, can you use the design recipe to "center" a
string within a larger space. For example, center(4, "hi")
would
return " hi "
. Once you get a version working, you might work on
figuring out what to do if the space specified is shorter than the
string, or if the amount of space can’t be even on both sides.
// image -> number
For example, we can use this to get back the width of a rectangle we created:
print(width(rectangle(100,10,"red")));
Using this, write a function area
that takes an image as input and
returns the total number of pixels in the image.
-
One more: A B52 bomber costs about $2 billion dollars (that’s nine zeroes). That is a number that is so large, it’s hard to wrap your head around it. Often activists will try saying, "For the cost of one B52 bomber, you could pay for this many of this thing." Create a function that will help an activist generate statements like this by taking the name of an item (like "year of college") and a cost per item ($23,872), and return a statement about how many of those items could be obtained for two billion dollars.
// Data // Type // Function description. function b52Equivalence(item, cost) { // ... } // Your tests here.